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;
}
&__navbar {
&__sidebar{
width: $navbar-width;
display: flex;
flex-direction: column;
@ -28,6 +28,19 @@ $navbar-width: 250px;
bottom: 0;
padding: 20px 20px;
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 {
@ -47,3 +60,35 @@ $navbar-width: 250px;
.react-datepicker-popper {
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 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 { Alerts } from 'redux/interfaces';
import NavContainer from './Nav/NavContainer';
import Nav from './Nav/Nav';
import PageLoader from './common/PageLoader/PageLoader';
import Dashboard from './Dashboard/Dashboard';
import Cluster from './Cluster/Cluster';
import ClusterPage from './Cluster/Cluster';
import Version from './Version/Version';
import Alert from './Alert/Alert';
export interface AppProps {
isClusterListFetched?: boolean;
alerts: Alerts;
clusters: Cluster[];
fetchClustersList: () => void;
}
const App: React.FC<AppProps> = ({
isClusterListFetched,
alerts,
clusters,
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(() => {
fetchClustersList();
}, [fetchClustersList]);
return (
<div className="Layout">
<div
className={cx('Layout', { 'Layout--sidebarVisible': isSidebarVisible })}
>
<nav
className="navbar is-fixed-top is-white Layout__header"
role="navigation"
aria-label="main navigation"
>
<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">
Kafka UI
</a>
</div>
<div className="navbar-end">
<div className="navbar-item mr-2">
<div className="navbar-item">
<Version tag={GIT_TAG} commit={GIT_COMMIT} />
</div>
</div>
</nav>
<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 ? (
<Switch>
<Route
@ -53,7 +99,7 @@ const App: React.FC<AppProps> = ({
path={['/', '/ui', '/ui/clusters']}
component={Dashboard}
/>
<Route path="/ui/clusters/:clusterName" component={Cluster} />
<Route path="/ui/clusters/:clusterName" component={ClusterPage} />
</Switch>
) : (
<PageLoader fullHeight />

View file

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

View file

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

View file

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

View file

@ -57,27 +57,29 @@ const List: React.FC<ListProps> = ({
<PageLoader />
) : (
<div className="box">
<table className="table is-fullwidth">
<thead>
<tr>
<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 && (
<div className="table-container">
<table className="table is-fullwidth">
<thead>
<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>
)}
</tbody>
</table>
</thead>
<tbody>
{connectors.length === 0 && (
<tr>
<td colSpan={10}>No connectors found</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</div>

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import { Cluster } from 'generated-sources';
import ClusterMenu from './ClusterMenu';
interface Props {
isClusterListFetched: boolean;
isClusterListFetched?: boolean;
clusters: Cluster[];
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} />
</div>
<div className="box">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Version</th>
<th>ID</th>
<th>Schema</th>
</tr>
</thead>
<tbody>
{versions.map((version) => (
<SchemaVersion key={version.id} version={version} />
))}
</tbody>
</table>
<div className="table-container">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Version</th>
<th>ID</th>
<th>Schema</th>
</tr>
</thead>
<tbody>
{versions.map((version) => (
<SchemaVersion key={version.id} version={version} />
))}
</tbody>
</table>
</div>
</div>
</>
) : (

View file

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

View file

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

View file

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

View file

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

View file

@ -81,33 +81,35 @@ const List: React.FC<Props> = ({
<PageLoader />
) : (
<div className="box">
<table className="table is-fullwidth">
<thead>
<tr>
<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 && (
<div className="table-container">
<table className="table is-fullwidth">
<thead>
<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>
)}
</tbody>
</table>
<Pagination totalPages={totalPages} />
</thead>
<tbody>
{items.map((topic) => (
<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>

View file

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

View file

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

View file

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

View file

@ -43,26 +43,28 @@ const Overview: React.FC<Props> = ({
<Indicator label="Segment count">{segmentCount}</Indicator>
</MetricsWrapper>
<div className="box">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Partition ID</th>
<th>Broker leader</th>
<th>Min offset</th>
<th>Max offset</th>
</tr>
</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>
<div className="table-container">
<table className="table is-striped is-fullwidth">
<thead>
<tr>
<th>Partition ID</th>
<th>Broker leader</th>
<th>Min offset</th>
<th>Max offset</th>
</tr>
))}
</tbody>
</table>
</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>
))}
</tbody>
</table>
</div>
</div>
</>
);

View file

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

View file

@ -1,61 +1,79 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { Alert } from 'redux/interfaces';
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 store = configureStore();
describe('App', () => {
const setupComponent = (props: Partial<AppProps> = {}) => (
<App
isClusterListFetched
alerts={[]}
fetchClustersList={fetchClustersList}
{...props}
/>
);
it('handles fetchClustersList', () => {
const wrapper = mount(
describe('container', () => {
it('renders view', () => {
const wrapper = mount(
<Provider store={store}>
<StaticRouter>
<AppContainer />
</StaticRouter>
</Provider>
);
expect(wrapper.exists('App')).toBeTruthy();
});
});
describe('view', () => {
const setupComponent = (props: Partial<AppProps> = {}) => (
<Provider store={store}>
<StaticRouter>{setupComponent()}</StaticRouter>
<StaticRouter>
<App
isClusterListFetched
alerts={[]}
clusters={[]}
fetchClustersList={fetchClustersList}
{...props}
/>
</StaticRouter>
</Provider>
);
expect(wrapper.exists()).toBeTruthy();
expect(fetchClustersList).toHaveBeenCalledTimes(1);
});
it('shows PageLoader until cluster list is fetched', () => {
const component = shallow(setupComponent({ isClusterListFetched: false }));
expect(component.exists('.Layout__container PageLoader')).toBeTruthy();
expect(component.exists('.Layout__container Switch')).toBeFalsy();
component.setProps({ isClusterListFetched: true });
expect(component.exists('.Layout__container PageLoader')).toBeFalsy();
expect(component.exists('.Layout__container Switch')).toBeTruthy();
});
it('handles fetchClustersList', () => {
const wrapper = mount(setupComponent());
expect(wrapper.exists()).toBeTruthy();
expect(fetchClustersList).toHaveBeenCalledTimes(1);
});
it('correctly renders alerts', () => {
const alert = {
id: 'alert-id',
type: 'success',
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();
it('shows PageLoader until cluster list is fetched', () => {
let component = mount(setupComponent({ isClusterListFetched: false }));
expect(component.exists('.Layout__container PageLoader')).toBeTruthy();
expect(component.exists('.Layout__container Switch')).toBeFalsy();
wrapper.setProps({ alerts: [alert] });
expect(wrapper.exists('Alert')).toBeTruthy();
expect(wrapper.find('Alert').length).toEqual(1);
});
component = mount(setupComponent({ isClusterListFetched: true }));
expect(component.exists('.Layout__container PageLoader')).toBeFalsy();
expect(component.exists('.Layout__container Switch')).toBeTruthy();
});
it('matches snapshot', () => {
const component = shallow(setupComponent());
expect(component).toMatchSnapshot();
it('correctly renders alerts', () => {
const alert: Alert = {
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
exports[`App matches snapshot 1`] = `
<div
className="Layout"
exports[`App view matches snapshot 1`] = `
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<nav
aria-label="main navigation"
className="navbar is-fixed-top is-white Layout__header"
role="navigation"
>
<div
className="navbar-brand"
>
<a
className="navbar-item title is-5 is-marginless"
href="/ui"
>
Kafka UI
</a>
</div>
<div
className="navbar-end"
>
<div
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",
]
<StaticRouter>
<Router
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],
}
/>
<Route
component={[Function]}
path="/ui/clusters/:clusterName"
/>
</Switch>
</main>
<div
className="Layout__alerts"
/>
</div>
}
staticContext={Object {}}
>
<App
alerts={Array []}
clusters={Array []}
fetchClustersList={
[MockFunction] {
"calls": Array [
Array [],
],
"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,
}) => {
return (
<div className={cx('level-item', 'level-left', className)}>
<div className={cx('level-item', className)}>
<div title={title || label}>
<p className="heading">{label}</p>
<p className="title has-text-centered">

View file

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