Add Cluster
This commit is contained in:
parent
15397cb459
commit
1468adbc45
26 changed files with 350 additions and 169 deletions
|
@ -1,92 +1,29 @@
|
||||||
$navbar-width: 64px;
|
$header-height: 52px;
|
||||||
|
$navbar-width: 250px;
|
||||||
|
|
||||||
.Layout {
|
.Layout {
|
||||||
width: 100%;
|
&__header {
|
||||||
height: 100%;
|
box-shadow: 0 0.46875rem 2.1875rem rgba(4,9,20,0.03),
|
||||||
display: flex;
|
0 0.9375rem 1.40625rem rgba(4,9,20,0.03),
|
||||||
overflow: hidden;
|
0 0.25rem 0.53125rem rgba(4,9,20,0.05),
|
||||||
position: relative;
|
0 0.125rem 0.1875rem rgba(4,9,20,0.03);
|
||||||
background-color: #F7F7F7;
|
|
||||||
|
|
||||||
&__navbar {
|
|
||||||
display: flex;
|
|
||||||
z-index: 4;
|
|
||||||
flex-direction: column;
|
|
||||||
width: $navbar-width;
|
|
||||||
margin-left: 0;
|
|
||||||
background-color: #192d3e;
|
|
||||||
box-shadow: 0 0 0 0 rgba(0,0,0,0.2), 0 0 0 0 rgba(0,0,0,0.12), 0px 2px 7px 0px rgba(0,0,0,0.2);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&Text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--expanded {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 280px;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.Layout__navbar {
|
|
||||||
&Icon {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&Text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__logo {
|
|
||||||
height: 52px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 52px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
flex: 1 1 auto;
|
margin-top: $header-height;
|
||||||
overflow: auto;
|
margin-left: $navbar-width;
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__navbar {
|
||||||
margin-top: 52px;
|
width: $navbar-width;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
&__header {
|
box-shadow: 7px 0 60px rgba(0,0,0,0.05);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: $header-height;
|
||||||
background-color: #F7F7F7;
|
left: 0;
|
||||||
width: 100%;
|
bottom: 0;
|
||||||
z-index: 3;
|
padding: 20px 20px;
|
||||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
|
||||||
box-shadow: 0 0 0 0 rgba(0,0,0,0.2), 0px 3px 5px 0px rgba(0,0,0,0.1), 0 0 0 0 rgba(0,0,0,0.12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
.Layout__navbar {
|
|
||||||
margin-left: -$navbar-width;
|
|
||||||
|
|
||||||
&--expanded {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
|
||||||
.Layout__navbar {
|
|
||||||
margin-left: -$navbar-width;
|
|
||||||
|
|
||||||
&--expanded {
|
|
||||||
margin-left: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 52px;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,60 +2,47 @@ import React from 'react';
|
||||||
import {
|
import {
|
||||||
Switch,
|
Switch,
|
||||||
Route,
|
Route,
|
||||||
NavLink,
|
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import TopicsContainer from './Topics/TopicsContainer';
|
import TopicsContainer from './Topics/TopicsContainer';
|
||||||
|
import NavConatiner from './Nav/NavConatiner';
|
||||||
|
import PageLoader from './common/PageLoader/PageLoader';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
interface AppProps {
|
||||||
const [expandedNavbar, setExpandedNavbar] = React.useState<boolean>(false);
|
isClusterListFetched: boolean;
|
||||||
const toggleNavbar = () => setExpandedNavbar(!expandedNavbar);
|
fetchClustersList: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: React.FC<AppProps> = ({
|
||||||
|
isClusterListFetched,
|
||||||
|
fetchClustersList,
|
||||||
|
}) => {
|
||||||
|
React.useEffect(() => { fetchClustersList() }, [fetchClustersList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Layout">
|
<div className="Layout">
|
||||||
<aside className={`Layout__navbar ${expandedNavbar && 'Layout__navbar--expanded'}`}>
|
<nav className="navbar is-fixed-top is-white Layout__header" role="navigation" aria-label="main navigation">
|
||||||
<header className="Layout__logo">
|
<div className="navbar-brand">
|
||||||
Kafka UI
|
<a className="navbar-item title is-5 is-marginless" href="/">
|
||||||
</header>
|
Kafka UI
|
||||||
<div className="menu">
|
</a>
|
||||||
<ul className="menu-list">
|
|
||||||
<li>
|
|
||||||
<NavLink exact to="/" activeClassName="is-active">
|
|
||||||
<i className="fas fa-tachometer-alt Layout__navbarIcon"></i>
|
|
||||||
<span className="Layout__navbarText">
|
|
||||||
Dashboard
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<NavLink to="/topics" activeClassName="is-active">
|
|
||||||
<i className="fas fa-stream Layout__navbarIcon"></i>
|
|
||||||
<span className="Layout__navbarText">
|
|
||||||
Topics
|
|
||||||
</span>
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</nav>
|
||||||
<main className="Layout__container">
|
<main className="Layout__container">
|
||||||
<nav className="Layout__header navbar">
|
<NavConatiner className="Layout__navbar" />
|
||||||
<div className="navbar-item">
|
{isClusterListFetched ? (
|
||||||
<a title="Collapse" href="#" onClick={toggleNavbar}>
|
<section className="section">
|
||||||
<span className="icon">
|
<Switch>
|
||||||
<i className="icon fas fa-bars"></i>
|
<Route path="/clusters/:clusterId/topics" component={TopicsContainer} />
|
||||||
</span>
|
<Route exact path="/">
|
||||||
</a>
|
Dashboard
|
||||||
</div>
|
</Route>
|
||||||
</nav>
|
</Switch>
|
||||||
<div className="Layout__content">
|
</section>
|
||||||
<Switch>
|
) : (
|
||||||
<Route path="/topics" component={TopicsContainer} />
|
<PageLoader />
|
||||||
<Route exact path="/">
|
)}
|
||||||
Dashboard
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
17
frontend/src/components/AppContainer.tsx
Normal file
17
frontend/src/components/AppContainer.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
fetchClustersList,
|
||||||
|
} from 'redux/reducers/clusters/thunks';
|
||||||
|
import App from './App';
|
||||||
|
import { getIsClusterListFetched } from 'redux/reducers/clusters/selectors';
|
||||||
|
import { RootState } from 'types';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => ({
|
||||||
|
isClusterListFetched: getIsClusterListFetched(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchClustersList,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
42
frontend/src/components/Nav/ClusterMenu.tsx
Normal file
42
frontend/src/components/Nav/ClusterMenu.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { Cluster } from 'types';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface Props extends Cluster {}
|
||||||
|
|
||||||
|
const DefaultIcon: React.FC = () => {
|
||||||
|
const style: CSSProperties = {
|
||||||
|
width: '.6rem',
|
||||||
|
left: '-8px',
|
||||||
|
top: '-4px',
|
||||||
|
position: 'relative',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span title="Default Cluster" className="icon has-text-primary is-small">
|
||||||
|
<i style={style} data-fa-transform="rotate-340" className="fas fa-thumbtack" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClusterMenu: React.FC<Props> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
defaultCluster,
|
||||||
|
}) => (
|
||||||
|
<ul className="menu-list">
|
||||||
|
<li>
|
||||||
|
<NavLink exact to={`/clusters/${id}`} activeClassName="is-active" title={name} className="has-text-overflow-ellipsis">
|
||||||
|
{defaultCluster && <DefaultIcon />}
|
||||||
|
{name}
|
||||||
|
</NavLink>
|
||||||
|
<ul>
|
||||||
|
<NavLink to={`/clusters/${id}/topics`} activeClassName="is-active" title="Dashboard">
|
||||||
|
Topics
|
||||||
|
</NavLink>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ClusterMenu;
|
38
frontend/src/components/Nav/Nav.tsx
Normal file
38
frontend/src/components/Nav/Nav.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Cluster } from 'types';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import ClusterMenu from './ClusterMenu';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isClusterListFetched: boolean,
|
||||||
|
clusters: Cluster[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Nav: React.FC<Props> = ({
|
||||||
|
isClusterListFetched,
|
||||||
|
clusters,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
|
<aside className={cx('menu has-shadow has-background-white', className)}>
|
||||||
|
<p className="menu-label">
|
||||||
|
General
|
||||||
|
</p>
|
||||||
|
<ul className="menu-list">
|
||||||
|
<li>
|
||||||
|
<NavLink exact to="/" activeClassName="is-active" title="Dashboard">
|
||||||
|
Dashboard
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="menu-label">
|
||||||
|
Clusters
|
||||||
|
</p>
|
||||||
|
{!isClusterListFetched && <div className="loader" />}
|
||||||
|
|
||||||
|
{isClusterListFetched && clusters.map((cluster) => <ClusterMenu {...cluster} key={cluster.id}/>)}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Nav;
|
11
frontend/src/components/Nav/NavConatiner.ts
Normal file
11
frontend/src/components/Nav/NavConatiner.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Nav from './Nav';
|
||||||
|
import { getIsClusterListFetched, getClusterList } from 'redux/reducers/clusters/selectors';
|
||||||
|
import { RootState } from 'types';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => ({
|
||||||
|
isClusterListFetched: getIsClusterListFetched(state),
|
||||||
|
clusters: getClusterList(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Nav);
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import { Topic } from 'types';
|
import { Topic } from 'types';
|
||||||
import ConfigRow from './ConfigRow';
|
import ConfigRow from './ConfigRow';
|
||||||
import Partition from './Partition';
|
import Partition from './Partition';
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
|
|
||||||
const Details: React.FC<{ topic: Topic }> = ({
|
const Details: React.FC<{ topic: Topic }> = ({
|
||||||
topic: {
|
topic: {
|
||||||
|
|
|
@ -6,20 +6,23 @@ import {
|
||||||
import ListContainer from './List/ListContainer';
|
import ListContainer from './List/ListContainer';
|
||||||
import DetailsContainer from './Details/DetailsContainer';
|
import DetailsContainer from './Details/DetailsContainer';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
|
import { ClusterId } from 'types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
clusterId: string;
|
||||||
isFetched: boolean;
|
isFetched: boolean;
|
||||||
fetchBrokers: () => void;
|
fetchBrokers: (clusterId: ClusterId) => void;
|
||||||
fetchTopicList: () => void;
|
fetchTopicList: (clusterId: ClusterId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Topics: React.FC<Props> = ({
|
const Topics: React.FC<Props> = ({
|
||||||
|
clusterId,
|
||||||
isFetched,
|
isFetched,
|
||||||
fetchBrokers,
|
fetchBrokers,
|
||||||
fetchTopicList,
|
fetchTopicList,
|
||||||
}) => {
|
}) => {
|
||||||
React.useEffect(() => { fetchTopicList(); }, [fetchTopicList]);
|
React.useEffect(() => { fetchTopicList(clusterId); }, [fetchTopicList, clusterId]);
|
||||||
React.useEffect(() => { fetchBrokers(); }, [fetchBrokers]);
|
React.useEffect(() => { fetchBrokers(clusterId); }, [fetchBrokers, clusterId]);
|
||||||
|
|
||||||
if (isFetched) {
|
if (isFetched) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,15 +5,23 @@ import {
|
||||||
} from 'redux/reducers/topics/thunks';
|
} from 'redux/reducers/topics/thunks';
|
||||||
import Topics from './Topics';
|
import Topics from './Topics';
|
||||||
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
|
import { getIsTopicListFetched } from 'redux/reducers/topics/selectors';
|
||||||
import { RootState } from 'types';
|
import { RootState, ClusterId } from 'types';
|
||||||
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState) => ({
|
interface RouteProps {
|
||||||
|
clusterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OwnProps extends RouteComponentProps<RouteProps> { }
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState, { match: { params: { clusterId } }}: OwnProps) => ({
|
||||||
isFetched: getIsTopicListFetched(state),
|
isFetched: getIsTopicListFetched(state),
|
||||||
|
clusterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchTopicList,
|
fetchTopicList: (clusterId: ClusterId) => fetchTopicList(clusterId),
|
||||||
fetchBrokers,
|
fetchBrokers: (clusterId: ClusterId) => fetchBrokers(clusterId),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
|
export default connect(mapStateToProps, mapDispatchToProps)(Topics);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import './theme/index.scss';
|
import './theme/index.scss';
|
||||||
import App from './components/App';
|
import AppContainer from './components/AppContainer';
|
||||||
import * as serviceWorker from './serviceWorker';
|
import * as serviceWorker from './serviceWorker';
|
||||||
import configureStore from './redux/store/configureStore';
|
import configureStore from './redux/store/configureStore';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ const store = configureStore();
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<AppContainer />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
|
|
11
frontend/src/lib/api/clusters.ts
Normal file
11
frontend/src/lib/api/clusters.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {
|
||||||
|
Cluster,
|
||||||
|
} from 'types';
|
||||||
|
import {
|
||||||
|
BASE_URL,
|
||||||
|
BASE_PARAMS,
|
||||||
|
} from 'lib/constants';
|
||||||
|
|
||||||
|
export const getClusters = (): Promise<Cluster[]> =>
|
||||||
|
fetch(`${BASE_URL}/clusters`, { ...BASE_PARAMS })
|
||||||
|
.then(res => res.json());
|
|
@ -1 +1,2 @@
|
||||||
export * from './topics';
|
export * from './topics';
|
||||||
|
export * from './clusters';
|
||||||
|
|
|
@ -2,27 +2,21 @@ import {
|
||||||
TopicName,
|
TopicName,
|
||||||
Topic,
|
Topic,
|
||||||
Broker,
|
Broker,
|
||||||
|
ClusterId,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
|
import {
|
||||||
const BASE_PARAMS: RequestInit = {
|
BASE_URL,
|
||||||
credentials: 'include',
|
BASE_PARAMS,
|
||||||
mode: 'cors',
|
} from 'lib/constants';
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/vnd.kafka.v2+json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:8082';
|
|
||||||
|
|
||||||
export const getTopic = (name: TopicName): Promise<Topic> =>
|
export const getTopic = (name: TopicName): Promise<Topic> =>
|
||||||
fetch(`${BASE_URL}/topics/${name}`, { ...BASE_PARAMS })
|
fetch(`${BASE_URL}/topics/${name}`, { ...BASE_PARAMS })
|
||||||
.then(res => res.json());
|
.then(res => res.json());
|
||||||
|
|
||||||
export const getTopics = (): Promise<TopicName[]> =>
|
export const getTopics = (clusterId: ClusterId): Promise<TopicName[]> =>
|
||||||
fetch(`${BASE_URL}/topics`, { ...BASE_PARAMS })
|
fetch(`${BASE_URL}/clusters/${clusterId}/topics`, { ...BASE_PARAMS })
|
||||||
.then(res => res.json());
|
.then(res => res.json());
|
||||||
|
|
||||||
export const getBrokers = (): Promise<{ brokers: Broker[] }> =>
|
export const getBrokers = (clusterId: ClusterId): Promise<{ brokers: Broker[] }> =>
|
||||||
fetch(`${BASE_URL}/brokers`, { ...BASE_PARAMS })
|
fetch(`${BASE_URL}/clusters/${clusterId}/brokers`, { ...BASE_PARAMS })
|
||||||
.then(res => res.json());
|
.then(res => res.json());
|
||||||
|
|
9
frontend/src/lib/constants.ts
Normal file
9
frontend/src/lib/constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const BASE_PARAMS: RequestInit = {
|
||||||
|
credentials: 'include',
|
||||||
|
mode: 'cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BASE_URL = 'http://localhost:3004';
|
|
@ -1,3 +1,7 @@
|
||||||
import topicsActionType from './topics/actionType';
|
import topicsActionType from './topics/actionType';
|
||||||
|
import clustersActionType from './clusters/actionType';
|
||||||
|
|
||||||
export default { ...topicsActionType };
|
export default {
|
||||||
|
...topicsActionType,
|
||||||
|
...clustersActionType,
|
||||||
|
};
|
||||||
|
|
7
frontend/src/redux/reducers/clusters/actionType.ts
Normal file
7
frontend/src/redux/reducers/clusters/actionType.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
enum ActionType {
|
||||||
|
CLUSTERS__FETCH_REQUEST = 'CLUSTERS__FETCH_REQUEST',
|
||||||
|
CLUSTERS__FETCH_SUCCESS = 'CLUSTERS__FETCH_SUCCESS',
|
||||||
|
CLUSTERS__FETCH_FAILURE = 'CLUSTERS__FETCH_FAILURE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActionType;
|
9
frontend/src/redux/reducers/clusters/actions.ts
Normal file
9
frontend/src/redux/reducers/clusters/actions.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { createAsyncAction} from 'typesafe-actions';
|
||||||
|
import ActionType from './actionType';
|
||||||
|
import { Cluster } from 'types';
|
||||||
|
|
||||||
|
export const fetchClusterListAction = createAsyncAction(
|
||||||
|
ActionType.CLUSTERS__FETCH_REQUEST,
|
||||||
|
ActionType.CLUSTERS__FETCH_SUCCESS,
|
||||||
|
ActionType.CLUSTERS__FETCH_FAILURE,
|
||||||
|
)<undefined, Cluster[], undefined>();
|
33
frontend/src/redux/reducers/clusters/reducer.ts
Normal file
33
frontend/src/redux/reducers/clusters/reducer.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { ClustersState, FetchStatus, Action } from 'types';
|
||||||
|
import actionType from 'redux/reducers/actionType';
|
||||||
|
|
||||||
|
export const initialState: ClustersState = {
|
||||||
|
fetchStatus: FetchStatus.notFetched,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state = initialState, action: Action): ClustersState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case actionType.CLUSTERS__FETCH_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchStatus: FetchStatus.fetching,
|
||||||
|
};
|
||||||
|
case actionType.CLUSTERS__FETCH_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchStatus: FetchStatus.fetched,
|
||||||
|
items: action.payload,
|
||||||
|
};
|
||||||
|
case actionType.CLUSTERS__FETCH_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchStatus: FetchStatus.errorFetching,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reducer;
|
8
frontend/src/redux/reducers/clusters/selectors.ts
Normal file
8
frontend/src/redux/reducers/clusters/selectors.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { ClustersState, RootState, FetchStatus } from 'types';
|
||||||
|
|
||||||
|
const clustersState = ({ clusters }: RootState): ClustersState => clusters;
|
||||||
|
|
||||||
|
export const getIsClusterListFetched = createSelector(clustersState, ({ fetchStatus }) => fetchStatus === FetchStatus.fetched);
|
||||||
|
|
||||||
|
export const getClusterList = createSelector(clustersState, ({ items }) => items);
|
19
frontend/src/redux/reducers/clusters/thunks.ts
Normal file
19
frontend/src/redux/reducers/clusters/thunks.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {
|
||||||
|
getClusters,
|
||||||
|
} from 'lib/api';
|
||||||
|
import {
|
||||||
|
fetchClusterListAction,
|
||||||
|
} from './actions';
|
||||||
|
import { Cluster, PromiseThunk } from 'types';
|
||||||
|
|
||||||
|
export const fetchClustersList = (): PromiseThunk<void> => async (dispatch) => {
|
||||||
|
dispatch(fetchClusterListAction.request());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clusters: Cluster[] = await getClusters();
|
||||||
|
|
||||||
|
dispatch(fetchClusterListAction.success(clusters));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(fetchClusterListAction.failure());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import topics from './topics/reducer';
|
import topics from './topics/reducer';
|
||||||
|
import clusters from './clusters/reducer';
|
||||||
import { RootState } from 'types';
|
import { RootState } from 'types';
|
||||||
|
|
||||||
export default combineReducers<RootState>({
|
export default combineReducers<RootState>({
|
||||||
topics,
|
topics,
|
||||||
|
clusters,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,14 +7,14 @@ import {
|
||||||
fetchTopicListAction,
|
fetchTopicListAction,
|
||||||
fetchBrokersAction,
|
fetchBrokersAction,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { Topic, TopicName, PromiseThunk } from 'types';
|
import { Topic, TopicName, PromiseThunk, ClusterId } from 'types';
|
||||||
|
|
||||||
|
|
||||||
export const fetchTopicList = (): PromiseThunk<void> => async (dispatch, getState) => {
|
export const fetchTopicList = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(fetchTopicListAction.request());
|
dispatch(fetchTopicListAction.request());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const topics = await getTopics();
|
const topics = await getTopics(clusterId);
|
||||||
const detailedList = await Promise.all(topics.map((topic: TopicName): Promise<Topic> => getTopic(topic)));
|
const detailedList = await Promise.all(topics.map((topic: TopicName): Promise<Topic> => getTopic(topic)));
|
||||||
|
|
||||||
dispatch(fetchTopicListAction.success(detailedList));
|
dispatch(fetchTopicListAction.success(detailedList));
|
||||||
|
@ -23,10 +23,10 @@ export const fetchTopicList = (): PromiseThunk<void> => async (dispatch, getStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchBrokers = (): PromiseThunk<void> => async (dispatch, getState) => {
|
export const fetchBrokers = (clusterId: ClusterId): PromiseThunk<void> => async (dispatch) => {
|
||||||
dispatch(fetchBrokersAction.request());
|
dispatch(fetchBrokersAction.request());
|
||||||
try {
|
try {
|
||||||
const { brokers } = await getBrokers();
|
const { brokers } = await getBrokers(clusterId);
|
||||||
dispatch(fetchBrokersAction.success(brokers));
|
dispatch(fetchBrokersAction.success(brokers));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(fetchBrokersAction.failure());
|
dispatch(fetchBrokersAction.failure());
|
||||||
|
|
|
@ -1,13 +1,4 @@
|
||||||
@import "../../node_modules/bulma/sass/utilities/_all.sass";
|
@import "../../node_modules/bulma/sass/utilities/_all.sass";
|
||||||
|
|
||||||
$menu-item-color: $white;
|
|
||||||
$menu-item-radius: 0;
|
|
||||||
$menu-item-hover-color: $white;
|
|
||||||
$menu-item-hover-background-color: transparent;
|
|
||||||
$menu-item-active-color: $text-strong;
|
|
||||||
$menu-item-active-background-color: $background;
|
|
||||||
$menu-list-border-left: 1px solid $border-light;
|
|
||||||
|
|
||||||
@import "../../node_modules/bulma/sass/base/_all.sass";
|
@import "../../node_modules/bulma/sass/base/_all.sass";
|
||||||
@import "../../node_modules/bulma/sass/elements/_all.sass";
|
@import "../../node_modules/bulma/sass/elements/_all.sass";
|
||||||
@import "../../node_modules/bulma/sass/form/_all.sass";
|
@import "../../node_modules/bulma/sass/form/_all.sass";
|
||||||
|
|
|
@ -11,9 +11,32 @@
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
145deg,
|
||||||
|
rgba(0,0,0,.003),
|
||||||
|
rgba(0,0,0,.005) 5px,
|
||||||
|
rgba(0,0,0,0) 5px,
|
||||||
|
rgba(0,0,0,0) 10px
|
||||||
|
),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
-145deg,
|
||||||
|
rgba(0,0,0,.003),
|
||||||
|
rgba(0,0,0,.005) 5px,
|
||||||
|
rgba(0,0,0,0) 5px,
|
||||||
|
rgba(0,0,0,0) 10px
|
||||||
|
);
|
||||||
|
background-color: $light;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-text-overflow-ellipsis {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
23
frontend/src/types/cluster.ts
Normal file
23
frontend/src/types/cluster.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { FetchStatus } from "types";
|
||||||
|
|
||||||
|
export enum ClusterStatus {
|
||||||
|
Online = 'online',
|
||||||
|
Offline = 'offline',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClusterId = string;
|
||||||
|
|
||||||
|
export interface Cluster {
|
||||||
|
id: ClusterId;
|
||||||
|
name: string;
|
||||||
|
defaultCluster: boolean;
|
||||||
|
status: ClusterStatus;
|
||||||
|
brokerCount: number;
|
||||||
|
onlinePartitionCount: number;
|
||||||
|
topicCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClustersState {
|
||||||
|
fetchStatus: FetchStatus;
|
||||||
|
items: Cluster[];
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
import { ActionType } from 'typesafe-actions';
|
import { ActionType } from 'typesafe-actions';
|
||||||
import * as topicsActions from 'redux/reducers/topics/actions';
|
import * as topicsActions from 'redux/reducers/topics/actions';
|
||||||
|
import * as clustersActions from 'redux/reducers/clusters/actions';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { TopicsState } from './topic';
|
import { TopicsState } from './topic';
|
||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
|
import { ClustersState } from './cluster';
|
||||||
|
|
||||||
export * from './topic';
|
export * from './topic';
|
||||||
|
export * from './cluster';
|
||||||
|
|
||||||
export enum FetchStatus {
|
export enum FetchStatus {
|
||||||
notFetched = 'notFetched',
|
notFetched = 'notFetched',
|
||||||
|
@ -14,8 +18,9 @@ export enum FetchStatus {
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
topics: TopicsState;
|
topics: TopicsState;
|
||||||
|
clusters: ClustersState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Action = ActionType<typeof topicsActions>;
|
export type Action = ActionType<typeof topicsActions | typeof clustersActions>;
|
||||||
|
|
||||||
export type PromiseThunk<T> = ThunkAction<Promise<T>, RootState, undefined, AnyAction>;
|
export type PromiseThunk<T> = ThunkAction<Promise<T>, RootState, undefined, AnyAction>;
|
||||||
|
|
Loading…
Add table
Reference in a new issue