Cleanup styling (#365)

This commit is contained in:
Oleg Shur 2021-04-09 14:29:39 +03:00 committed by GitHub
parent 6e8226298f
commit d471759b79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1033 additions and 504 deletions

View file

@ -17,7 +17,7 @@ $navbar-width: 250px;
z-index: 20; z-index: 20;
} }
&__navbar { &__sidebar{
width: $navbar-width; width: $navbar-width;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -28,6 +28,19 @@ $navbar-width: 250px;
bottom: 0; bottom: 0;
padding: 20px 20px; padding: 20px 20px;
overflow-y: scroll; overflow-y: scroll;
transition: width .25s,opacity .25s,transform .25s,-webkit-transform .25s;
&Overlay {
position: fixed;
top: 0;
height: 120vh;
z-index: 99;
display: block;
visibility: hidden;
opacity: 0;
-webkit-transition: all .5s ease;
transition: all .5s ease;
}
} }
&__alerts { &__alerts {
@ -47,3 +60,35 @@ $navbar-width: 250px;
.react-datepicker-popper { .react-datepicker-popper {
z-index: 30 !important; z-index: 30 !important;
} }
@media screen and (max-width: 1023px) {
.Layout {
&__container {
margin-left: initial;
margin-top: 1.5rem;
}
&__sidebar {
left: -$navbar-width;
z-index: 100;
}
&__alerts {
max-width: initial;
}
&--sidebarVisible {
.Layout__sidebar {
transform: translate3d($navbar-width,0,0);
&Overlay {
background-color: rgba(34,41,47,.5);
left: 0;
right: 0;
opacity: 1;
visibility: visible;
}
}
}
}
}

View file

@ -1,51 +1,97 @@
import './App.scss'; import './App.scss';
import React from 'react'; import React from 'react';
import { Switch, Route } from 'react-router-dom'; import cx from 'classnames';
import { Cluster } from 'generated-sources';
import { Switch, Route, useLocation } from 'react-router-dom';
import { GIT_TAG, GIT_COMMIT } from 'lib/constants'; import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
import { Alerts } from 'redux/interfaces'; import { Alerts } from 'redux/interfaces';
import NavContainer from './Nav/NavContainer'; import Nav from './Nav/Nav';
import PageLoader from './common/PageLoader/PageLoader'; import PageLoader from './common/PageLoader/PageLoader';
import Dashboard from './Dashboard/Dashboard'; import Dashboard from './Dashboard/Dashboard';
import Cluster from './Cluster/Cluster'; import ClusterPage from './Cluster/Cluster';
import Version from './Version/Version'; import Version from './Version/Version';
import Alert from './Alert/Alert'; import Alert from './Alert/Alert';
export interface AppProps { export interface AppProps {
isClusterListFetched?: boolean; isClusterListFetched?: boolean;
alerts: Alerts; alerts: Alerts;
clusters: Cluster[];
fetchClustersList: () => void; fetchClustersList: () => void;
} }
const App: React.FC<AppProps> = ({ const App: React.FC<AppProps> = ({
isClusterListFetched, isClusterListFetched,
alerts, alerts,
clusters,
fetchClustersList, fetchClustersList,
}) => { }) => {
const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
const onBurgerClick = React.useCallback(
() => setIsSidebarVisible(!isSidebarVisible),
[isSidebarVisible]
);
const closeSidebar = React.useCallback(() => setIsSidebarVisible(false), []);
const location = useLocation();
React.useEffect(() => {
closeSidebar();
}, [location]);
React.useEffect(() => { React.useEffect(() => {
fetchClustersList(); fetchClustersList();
}, [fetchClustersList]); }, [fetchClustersList]);
return ( return (
<div className="Layout"> <div
className={cx('Layout', { 'Layout--sidebarVisible': isSidebarVisible })}
>
<nav <nav
className="navbar is-fixed-top is-white Layout__header" className="navbar is-fixed-top is-white Layout__header"
role="navigation" role="navigation"
aria-label="main navigation" aria-label="main navigation"
> >
<div className="navbar-brand"> <div className="navbar-brand">
<div
className={cx('navbar-burger', 'ml-0', {
'is-active': isSidebarVisible,
})}
onClick={onBurgerClick}
onKeyDown={onBurgerClick}
role="button"
tabIndex={0}
>
<span />
<span />
<span />
</div>
<a className="navbar-item title is-5 is-marginless" href="/ui"> <a className="navbar-item title is-5 is-marginless" href="/ui">
Kafka UI Kafka UI
</a> </a>
</div>
<div className="navbar-end"> <div className="navbar-item">
<div className="navbar-item mr-2">
<Version tag={GIT_TAG} commit={GIT_COMMIT} /> <Version tag={GIT_TAG} commit={GIT_COMMIT} />
</div> </div>
</div> </div>
</nav> </nav>
<main className="Layout__container"> <main className="Layout__container">
<NavContainer className="Layout__navbar" /> <div className="Layout__sidebar has-shadow has-background-white">
<Nav
clusters={clusters}
isClusterListFetched={isClusterListFetched}
/>
</div>
<div
className="Layout__sidebarOverlay is-overlay"
onClick={closeSidebar}
onKeyDown={closeSidebar}
tabIndex={-1}
aria-hidden="true"
/>
{isClusterListFetched ? ( {isClusterListFetched ? (
<Switch> <Switch>
<Route <Route
@ -53,7 +99,7 @@ const App: React.FC<AppProps> = ({
path={['/', '/ui', '/ui/clusters']} path={['/', '/ui', '/ui/clusters']}
component={Dashboard} component={Dashboard}
/> />
<Route path="/ui/clusters/:clusterName" component={Cluster} /> <Route path="/ui/clusters/:clusterName" component={ClusterPage} />
</Switch> </Switch>
) : ( ) : (
<PageLoader fullHeight /> <PageLoader fullHeight />

View file

@ -1,6 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchClustersList } from 'redux/actions'; import { fetchClustersList } from 'redux/actions';
import { getIsClusterListFetched } from 'redux/reducers/clusters/selectors'; import {
getClusterList,
getIsClusterListFetched,
} from 'redux/reducers/clusters/selectors';
import { getAlerts } from 'redux/reducers/alerts/selectors'; import { getAlerts } from 'redux/reducers/alerts/selectors';
import { RootState } from 'redux/interfaces'; import { RootState } from 'redux/interfaces';
import App from './App'; import App from './App';
@ -8,6 +11,7 @@ import App from './App';
const mapStateToProps = (state: RootState) => ({ const mapStateToProps = (state: RootState) => ({
isClusterListFetched: getIsClusterListFetched(state), isClusterListFetched: getIsClusterListFetched(state),
alerts: getAlerts(state), alerts: getAlerts(state),
clusters: getClusterList(state),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {

View file

@ -7,16 +7,15 @@ import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import Indicator from 'components/common/Dashboard/Indicator'; import Indicator from 'components/common/Dashboard/Indicator';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
import { useParams } from 'react-router';
interface Props extends ClusterStats { interface Props extends ClusterStats {
clusterName: ClusterName;
isFetched: boolean; isFetched: boolean;
fetchClusterStats: (clusterName: ClusterName) => void; fetchClusterStats: (clusterName: ClusterName) => void;
fetchBrokers: (clusterName: ClusterName) => void; fetchBrokers: (clusterName: ClusterName) => void;
} }
const Brokers: React.FC<Props> = ({ const Brokers: React.FC<Props> = ({
clusterName,
brokerCount, brokerCount,
activeControllers, activeControllers,
zooKeeperStatus, zooKeeperStatus,
@ -29,6 +28,8 @@ const Brokers: React.FC<Props> = ({
fetchClusterStats, fetchClusterStats,
fetchBrokers, fetchBrokers,
}) => { }) => {
const { clusterName } = useParams<{ clusterName: ClusterName }>();
React.useEffect(() => { React.useEffect(() => {
fetchClusterStats(clusterName); fetchClusterStats(clusterName);
fetchBrokers(clusterName); fetchBrokers(clusterName);
@ -44,9 +45,13 @@ const Brokers: React.FC<Props> = ({
<div className="section"> <div className="section">
<Breadcrumb>Brokers overview</Breadcrumb> <Breadcrumb>Brokers overview</Breadcrumb>
<MetricsWrapper title="Uptime"> <MetricsWrapper title="Uptime">
<Indicator label="Total Brokers">{brokerCount}</Indicator> <Indicator className="is-one-third" label="Total Brokers">
<Indicator label="Active Controllers">{activeControllers}</Indicator> {brokerCount}
<Indicator label="Zookeeper Status"> </Indicator>
<Indicator className="is-one-third" label="Active Controllers">
{activeControllers}
</Indicator>
<Indicator className="is-one-third" label="Zookeeper Status">
<span className={cx('tag', zkOnline ? 'is-primary' : 'is-danger')}> <span className={cx('tag', zkOnline ? 'is-primary' : 'is-danger')}>
{zkOnline ? 'Online' : 'Offline'} {zkOnline ? 'Online' : 'Offline'}
</span> </span>

View file

@ -1,43 +1,36 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchClusterStats, fetchBrokers } from 'redux/actions'; import { fetchClusterStats, fetchBrokers } from 'redux/actions';
import * as brokerSelectors from 'redux/reducers/brokers/selectors'; import { RootState } from 'redux/interfaces';
import { RootState, ClusterName } from 'redux/interfaces'; import {
import { RouteComponentProps } from 'react-router-dom'; getIsBrokerListFetched,
getBrokerCount,
getZooKeeperStatus,
getActiveControllers,
getOnlinePartitionCount,
getOfflinePartitionCount,
getInSyncReplicasCount,
getOutOfSyncReplicasCount,
getUnderReplicatedPartitionCount,
getDiskUsage,
} from 'redux/reducers/brokers/selectors';
import Brokers from './Brokers'; import Brokers from './Brokers';
interface RouteProps { const mapStateToProps = (state: RootState) => ({
clusterName: ClusterName; isFetched: getIsBrokerListFetched(state),
} brokerCount: getBrokerCount(state),
zooKeeperStatus: getZooKeeperStatus(state),
type OwnProps = RouteComponentProps<RouteProps>; activeControllers: getActiveControllers(state),
onlinePartitionCount: getOnlinePartitionCount(state),
const mapStateToProps = ( offlinePartitionCount: getOfflinePartitionCount(state),
state: RootState, inSyncReplicasCount: getInSyncReplicasCount(state),
{ outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
match: { underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
params: { clusterName }, diskUsage: getDiskUsage(state),
},
}: OwnProps
) => ({
isFetched: brokerSelectors.getIsBrokerListFetched(state),
clusterName,
brokerCount: brokerSelectors.getBrokerCount(state),
zooKeeperStatus: brokerSelectors.getZooKeeperStatus(state),
activeControllers: brokerSelectors.getActiveControllers(state),
onlinePartitionCount: brokerSelectors.getOnlinePartitionCount(state),
offlinePartitionCount: brokerSelectors.getOfflinePartitionCount(state),
inSyncReplicasCount: brokerSelectors.getInSyncReplicasCount(state),
outOfSyncReplicasCount: brokerSelectors.getOutOfSyncReplicasCount(state),
underReplicatedPartitionCount: brokerSelectors.getUnderReplicatedPartitionCount(
state
),
diskUsage: brokerSelectors.getDiskUsage(state),
}); });
const mapDispatchToProps = { const mapDispatchToProps = {
fetchClusterStats: (clusterName: ClusterName) => fetchClusterStats,
fetchClusterStats(clusterName), fetchBrokers,
fetchBrokers: (clusterName: ClusterName) => fetchBrokers(clusterName),
}; };
export default connect(mapStateToProps, mapDispatchToProps)(Brokers); export default connect(mapStateToProps, mapDispatchToProps)(Brokers);

View file

@ -57,27 +57,29 @@ const List: React.FC<ListProps> = ({
<PageLoader /> <PageLoader />
) : ( ) : (
<div className="box"> <div className="box">
<table className="table is-fullwidth"> <div className="table-container">
<thead> <table className="table is-fullwidth">
<tr> <thead>
<th>Name</th>
<th>Connect</th>
<th>Type</th>
<th>Plugin</th>
<th>Topics</th>
<th>Status</th>
<th>Tasks</th>
<th> </th>
</tr>
</thead>
<tbody>
{connectors.length === 0 && (
<tr> <tr>
<td colSpan={10}>No connectors found</td> <th>Name</th>
<th>Connect</th>
<th>Type</th>
<th>Plugin</th>
<th>Topics</th>
<th>Status</th>
<th>Tasks</th>
<th> </th>
</tr> </tr>
)} </thead>
</tbody> <tbody>
</table> {connectors.length === 0 && (
<tr>
<td colSpan={10}>No connectors found</td>
</tr>
)}
</tbody>
</table>
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -54,28 +54,30 @@ const Details: React.FC<Props> = ({
{isFetched ? ( {isFetched ? (
<div className="box"> <div className="box">
<table className="table is-striped is-fullwidth"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth">
<tr> <thead>
<th>Consumer ID</th> <tr>
<th>Host</th> <th>Consumer ID</th>
<th>Topic</th> <th>Host</th>
<th>Partition</th> <th>Topic</th>
<th>Messages behind</th> <th>Partition</th>
<th>Current offset</th> <th>Messages behind</th>
<th>End offset</th> <th>Current offset</th>
</tr> <th>End offset</th>
</thead> </tr>
<tbody> </thead>
{items.map((consumer) => ( <tbody>
<ListItem {items.map((consumer) => (
key={consumer.consumerId} <ListItem
clusterName={clusterName} key={consumer.consumerId}
consumer={consumer} clusterName={clusterName}
/> consumer={consumer}
))} />
</tbody> ))}
</table> </tbody>
</table>
</div>
</div> </div>
) : ( ) : (
<PageLoader /> <PageLoader />

View file

@ -36,29 +36,31 @@ const List: React.FC<Props> = ({ consumerGroups }) => {
/> />
</div> </div>
</div> </div>
<table className="table is-striped is-fullwidth is-hoverable"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth is-hoverable">
<tr> <thead>
<th>Consumer group ID</th> <tr>
<th>Num of consumers</th> <th>Consumer group ID</th>
<th>Num of topics</th> <th>Num of consumers</th>
</tr> <th>Num of topics</th>
</thead> </tr>
<tbody> </thead>
{consumerGroups <tbody>
.filter( {consumerGroups
(consumerGroup) => .filter(
!searchText || (consumerGroup) =>
consumerGroup?.consumerGroupId?.indexOf(searchText) >= 0 !searchText ||
) consumerGroup?.consumerGroupId?.indexOf(searchText) >= 0
.map((consumerGroup) => ( )
<ListItem .map((consumerGroup) => (
key={consumerGroup.consumerGroupId} <ListItem
consumerGroup={consumerGroup} key={consumerGroup.consumerGroupId}
/> consumerGroup={consumerGroup}
))} />
</tbody> ))}
</table> </tbody>
</table>
</div>
</div> </div>
) : ( ) : (
'No active consumer groups' 'No active consumer groups'

View file

@ -5,7 +5,7 @@ import { Cluster } from 'generated-sources';
import ClusterMenu from './ClusterMenu'; import ClusterMenu from './ClusterMenu';
interface Props { interface Props {
isClusterListFetched: boolean; isClusterListFetched?: boolean;
clusters: Cluster[]; clusters: Cluster[];
className?: string; className?: string;
} }

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import {
getIsClusterListFetched,
getClusterList,
} from 'redux/reducers/clusters/selectors';
import { RootState } from 'redux/interfaces';
import Nav from './Nav';
const mapStateToProps = (state: RootState) => ({
isClusterListFetched: getIsClusterListFetched(state),
clusters: getClusterList(state),
});
export default connect(mapStateToProps)(Nav);

View file

@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
import Nav from '../Nav';
describe('Nav', () => {
it('renders loader', () => {
const wrapper = shallow(<Nav clusters={[]} />);
expect(wrapper.find('.loader')).toBeTruthy();
expect(wrapper.exists('ClusterMenu')).toBeFalsy();
});
it('renders ClusterMenu', () => {
const wrapper = shallow(
<Nav clusters={[onlineClusterPayload]} isClusterListFetched />
);
expect(wrapper.exists('.loader')).toBeFalsy();
expect(wrapper.exists('ClusterMenu')).toBeTruthy();
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nav renders ClusterMenu 1`] = `
<aside
className="menu has-shadow has-background-white"
>
<p
className="menu-label"
>
General
</p>
<ul
className="menu-list"
>
<li>
<NavLink
activeClassName="is-active"
exact={true}
title="Dashboard"
to="/ui"
>
Dashboard
</NavLink>
</li>
</ul>
<p
className="menu-label"
>
Clusters
</p>
<ClusterMenu
cluster={
Object {
"brokerCount": 1,
"bytesInPerSec": 1.55,
"bytesOutPerSec": 9.314,
"defaultCluster": true,
"features": Array [],
"name": "secondLocal",
"onlinePartitionCount": 6,
"status": "online",
"topicCount": 3,
}
}
key="secondLocal"
/>
</aside>
`;

View file

@ -85,20 +85,22 @@ const Details: React.FC<DetailsProps> = ({
<LatestVersionItem schema={schema} /> <LatestVersionItem schema={schema} />
</div> </div>
<div className="box"> <div className="box">
<table className="table is-striped is-fullwidth"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth">
<tr> <thead>
<th>Version</th> <tr>
<th>ID</th> <th>Version</th>
<th>Schema</th> <th>ID</th>
</tr> <th>Schema</th>
</thead> </tr>
<tbody> </thead>
{versions.map((version) => ( <tbody>
<SchemaVersion key={version.id} version={version} /> {versions.map((version) => (
))} <SchemaVersion key={version.id} version={version} />
</tbody> ))}
</table> </tbody>
</table>
</div>
</div> </div>
</> </>
) : ( ) : (

View file

@ -12,22 +12,24 @@ const LatestVersionItem: React.FC<LatestVersionProps> = ({
<div className="tile is-ancestor mt-1"> <div className="tile is-ancestor mt-1">
<div className="tile is-4 is-parent"> <div className="tile is-4 is-parent">
<div className="tile is-child"> <div className="tile is-child">
<table className="table is-fullwidth"> <div className="table-container">
<tbody> <table className="table is-fullwidth">
<tr> <tbody>
<td>ID</td> <tr>
<td>{id}</td> <td>ID</td>
</tr> <td>{id}</td>
<tr> </tr>
<td>Subject</td> <tr>
<td>{subject}</td> <td>Subject</td>
</tr> <td>{subject}</td>
<tr> </tr>
<td>Compatibility</td> <tr>
<td>{compatibilityLevel}</td> <td>Compatibility</td>
</tr> <td>{compatibilityLevel}</td>
</tbody> </tr>
</table> </tbody>
</table>
</div>
</div> </div>
</div> </div>
<div className="tile is-parent"> <div className="tile is-parent">

View file

@ -85,24 +85,28 @@ exports[`Details View Initial state matches snapshot 1`] = `
<div <div
className="box" className="box"
> >
<table <div
className="table is-striped is-fullwidth" className="table-container"
> >
<thead> <table
<tr> className="table is-striped is-fullwidth"
<th> >
Version <thead>
</th> <tr>
<th> <th>
ID Version
</th> </th>
<th> <th>
Schema ID
</th> </th>
</tr> <th>
</thead> Schema
<tbody /> </th>
</table> </tr>
</thead>
<tbody />
</table>
</div>
</div> </div>
</div> </div>
`; `;
@ -216,51 +220,55 @@ exports[`Details View when page with schema versions loaded when schema has vers
<div <div
className="box" className="box"
> >
<table <div
className="table is-striped is-fullwidth" className="table-container"
> >
<thead> <table
<tr> className="table is-striped is-fullwidth"
<th> >
Version <thead>
</th> <tr>
<th> <th>
ID Version
</th> </th>
<th> <th>
Schema ID
</th> </th>
</tr> <th>
</thead> Schema
<tbody> </th>
<SchemaVersion </tr>
key="1" </thead>
version={ <tbody>
Object { <SchemaVersion
"compatibilityLevel": "BACKWARD", key="1"
"id": 1, version={
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}", Object {
"schemaType": "JSON", "compatibilityLevel": "BACKWARD",
"subject": "test", "id": 1,
"version": "1", "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"schemaType": "JSON",
"subject": "test",
"version": "1",
}
} }
} />
/> <SchemaVersion
<SchemaVersion key="2"
key="2" version={
version={ Object {
Object { "compatibilityLevel": "BACKWARD",
"compatibilityLevel": "BACKWARD", "id": 2,
"id": 2, "schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}", "schemaType": "JSON",
"schemaType": "JSON", "subject": "test",
"subject": "test", "version": "2",
"version": "2", }
} }
} />
/> </tbody>
</tbody> </table>
</table> </div>
</div> </div>
</div> </div>
`; `;
@ -350,24 +358,28 @@ exports[`Details View when page with schema versions loaded when versions are em
<div <div
className="box" className="box"
> >
<table <div
className="table is-striped is-fullwidth" className="table-container"
> >
<thead> <table
<tr> className="table is-striped is-fullwidth"
<th> >
Version <thead>
</th> <tr>
<th> <th>
ID Version
</th> </th>
<th> <th>
Schema ID
</th> </th>
</tr> <th>
</thead> Schema
<tbody /> </th>
</table> </tr>
</thead>
<tbody />
</table>
</div>
</div> </div>
</div> </div>
`; `;

View file

@ -10,36 +10,40 @@ exports[`LatestVersionItem matches snapshot 1`] = `
<div <div
className="tile is-child" className="tile is-child"
> >
<table <div
className="table is-fullwidth" className="table-container"
> >
<tbody> <table
<tr> className="table is-fullwidth"
<td> >
ID <tbody>
</td> <tr>
<td> <td>
1 ID
</td> </td>
</tr> <td>
<tr> 1
<td> </td>
Subject </tr>
</td> <tr>
<td> <td>
test Subject
</td> </td>
</tr> <td>
<tr> test
<td> </td>
Compatibility </tr>
</td> <tr>
<td> <td>
BACKWARD Compatibility
</td> </td>
</tr> <td>
</tbody> BACKWARD
</table> </td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<div <div

View file

@ -48,25 +48,27 @@ const List: React.FC<ListProps> = ({
<PageLoader /> <PageLoader />
) : ( ) : (
<div className="box"> <div className="box">
<table className="table is-striped is-fullwidth"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth">
<tr> <thead>
<th>Schema Name</th>
<th>Version</th>
<th>Compatibility</th>
</tr>
</thead>
<tbody>
{schemas.length === 0 && (
<tr> <tr>
<td colSpan={10}>No schemas found</td> <th>Schema Name</th>
<th>Version</th>
<th>Compatibility</th>
</tr> </tr>
)} </thead>
{schemas.map((subject) => ( <tbody>
<ListItem key={subject.id} subject={subject} /> {schemas.length === 0 && (
))} <tr>
</tbody> <td colSpan={10}>No schemas found</td>
</table> </tr>
)}
{schemas.map((subject) => (
<ListItem key={subject.id} subject={subject} />
))}
</tbody>
</table>
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -81,33 +81,35 @@ const List: React.FC<Props> = ({
<PageLoader /> <PageLoader />
) : ( ) : (
<div className="box"> <div className="box">
<table className="table is-fullwidth"> <div className="table-container">
<thead> <table className="table is-fullwidth">
<tr> <thead>
<th>Topic Name</th>
<th>Total Partitions</th>
<th>Out of sync replicas</th>
<th>Type</th>
<th> </th>
</tr>
</thead>
<tbody>
{items.map((topic) => (
<ListItem
clusterName={clusterName}
key={topic.name}
topic={topic}
deleteTopic={deleteTopic}
/>
))}
{items.length === 0 && (
<tr> <tr>
<td colSpan={10}>No topics found</td> <th>Topic Name</th>
<th>Total Partitions</th>
<th>Out of sync replicas</th>
<th>Type</th>
<th> </th>
</tr> </tr>
)} </thead>
</tbody> <tbody>
</table> {items.map((topic) => (
<Pagination totalPages={totalPages} /> <ListItem
clusterName={clusterName}
key={topic.name}
topic={topic}
deleteTopic={deleteTopic}
/>
))}
{items.length === 0 && (
<tr>
<td colSpan={10}>No topics found</td>
</tr>
)}
</tbody>
</table>
<Pagination totalPages={totalPages} />
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -37,7 +37,7 @@ const ListItem: React.FC<ListItemProps> = ({
return ( return (
<tr> <tr>
<td> <td className="has-text-overflow-ellipsis">
<NavLink <NavLink
exact exact
to={`topics/${name}`} to={`topics/${name}`}

View file

@ -15,30 +15,32 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
return ( return (
<> <>
<table className="table is-fullwidth"> <div className="table-container">
<thead> <table className="table is-fullwidth">
<tr> <thead>
<th>Timestamp</th> <tr>
<th>Offset</th> <th>Timestamp</th>
<th>Partition</th> <th>Offset</th>
<th>Content</th> <th>Partition</th>
<th> </th> <th>Content</th>
</tr> <th> </th>
</thead> </tr>
<tbody> </thead>
{messages.map( <tbody>
({ partition, offset, timestamp, content }: TopicMessage) => ( {messages.map(
<MessageItem ({ partition, offset, timestamp, content }: TopicMessage) => (
key={`message-${timestamp.getTime()}-${offset}`} <MessageItem
partition={partition} key={`message-${timestamp.getTime()}-${offset}`}
offset={offset} partition={partition}
timestamp={timestamp} offset={offset}
content={content} timestamp={timestamp}
/> content={content}
) />
)} )
</tbody> )}
</table> </tbody>
</table>
</div>
<div className="columns"> <div className="columns">
<div className="column is-full"> <div className="column is-full">
<CustomParamButton <CustomParamButton

View file

@ -2,49 +2,53 @@
exports[`MessagesTable when topic contains messages matches snapshot 1`] = ` exports[`MessagesTable when topic contains messages matches snapshot 1`] = `
<Fragment> <Fragment>
<table <div
className="table is-fullwidth" className="table-container"
> >
<thead> <table
<tr> className="table is-fullwidth"
<th> >
Timestamp <thead>
</th> <tr>
<th> <th>
Offset Timestamp
</th> </th>
<th> <th>
Partition Offset
</th> </th>
<th> <th>
Content Partition
</th> </th>
<th> <th>
Content
</th>
<th>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<MessageItem <MessageItem
content={ content={
Object { Object {
"foo": "bar", "foo": "bar",
"key": "val", "key": "val",
}
} }
} key="message-802310400000-2"
key="message-802310400000-2" offset={2}
offset={2} partition={1}
partition={1} timestamp={1995-06-05T00:00:00.000Z}
timestamp={1995-06-05T00:00:00.000Z} />
/> <MessageItem
<MessageItem key="message-1596585600000-20"
key="message-1596585600000-20" offset={20}
offset={20} partition={2}
partition={2} timestamp={2020-08-05T00:00:00.000Z}
timestamp={2020-08-05T00:00:00.000Z} />
/> </tbody>
</tbody> </table>
</table> </div>
<div <div
className="columns" className="columns"
> >

View file

@ -43,26 +43,28 @@ const Overview: React.FC<Props> = ({
<Indicator label="Segment count">{segmentCount}</Indicator> <Indicator label="Segment count">{segmentCount}</Indicator>
</MetricsWrapper> </MetricsWrapper>
<div className="box"> <div className="box">
<table className="table is-striped is-fullwidth"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth">
<tr> <thead>
<th>Partition ID</th> <tr>
<th>Broker leader</th> <th>Partition ID</th>
<th>Min offset</th> <th>Broker leader</th>
<th>Max offset</th> <th>Min offset</th>
</tr> <th>Max offset</th>
</thead>
<tbody>
{partitions?.map(({ partition, leader, offsetMin, offsetMax }) => (
<tr key={`partition-list-item-key-${partition}`}>
<td>{partition}</td>
<td>{leader}</td>
<td>{offsetMin}</td>
<td>{offsetMax}</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {partitions?.map(({ partition, leader, offsetMin, offsetMax }) => (
<tr key={`partition-list-item-key-${partition}`}>
<td>{partition}</td>
<td>{leader}</td>
<td>{offsetMin}</td>
<td>{offsetMax}</td>
</tr>
))}
</tbody>
</table>
</div>
</div> </div>
</> </>
); );

View file

@ -47,20 +47,22 @@ const Settings: React.FC<Props> = ({
return ( return (
<div className="box"> <div className="box">
<table className="table is-striped is-fullwidth"> <div className="table-container">
<thead> <table className="table is-striped is-fullwidth">
<tr> <thead>
<th>Key</th> <tr>
<th>Value</th> <th>Key</th>
<th>Default Value</th> <th>Value</th>
</tr> <th>Default Value</th>
</thead> </tr>
<tbody> </thead>
{config.map((item) => ( <tbody>
<ConfigListItem key={item.name} config={item} /> {config.map((item) => (
))} <ConfigListItem key={item.name} config={item} />
</tbody> ))}
</table> </tbody>
</table>
</div>
</div> </div>
); );
}; };

View file

@ -1,61 +1,79 @@
import React from 'react'; import React from 'react';
import { mount, shallow } from 'enzyme'; import { mount } from 'enzyme';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom'; import { StaticRouter } from 'react-router-dom';
import { Alert } from 'redux/interfaces';
import configureStore from 'redux/store/configureStore'; import configureStore from 'redux/store/configureStore';
import App, { AppProps } from '../App'; import App, { AppProps } from 'components/App';
import AppContainer from 'components/AppContainer';
const fetchClustersList = jest.fn(); const fetchClustersList = jest.fn();
const store = configureStore(); const store = configureStore();
describe('App', () => { describe('App', () => {
const setupComponent = (props: Partial<AppProps> = {}) => ( describe('container', () => {
<App it('renders view', () => {
isClusterListFetched const wrapper = mount(
alerts={[]} <Provider store={store}>
fetchClustersList={fetchClustersList} <StaticRouter>
{...props} <AppContainer />
/> </StaticRouter>
); </Provider>
);
it('handles fetchClustersList', () => { expect(wrapper.exists('App')).toBeTruthy();
const wrapper = mount( });
});
describe('view', () => {
const setupComponent = (props: Partial<AppProps> = {}) => (
<Provider store={store}> <Provider store={store}>
<StaticRouter>{setupComponent()}</StaticRouter> <StaticRouter>
<App
isClusterListFetched
alerts={[]}
clusters={[]}
fetchClustersList={fetchClustersList}
{...props}
/>
</StaticRouter>
</Provider> </Provider>
); );
expect(wrapper.exists()).toBeTruthy();
expect(fetchClustersList).toHaveBeenCalledTimes(1);
});
it('shows PageLoader until cluster list is fetched', () => { it('handles fetchClustersList', () => {
const component = shallow(setupComponent({ isClusterListFetched: false })); const wrapper = mount(setupComponent());
expect(component.exists('.Layout__container PageLoader')).toBeTruthy(); expect(wrapper.exists()).toBeTruthy();
expect(component.exists('.Layout__container Switch')).toBeFalsy(); expect(fetchClustersList).toHaveBeenCalledTimes(1);
component.setProps({ isClusterListFetched: true }); });
expect(component.exists('.Layout__container PageLoader')).toBeFalsy();
expect(component.exists('.Layout__container Switch')).toBeTruthy();
});
it('correctly renders alerts', () => { it('shows PageLoader until cluster list is fetched', () => {
const alert = { let component = mount(setupComponent({ isClusterListFetched: false }));
id: 'alert-id', expect(component.exists('.Layout__container PageLoader')).toBeTruthy();
type: 'success', expect(component.exists('.Layout__container Switch')).toBeFalsy();
title: 'My Custom Title',
message: 'My Custom Message',
createdAt: 1234567890,
};
const wrapper = shallow(setupComponent());
expect(wrapper.exists('.Layout__alerts')).toBeTruthy();
expect(wrapper.exists('Alert')).toBeFalsy();
wrapper.setProps({ alerts: [alert] }); component = mount(setupComponent({ isClusterListFetched: true }));
expect(wrapper.exists('Alert')).toBeTruthy(); expect(component.exists('.Layout__container PageLoader')).toBeFalsy();
expect(wrapper.find('Alert').length).toEqual(1); expect(component.exists('.Layout__container Switch')).toBeTruthy();
}); });
it('matches snapshot', () => { it('correctly renders alerts', () => {
const component = shallow(setupComponent()); const alert: Alert = {
expect(component).toMatchSnapshot(); id: 'alert-id',
type: 'success',
title: 'My Custom Title',
message: 'My Custom Message',
createdAt: 1234567890,
};
let wrapper = mount(setupComponent());
expect(wrapper.exists('.Layout__alerts')).toBeTruthy();
expect(wrapper.exists('Alert')).toBeFalsy();
wrapper = mount(setupComponent({ alerts: [alert] }));
expect(wrapper.exists('Alert')).toBeTruthy();
expect(wrapper.find('Alert').length).toEqual(1);
});
it('matches snapshot', () => {
const wrapper = mount(setupComponent());
expect(wrapper).toMatchSnapshot();
});
}); });
}); });

View file

@ -1,60 +1,383 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App matches snapshot 1`] = ` exports[`App view matches snapshot 1`] = `
<div <Provider
className="Layout" store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
> >
<nav <StaticRouter>
aria-label="main navigation" <Router
className="navbar is-fixed-top is-white Layout__header" history={
role="navigation" Object {
> "action": "POP",
<div "block": [Function],
className="navbar-brand" "createHref": [Function],
> "go": [Function],
<a "goBack": [Function],
className="navbar-item title is-5 is-marginless" "goForward": [Function],
href="/ui" "listen": [Function],
> "location": Object {
Kafka UI "hash": "",
</a> "pathname": "/",
</div> "search": "",
<div "state": undefined,
className="navbar-end" },
> "push": [Function],
<div "replace": [Function],
className="navbar-item mr-2"
>
<Version />
</div>
</div>
</nav>
<main
className="Layout__container"
>
<Connect(Nav)
className="Layout__navbar"
/>
<Switch>
<Route
component={[Function]}
exact={true}
path={
Array [
"/",
"/ui",
"/ui/clusters",
]
} }
/> }
<Route staticContext={Object {}}
component={[Function]} >
path="/ui/clusters/:clusterName" <App
/> alerts={Array []}
</Switch> clusters={Array []}
</main> fetchClustersList={
<div [MockFunction] {
className="Layout__alerts" "calls": Array [
/> Array [],
</div> ],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
isClusterListFetched={true}
>
<div
className="Layout"
>
<nav
aria-label="main navigation"
className="navbar is-fixed-top is-white Layout__header"
role="navigation"
>
<div
className="navbar-brand"
>
<div
className="navbar-burger ml-0"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<span />
<span />
<span />
</div>
<a
className="navbar-item title is-5 is-marginless"
href="/ui"
>
Kafka UI
</a>
<div
className="navbar-item"
>
<Version />
</div>
</div>
</nav>
<main
className="Layout__container"
>
<div
className="Layout__sidebar has-shadow has-background-white"
>
<Nav
clusters={Array []}
isClusterListFetched={true}
>
<aside
className="menu has-shadow has-background-white"
>
<p
className="menu-label"
>
General
</p>
<ul
className="menu-list"
>
<li>
<NavLink
activeClassName="is-active"
exact={true}
title="Dashboard"
to="/ui"
>
<Link
aria-current={null}
title="Dashboard"
to={
Object {
"hash": "",
"pathname": "/ui",
"search": "",
"state": null,
}
}
>
<LinkAnchor
aria-current={null}
href="/ui"
navigate={[Function]}
title="Dashboard"
>
<a
aria-current={null}
href="/ui"
onClick={[Function]}
title="Dashboard"
>
Dashboard
</a>
</LinkAnchor>
</Link>
</NavLink>
</li>
</ul>
<p
className="menu-label"
>
Clusters
</p>
</aside>
</Nav>
</div>
<div
aria-hidden="true"
className="Layout__sidebarOverlay is-overlay"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={-1}
/>
<Switch>
<Route
component={[Function]}
computedMatch={
Object {
"isExact": true,
"params": Object {},
"path": "/",
"url": "/",
}
}
exact={true}
location={
Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
}
}
path={
Array [
"/",
"/ui",
"/ui/clusters",
]
}
>
<Dashboard
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
},
"push": [Function],
"replace": [Function],
}
}
location={
Object {
"hash": "",
"pathname": "/",
"search": "",
"state": undefined,
}
}
match={
Object {
"isExact": true,
"params": Object {},
"path": "/",
"url": "/",
}
}
staticContext={Object {}}
>
<div
className="section"
>
<div
className="level"
>
<div
className="level-item level-left"
>
<Breadcrumb>
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li
className="is-active"
>
<span
className=""
>
Dashboard
</span>
</li>
</ul>
</nav>
</Breadcrumb>
</div>
</div>
<Connect(ClustersWidget)>
<ClustersWidget
clusters={Array []}
dispatch={[Function]}
offlineClusters={Array []}
onlineClusters={Array []}
>
<div>
<h5
className="title is-5"
>
Clusters
</h5>
<MetricsWrapper>
<div
className="box"
>
<div
className="level"
>
<Indicator
label="Online Clusters"
>
<div
className="level-item"
>
<div
title="Online Clusters"
>
<p
className="heading"
>
Online Clusters
</p>
<p
className="title has-text-centered"
>
<span
className="tag is-primary"
>
0
</span>
</p>
</div>
</div>
</Indicator>
<Indicator
label="Offline Clusters"
>
<div
className="level-item"
>
<div
title="Offline Clusters"
>
<p
className="heading"
>
Offline Clusters
</p>
<p
className="title has-text-centered"
>
<span
className="tag is-danger"
>
0
</span>
</p>
</div>
</div>
</Indicator>
<Indicator
label="Hide online clusters"
>
<div
className="level-item"
>
<div
title="Hide online clusters"
>
<p
className="heading"
>
Hide online clusters
</p>
<p
className="title has-text-centered"
>
<input
checked={false}
className="switch is-rounded"
id="switchRoundedDefault"
name="switchRoundedDefault"
onChange={[Function]}
type="checkbox"
/>
<label
htmlFor="switchRoundedDefault"
/>
</p>
</div>
</div>
</Indicator>
</div>
</div>
</MetricsWrapper>
</div>
</ClustersWidget>
</Connect(ClustersWidget)>
</div>
</Dashboard>
</Route>
</Switch>
</main>
<div
className="Layout__alerts"
/>
</div>
</App>
</Router>
</StaticRouter>
</Provider>
`; `;

View file

@ -16,7 +16,7 @@ const Indicator: React.FC<Props> = ({
children, children,
}) => { }) => {
return ( return (
<div className={cx('level-item', 'level-left', className)}> <div className={cx('level-item', className)}>
<div title={title || label}> <div title={title || label}>
<p className="heading">{label}</p> <p className="heading">{label}</p>
<p className="title has-text-centered"> <p className="title has-text-centered">

View file

@ -6,7 +6,7 @@ exports[`Indicator matches the snapshot 1`] = `
title="title" title="title"
> >
<div <div
className="level-item level-left" className="level-item"
> >
<div <div
title="title" title="title"