Get rid of SmartTable component (#2444)
* Get rid of SmartTable component * Clickable rows * Improve test coverage
This commit is contained in:
parent
21f17ad39e
commit
e1fb6bacc3
57 changed files with 597 additions and 1647 deletions
|
@ -1,5 +0,0 @@
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const ClickableRow = styled.tr`
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
|
@ -1,25 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterName } from 'redux/interfaces';
|
import { ClusterName } from 'redux/interfaces';
|
||||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { NavLink, useNavigate } from 'react-router-dom';
|
|
||||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
|
||||||
import { Table } from 'components/common/table/Table/Table.styled';
|
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
import * as Metrics from 'components/common/Metrics';
|
import * as Metrics from 'components/common/Metrics';
|
||||||
import useAppParams from 'lib/hooks/useAppParams';
|
import useAppParams from 'lib/hooks/useAppParams';
|
||||||
import { useBrokers } from 'lib/hooks/api/brokers';
|
import { useBrokers } from 'lib/hooks/api/brokers';
|
||||||
import { useClusterStats } from 'lib/hooks/api/clusters';
|
import { useClusterStats } from 'lib/hooks/api/clusters';
|
||||||
|
import Table, { LinkCell, SizeCell } from 'components/common/NewTable';
|
||||||
import { ClickableRow } from './BrokersList.style';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { clusterBrokerPath } from 'lib/paths';
|
||||||
|
|
||||||
const BrokersList: React.FC = () => {
|
const BrokersList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
|
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
|
||||||
const { data: clusterStats } = useClusterStats(clusterName);
|
const { data: clusterStats = {} } = useClusterStats(clusterName);
|
||||||
const { data: brokers } = useBrokers(clusterName);
|
const { data: brokers } = useBrokers(clusterName);
|
||||||
|
|
||||||
if (!clusterStats) return null;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
brokerCount,
|
brokerCount,
|
||||||
activeControllers,
|
activeControllers,
|
||||||
|
@ -32,6 +28,32 @@ const BrokersList: React.FC = () => {
|
||||||
version,
|
version,
|
||||||
} = clusterStats;
|
} = clusterStats;
|
||||||
|
|
||||||
|
const rows = React.useMemo(() => {
|
||||||
|
if (!diskUsage) return [];
|
||||||
|
|
||||||
|
return diskUsage.map(({ brokerId, segmentSize, segmentCount }) => {
|
||||||
|
const broker = brokers?.find(({ id }) => id === brokerId);
|
||||||
|
return {
|
||||||
|
brokerId,
|
||||||
|
size: segmentSize,
|
||||||
|
count: segmentCount,
|
||||||
|
port: broker?.port,
|
||||||
|
host: broker?.host,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [diskUsage, brokers]);
|
||||||
|
|
||||||
|
const columns = React.useMemo<ColumnDef<typeof rows>[]>(
|
||||||
|
() => [
|
||||||
|
{ header: 'Broker ID', accessorKey: 'brokerId', cell: LinkCell },
|
||||||
|
{ header: 'Segment Size', accessorKey: 'size', cell: SizeCell },
|
||||||
|
{ header: 'Segment Count', accessorKey: 'count' },
|
||||||
|
{ header: 'Port', accessorKey: 'port' },
|
||||||
|
{ header: 'Host', accessorKey: 'host' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const replicas = (inSyncReplicasCount ?? 0) + (outOfSyncReplicasCount ?? 0);
|
const replicas = (inSyncReplicasCount ?? 0) + (outOfSyncReplicasCount ?? 0);
|
||||||
const areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount;
|
const areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount;
|
||||||
const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0;
|
const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0;
|
||||||
|
@ -95,49 +117,15 @@ const BrokersList: React.FC = () => {
|
||||||
</Metrics.Indicator>
|
</Metrics.Indicator>
|
||||||
</Metrics.Section>
|
</Metrics.Section>
|
||||||
</Metrics.Wrapper>
|
</Metrics.Wrapper>
|
||||||
<Table isFullwidth>
|
<Table
|
||||||
<thead>
|
columns={columns}
|
||||||
<tr>
|
data={rows}
|
||||||
<TableHeaderCell title="Broker" />
|
enableSorting
|
||||||
<TableHeaderCell title="Segment Size" />
|
onRowClick={({ original: { brokerId } }) =>
|
||||||
<TableHeaderCell title="Segment Count" />
|
navigate(clusterBrokerPath(clusterName, brokerId))
|
||||||
<TableHeaderCell title="Port" />
|
}
|
||||||
<TableHeaderCell title="Host" />
|
emptyMessage="Disk usage data not available"
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{(!diskUsage || diskUsage.length === 0) && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={10}>Disk usage data not available</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{diskUsage &&
|
|
||||||
diskUsage.length !== 0 &&
|
|
||||||
diskUsage.map(({ brokerId, segmentSize, segmentCount }) => {
|
|
||||||
const brokerItem = brokers?.find(({ id }) => id === brokerId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClickableRow
|
|
||||||
key={brokerId}
|
|
||||||
onClick={() => navigate(`${brokerId}`)}
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<NavLink to={`${brokerId}`} role="link">
|
|
||||||
{brokerId}
|
|
||||||
</NavLink>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<BytesFormatted value={segmentSize} />
|
|
||||||
</td>
|
|
||||||
<td>{segmentCount}</td>
|
|
||||||
<td>{brokerItem?.port}</td>
|
|
||||||
<td>{brokerItem?.host}</td>
|
|
||||||
</ClickableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, WithRoute } from 'lib/testHelpers';
|
import { render, WithRoute } from 'lib/testHelpers';
|
||||||
import { screen, waitFor } from '@testing-library/dom';
|
import { screen, waitFor } from '@testing-library/dom';
|
||||||
import { clusterBrokersPath } from 'lib/paths';
|
import { clusterBrokerPath, clusterBrokersPath } from 'lib/paths';
|
||||||
import { act } from '@testing-library/react';
|
import { act } from '@testing-library/react';
|
||||||
import BrokersList from 'components/Brokers/BrokersList/BrokersList';
|
import BrokersList from 'components/Brokers/BrokersList/BrokersList';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
@ -41,84 +41,108 @@ describe('BrokersList Component', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('BrokersList', () => {
|
describe('BrokersList', () => {
|
||||||
beforeEach(() => {
|
describe('when the brokers are loaded', () => {
|
||||||
(useBrokers as jest.Mock).mockImplementation(() => ({
|
beforeEach(() => {
|
||||||
data: brokersPayload,
|
(useBrokers as jest.Mock).mockImplementation(() => ({
|
||||||
}));
|
data: brokersPayload,
|
||||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
}));
|
||||||
data: clusterStatsPayload,
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
}));
|
data: clusterStatsPayload,
|
||||||
});
|
}));
|
||||||
|
});
|
||||||
it('renders', async () => {
|
it('renders', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
const rows = screen.getAllByRole('row');
|
expect(screen.getAllByRole('row').length).toEqual(3);
|
||||||
expect(rows.length).toEqual(3);
|
});
|
||||||
});
|
it('opens broker when row clicked', async () => {
|
||||||
it('opens broker when row clicked', async () => {
|
renderComponent();
|
||||||
renderComponent();
|
await act(() => {
|
||||||
await act(() => {
|
userEvent.click(screen.getByRole('cell', { name: '0' }));
|
||||||
userEvent.click(screen.getByRole('cell', { name: '0' }));
|
});
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mockedUsedNavigate).toBeCalledWith(
|
||||||
|
clusterBrokerPath(clusterName, '0')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('shows warning when offlinePartitionCount > 0', async () => {
|
||||||
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
|
data: {
|
||||||
|
...clusterStatsPayload,
|
||||||
|
offlinePartitionCount: 1345,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
renderComponent();
|
||||||
|
const onlineWidget = screen.getByText(
|
||||||
|
clusterStatsPayload.onlinePartitionCount
|
||||||
|
);
|
||||||
|
expect(onlineWidget).toBeInTheDocument();
|
||||||
|
expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });
|
||||||
|
});
|
||||||
|
it('shows right count when offlinePartitionCount > 0', async () => {
|
||||||
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
|
data: {
|
||||||
|
...clusterStatsPayload,
|
||||||
|
inSyncReplicasCount: testInSyncReplicasCount,
|
||||||
|
outOfSyncReplicasCount: testOutOfSyncReplicasCount,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
renderComponent();
|
||||||
|
const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
|
||||||
|
const onlineWidget = screen.getByText(
|
||||||
|
`of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}`
|
||||||
|
);
|
||||||
|
expect(onlineWidgetDef).toBeInTheDocument();
|
||||||
|
expect(onlineWidget).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('shows right count when inSyncReplicasCount: undefined && outOfSyncReplicasCount: 1', async () => {
|
||||||
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
|
data: {
|
||||||
|
...clusterStatsPayload,
|
||||||
|
inSyncReplicasCount: undefined,
|
||||||
|
outOfSyncReplicasCount: testOutOfSyncReplicasCount,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
renderComponent();
|
||||||
|
const onlineWidget = screen.getByText(
|
||||||
|
`of ${testOutOfSyncReplicasCount}`
|
||||||
|
);
|
||||||
|
expect(onlineWidget).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => {
|
||||||
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
|
data: {
|
||||||
|
...clusterStatsPayload,
|
||||||
|
inSyncReplicasCount: testInSyncReplicasCount,
|
||||||
|
outOfSyncReplicasCount: undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
renderComponent();
|
||||||
|
const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
|
||||||
|
const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`);
|
||||||
|
expect(onlineWidgetDef).toBeInTheDocument();
|
||||||
|
expect(onlineWidget).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(mockedUsedNavigate).toBeCalledWith('0'));
|
|
||||||
});
|
|
||||||
it('shows warning when offlinePartitionCount > 0', async () => {
|
|
||||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
|
||||||
data: {
|
|
||||||
...clusterStatsPayload,
|
|
||||||
offlinePartitionCount: 1345,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
renderComponent();
|
|
||||||
const onlineWidget = screen.getByText(
|
|
||||||
clusterStatsPayload.onlinePartitionCount
|
|
||||||
);
|
|
||||||
expect(onlineWidget).toBeInTheDocument();
|
|
||||||
expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });
|
|
||||||
});
|
|
||||||
it('shows right count when offlinePartitionCount > 0', async () => {
|
|
||||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
|
||||||
data: {
|
|
||||||
...clusterStatsPayload,
|
|
||||||
inSyncReplicasCount: testInSyncReplicasCount,
|
|
||||||
outOfSyncReplicasCount: testOutOfSyncReplicasCount,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
renderComponent();
|
|
||||||
const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
|
|
||||||
const onlineWidget = screen.getByText(
|
|
||||||
`of ${testInSyncReplicasCount + testOutOfSyncReplicasCount}`
|
|
||||||
);
|
|
||||||
expect(onlineWidgetDef).toBeInTheDocument();
|
|
||||||
expect(onlineWidget).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows right count when inSyncReplicasCount: undefined outOfSyncReplicasCount: 1', async () => {
|
describe('when diskUsage is empty', () => {
|
||||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
beforeEach(() => {
|
||||||
data: {
|
(useBrokers as jest.Mock).mockImplementation(() => ({
|
||||||
...clusterStatsPayload,
|
data: brokersPayload,
|
||||||
inSyncReplicasCount: undefined,
|
}));
|
||||||
outOfSyncReplicasCount: testOutOfSyncReplicasCount,
|
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||||
},
|
data: { ...clusterStatsPayload, diskUsage: undefined },
|
||||||
}));
|
}));
|
||||||
renderComponent();
|
});
|
||||||
const onlineWidget = screen.getByText(`of ${testOutOfSyncReplicasCount}`);
|
|
||||||
expect(onlineWidget).toBeInTheDocument();
|
it('renders empty table', async () => {
|
||||||
});
|
renderComponent();
|
||||||
it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => {
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
expect(
|
||||||
data: {
|
screen.getByRole('row', { name: 'Disk usage data not available' })
|
||||||
...clusterStatsPayload,
|
).toBeInTheDocument();
|
||||||
inSyncReplicasCount: testInSyncReplicasCount,
|
});
|
||||||
outOfSyncReplicasCount: undefined,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
renderComponent();
|
|
||||||
const onlineWidgetDef = screen.getByText(testInSyncReplicasCount);
|
|
||||||
const onlineWidget = screen.getByText(`of ${testInSyncReplicasCount}`);
|
|
||||||
expect(onlineWidgetDef).toBeInTheDocument();
|
|
||||||
expect(onlineWidget).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,7 +35,7 @@ const Overview: React.FC = () => {
|
||||||
</Metrics.Indicator>
|
</Metrics.Indicator>
|
||||||
)}
|
)}
|
||||||
<Metrics.Indicator label="State">
|
<Metrics.Indicator label="State">
|
||||||
<C.Tag color={getTagColor(connector.status)}>
|
<C.Tag color={getTagColor(connector.status.state)}>
|
||||||
{connector.status.state}
|
{connector.status.state}
|
||||||
</C.Tag>
|
</C.Tag>
|
||||||
</Metrics.Indicator>
|
</Metrics.Indicator>
|
||||||
|
|
|
@ -43,7 +43,9 @@ const Tasks: React.FC = () => {
|
||||||
<td>{task.status?.id}</td>
|
<td>{task.status?.id}</td>
|
||||||
<td>{task.status?.workerId}</td>
|
<td>{task.status?.workerId}</td>
|
||||||
<td>
|
<td>
|
||||||
<Tag color={getTagColor(task.status)}>{task.status.state}</Tag>
|
<Tag color={getTagColor(task.status.state)}>
|
||||||
|
{task.status.state}
|
||||||
|
</Tag>
|
||||||
</td>
|
</td>
|
||||||
<td>{task.status.trace || 'null'}</td>
|
<td>{task.status.trace || 'null'}</td>
|
||||||
<td style={{ width: '5%' }}>
|
<td style={{ width: '5%' }}>
|
||||||
|
|
|
@ -72,7 +72,9 @@ const ListItem: React.FC<ListItemProps> = ({
|
||||||
))}
|
))}
|
||||||
</S.TagsWrapper>
|
</S.TagsWrapper>
|
||||||
</td>
|
</td>
|
||||||
<td>{status && <Tag color={getTagColor(status)}>{status.state}</Tag>}</td>
|
<td>
|
||||||
|
{status && <Tag color={getTagColor(status.state)}>{status.state}</Tag>}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{runningTasks && (
|
{runningTasks && (
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -82,7 +82,9 @@ const Details: React.FC = () => {
|
||||||
<Metrics.Wrapper>
|
<Metrics.Wrapper>
|
||||||
<Metrics.Section>
|
<Metrics.Section>
|
||||||
<Metrics.Indicator label="State">
|
<Metrics.Indicator label="State">
|
||||||
<Tag color={getTagColor(consumerGroup)}>{consumerGroup.state}</Tag>
|
<Tag color={getTagColor(consumerGroup.state)}>
|
||||||
|
{consumerGroup.state}
|
||||||
|
</Tag>
|
||||||
</Metrics.Indicator>
|
</Metrics.Indicator>
|
||||||
<Metrics.Indicator label="Members">
|
<Metrics.Indicator label="Members">
|
||||||
{consumerGroup.members}
|
{consumerGroup.members}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Tag } from 'components/common/Tag/Tag.styled';
|
|
||||||
import { TableCellProps } from 'components/common/SmartTable/TableColumn';
|
|
||||||
import { ConsumerGroup } from 'generated-sources';
|
|
||||||
import { SmartTableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
|
||||||
import getTagColor from 'components/common/Tag/getTagColor';
|
|
||||||
|
|
||||||
export const StatusCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
|
|
||||||
dataItem,
|
|
||||||
}) => {
|
|
||||||
return <Tag color={getTagColor(dataItem)}>{dataItem.state}</Tag>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GroupIDCell: React.FC<TableCellProps<ConsumerGroup, string>> = ({
|
|
||||||
dataItem: { groupId },
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<SmartTableKeyLink>
|
|
||||||
<Link to={groupId}>{groupId}</Link>
|
|
||||||
</SmartTableKeyLink>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -7,75 +7,84 @@ import {
|
||||||
ConsumerGroupOrdering,
|
ConsumerGroupOrdering,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from 'generated-sources';
|
} from 'generated-sources';
|
||||||
import { useTableState } from 'lib/hooks/useTableState';
|
|
||||||
import { SmartTable } from 'components/common/SmartTable/SmartTable';
|
|
||||||
import { TableColumn } from 'components/common/SmartTable/TableColumn';
|
|
||||||
import {
|
|
||||||
GroupIDCell,
|
|
||||||
StatusCell,
|
|
||||||
} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells';
|
|
||||||
import usePagination from 'lib/hooks/usePagination';
|
|
||||||
import useSearch from 'lib/hooks/useSearch';
|
import useSearch from 'lib/hooks/useSearch';
|
||||||
import { useAppDispatch } from 'lib/hooks/redux';
|
import { useAppDispatch } from 'lib/hooks/redux';
|
||||||
import useAppParams from 'lib/hooks/useAppParams';
|
import useAppParams from 'lib/hooks/useAppParams';
|
||||||
import { ClusterNameRoute } from 'lib/paths';
|
import { clusterConsumerGroupDetailsPath, ClusterNameRoute } from 'lib/paths';
|
||||||
import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
import { fetchConsumerGroupsPaged } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import Table, { TagCell, LinkCell } from 'components/common/NewTable';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { PER_PAGE } from 'lib/constants';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
consumerGroups: ConsumerGroupDetails[];
|
consumerGroups: ConsumerGroupDetails[];
|
||||||
orderBy: string | null;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
isFetched: boolean;
|
|
||||||
setConsumerGroupsSortOrderBy(orderBy: string | null): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: React.FC<Props> = ({
|
const List: React.FC<Props> = ({ consumerGroups, totalPages }) => {
|
||||||
consumerGroups,
|
|
||||||
sortOrder,
|
|
||||||
orderBy,
|
|
||||||
totalPages,
|
|
||||||
isFetched,
|
|
||||||
setConsumerGroupsSortOrderBy,
|
|
||||||
}) => {
|
|
||||||
const { page, perPage } = usePagination();
|
|
||||||
const [searchText, handleSearchText] = useSearch();
|
const [searchText, handleSearchText] = useSearch();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
fetchConsumerGroupsPaged({
|
fetchConsumerGroupsPaged({
|
||||||
clusterName,
|
clusterName,
|
||||||
orderBy: (orderBy as ConsumerGroupOrdering) || undefined,
|
orderBy:
|
||||||
sortOrder,
|
(searchParams.get('sortBy') as ConsumerGroupOrdering) || undefined,
|
||||||
page,
|
sortOrder:
|
||||||
perPage,
|
(searchParams.get('sortDirection')?.toUpperCase() as SortOrder) ||
|
||||||
|
undefined,
|
||||||
|
page: Number(searchParams.get('page') || 1),
|
||||||
|
perPage: Number(searchParams.get('perPage') || PER_PAGE),
|
||||||
search: searchText,
|
search: searchText,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [clusterName, orderBy, searchText, sortOrder, page, perPage, dispatch]);
|
}, [clusterName, searchText, dispatch, searchParams]);
|
||||||
|
|
||||||
const tableState = useTableState<ConsumerGroupDetails, string>(
|
const columns = React.useMemo<ColumnDef<ConsumerGroupDetails>[]>(
|
||||||
consumerGroups,
|
() => [
|
||||||
{
|
{
|
||||||
totalPages,
|
id: ConsumerGroupOrdering.NAME,
|
||||||
idSelector: (consumerGroup) => consumerGroup.groupId,
|
header: 'Group ID',
|
||||||
},
|
accessorKey: 'groupId',
|
||||||
{
|
cell: LinkCell,
|
||||||
handleOrderBy: setConsumerGroupsSortOrderBy,
|
},
|
||||||
orderBy,
|
{
|
||||||
sortOrder,
|
id: ConsumerGroupOrdering.MEMBERS,
|
||||||
}
|
header: 'Num Of Members',
|
||||||
|
accessorKey: 'members',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Num Of Topics',
|
||||||
|
accessorKey: 'topics',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Messages Behind',
|
||||||
|
accessorKey: 'messagesBehind',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Coordinator',
|
||||||
|
accessorKey: 'coordinator.id',
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ConsumerGroupOrdering.STATE,
|
||||||
|
header: 'State',
|
||||||
|
accessorKey: 'state',
|
||||||
|
cell: TagCell,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isFetched) {
|
|
||||||
return <PageLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<PageHeading text="Consumers" />
|
<PageHeading text="Consumers" />
|
||||||
<ControlPanelWrapper hasInput>
|
<ControlPanelWrapper hasInput>
|
||||||
<Search
|
<Search
|
||||||
|
@ -84,33 +93,20 @@ const List: React.FC<Props> = ({
|
||||||
handleSearch={handleSearchText}
|
handleSearch={handleSearchText}
|
||||||
/>
|
/>
|
||||||
</ControlPanelWrapper>
|
</ControlPanelWrapper>
|
||||||
<SmartTable
|
<Table
|
||||||
tableState={tableState}
|
columns={columns}
|
||||||
isFullwidth
|
pageCount={totalPages}
|
||||||
placeholder="No active consumer groups"
|
data={consumerGroups}
|
||||||
hoverable
|
emptyMessage="No active consumer groups found"
|
||||||
paginated
|
serverSideProcessing
|
||||||
>
|
enableSorting
|
||||||
<TableColumn
|
onRowClick={({ original }) =>
|
||||||
title="Consumer Group ID"
|
navigate(
|
||||||
cell={GroupIDCell}
|
clusterConsumerGroupDetailsPath(clusterName, original.groupId)
|
||||||
orderValue={ConsumerGroupOrdering.NAME}
|
)
|
||||||
/>
|
}
|
||||||
<TableColumn
|
/>
|
||||||
title="Num Of Members"
|
</>
|
||||||
field="members"
|
|
||||||
orderValue={ConsumerGroupOrdering.MEMBERS}
|
|
||||||
/>
|
|
||||||
<TableColumn title="Num Of Topics" field="topics" />
|
|
||||||
<TableColumn title="Messages Behind" field="messagesBehind" />
|
|
||||||
<TableColumn title="Coordinator" field="coordinator.id" />
|
|
||||||
<TableColumn
|
|
||||||
title="State"
|
|
||||||
cell={StatusCell}
|
|
||||||
orderValue={ConsumerGroupOrdering.STATE}
|
|
||||||
/>
|
|
||||||
</SmartTable>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,24 +2,15 @@ import { connect } from 'react-redux';
|
||||||
import { RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
import {
|
import {
|
||||||
getConsumerGroupsOrderBy,
|
getConsumerGroupsOrderBy,
|
||||||
getConsumerGroupsSortOrder,
|
|
||||||
getConsumerGroupsTotalPages,
|
getConsumerGroupsTotalPages,
|
||||||
sortBy,
|
|
||||||
selectAll,
|
selectAll,
|
||||||
getAreConsumerGroupsPagedFulfilled,
|
|
||||||
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||||
import List from 'components/ConsumerGroups/List/List';
|
import List from 'components/ConsumerGroups/List/List';
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState) => ({
|
const mapStateToProps = (state: RootState) => ({
|
||||||
consumerGroups: selectAll(state),
|
consumerGroups: selectAll(state),
|
||||||
orderBy: getConsumerGroupsOrderBy(state),
|
orderBy: getConsumerGroupsOrderBy(state),
|
||||||
sortOrder: getConsumerGroupsSortOrder(state),
|
|
||||||
totalPages: getConsumerGroupsTotalPages(state),
|
totalPages: getConsumerGroupsTotalPages(state),
|
||||||
isFetched: getAreConsumerGroupsPagedFulfilled(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
export default connect(mapStateToProps)(List);
|
||||||
setConsumerGroupsSortOrderBy: sortBy,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { ConsumerGroup } from 'generated-sources';
|
|
||||||
import { Tag } from 'components/common/Tag/Tag.styled';
|
|
||||||
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
|
||||||
import getTagColor from 'components/common/Tag/getTagColor';
|
|
||||||
|
|
||||||
const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
|
|
||||||
consumerGroup,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<TableKeyLink>
|
|
||||||
<Link to={`consumer-groups/${consumerGroup.groupId}`}>
|
|
||||||
{consumerGroup.groupId}
|
|
||||||
</Link>
|
|
||||||
</TableKeyLink>
|
|
||||||
<td>{consumerGroup.members}</td>
|
|
||||||
<td>{consumerGroup.topics}</td>
|
|
||||||
<td>{consumerGroup.messagesBehind}</td>
|
|
||||||
<td>{consumerGroup.coordinator?.id}</td>
|
|
||||||
<td>
|
|
||||||
<Tag color={getTagColor(consumerGroup)}>{consumerGroup.state}</Tag>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListItem;
|
|
|
@ -1,61 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
import {
|
|
||||||
GroupIDCell,
|
|
||||||
StatusCell,
|
|
||||||
} from 'components/ConsumerGroups/List/ConsumerGroupsTableCells';
|
|
||||||
import { TableState } from 'lib/hooks/useTableState';
|
|
||||||
import { ConsumerGroup, ConsumerGroupState } from 'generated-sources';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
|
|
||||||
describe('Consumer Groups Table Cells', () => {
|
|
||||||
const consumerGroup: ConsumerGroup = {
|
|
||||||
groupId: 'groupId',
|
|
||||||
members: 1,
|
|
||||||
topics: 1,
|
|
||||||
simple: true,
|
|
||||||
state: ConsumerGroupState.STABLE,
|
|
||||||
coordinator: {
|
|
||||||
id: 6598,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const mockTableState: TableState<ConsumerGroup, string> = {
|
|
||||||
data: [consumerGroup],
|
|
||||||
selectedIds: new Set([]),
|
|
||||||
idSelector: jest.fn(),
|
|
||||||
isRowSelectable: jest.fn(),
|
|
||||||
selectedCount: 0,
|
|
||||||
setRowsSelection: jest.fn(),
|
|
||||||
toggleSelection: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('StatusCell', () => {
|
|
||||||
it('should Tag props render normally', () => {
|
|
||||||
render(
|
|
||||||
<GroupIDCell
|
|
||||||
rowIndex={1}
|
|
||||||
dataItem={consumerGroup}
|
|
||||||
tableState={mockTableState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const linkElement = screen.getByRole('link');
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
expect(linkElement).toHaveAttribute('href', `/${consumerGroup.groupId}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GroupIdCell', () => {
|
|
||||||
it('should GroupIdCell props render normally', () => {
|
|
||||||
render(
|
|
||||||
<StatusCell
|
|
||||||
rowIndex={1}
|
|
||||||
dataItem={consumerGroup}
|
|
||||||
tableState={mockTableState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
screen.getByText(consumerGroup.state as string)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,77 +1,59 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import List, { Props } from 'components/ConsumerGroups/List/List';
|
import List, { Props } from 'components/ConsumerGroups/List/List';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||||
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
|
import { clusterConsumerGroupDetailsPath } from 'lib/paths';
|
||||||
import theme from 'theme/theme';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import ListContainer from 'components/ConsumerGroups/List/ListContainer';
|
||||||
|
|
||||||
|
const mockedUsedNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: () => mockedUsedNavigate,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ListContainer', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
render(<ListContainer />);
|
||||||
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('List', () => {
|
describe('List', () => {
|
||||||
const setUpComponent = (props: Partial<Props> = {}) => {
|
const renderComponent = (props: Partial<Props> = {}) => {
|
||||||
const {
|
const { consumerGroups, totalPages } = props;
|
||||||
consumerGroups,
|
|
||||||
orderBy,
|
|
||||||
sortOrder,
|
|
||||||
totalPages,
|
|
||||||
setConsumerGroupsSortOrderBy,
|
|
||||||
} = props;
|
|
||||||
return render(
|
return render(
|
||||||
<List
|
<List
|
||||||
consumerGroups={consumerGroups || []}
|
consumerGroups={consumerGroups || []}
|
||||||
orderBy={orderBy || ConsumerGroupOrdering.NAME}
|
|
||||||
sortOrder={sortOrder || SortOrder.ASC}
|
|
||||||
setConsumerGroupsSortOrderBy={setConsumerGroupsSortOrderBy || jest.fn()}
|
|
||||||
totalPages={totalPages || 1}
|
totalPages={totalPages || 1}
|
||||||
isFetched={'isFetched' in props ? !!props.isFetched : true}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders empty table', () => {
|
it('renders empty table', () => {
|
||||||
setUpComponent();
|
renderComponent();
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText('No active consumer groups found')
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('consumerGroups are fetched', () => {
|
describe('consumerGroups are fetched', () => {
|
||||||
beforeEach(() => setUpComponent({ consumerGroups: consumerGroupMock }));
|
beforeEach(() => renderComponent({ consumerGroups: consumerGroupMock }));
|
||||||
|
|
||||||
it('renders all rows with consumers', () => {
|
it('renders all rows with consumers', () => {
|
||||||
expect(screen.getByText('groupId1')).toBeInTheDocument();
|
expect(screen.getByText('groupId1')).toBeInTheDocument();
|
||||||
expect(screen.getByText('groupId2')).toBeInTheDocument();
|
expect(screen.getByText('groupId2')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Testing the Ordering', () => {
|
it('handles onRowClick', () => {
|
||||||
it('should test the sort order functionality', async () => {
|
const row = screen.getByRole('row', { name: 'groupId1 0 1 1' });
|
||||||
const thElement = screen.getByText(/consumer group id/i);
|
expect(row).toBeInTheDocument();
|
||||||
expect(thElement).toBeInTheDocument();
|
userEvent.click(row);
|
||||||
expect(thElement).toHaveStyle(`color:${theme.table.th.color.active}`);
|
expect(mockedUsedNavigate).toHaveBeenCalledWith(
|
||||||
});
|
clusterConsumerGroupDetailsPath(':clusterName', 'groupId1')
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('consumerGroups are fetched with custom parameters', () => {
|
|
||||||
it('should test the order by functionality of another element', async () => {
|
|
||||||
const sortOrder = jest.fn();
|
|
||||||
setUpComponent({
|
|
||||||
consumerGroups: consumerGroupMock,
|
|
||||||
setConsumerGroupsSortOrderBy: sortOrder,
|
|
||||||
});
|
|
||||||
const thElement = screen.getByText(/num of members/i);
|
|
||||||
expect(thElement).toBeInTheDocument();
|
|
||||||
|
|
||||||
userEvent.click(thElement);
|
|
||||||
expect(sortOrder).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should view the ordered list with the right prop', () => {
|
|
||||||
setUpComponent({
|
|
||||||
consumerGroups: consumerGroupMock,
|
|
||||||
orderBy: ConsumerGroupOrdering.MEMBERS,
|
|
||||||
});
|
|
||||||
expect(screen.getByText(/num of members/i)).toHaveStyle(
|
|
||||||
`color:${theme.table.th.color.active}`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ListItem from 'components/ConsumerGroups/List/ListItem';
|
|
||||||
import { ConsumerGroupState, ConsumerGroup } from 'generated-sources';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
|
|
||||||
describe('List', () => {
|
|
||||||
const mockConsumerGroup = {
|
|
||||||
groupId: 'groupId',
|
|
||||||
members: 0,
|
|
||||||
topics: 1,
|
|
||||||
simple: false,
|
|
||||||
partitionAssignor: '',
|
|
||||||
coordinator: {
|
|
||||||
id: 1,
|
|
||||||
host: 'host',
|
|
||||||
},
|
|
||||||
partitions: [
|
|
||||||
{
|
|
||||||
consumerId: null,
|
|
||||||
currentOffset: 0,
|
|
||||||
endOffset: 0,
|
|
||||||
host: null,
|
|
||||||
messagesBehind: 0,
|
|
||||||
partition: 1,
|
|
||||||
topic: 'topic',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const setupWrapper = (consumerGroup: ConsumerGroup) => (
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ListItem consumerGroup={consumerGroup} />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getCell = () => screen.getAllByRole('cell')[5];
|
|
||||||
|
|
||||||
it('render empty ListItem', () => {
|
|
||||||
render(setupWrapper(mockConsumerGroup));
|
|
||||||
expect(screen.getByRole('row')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders item with stable status', () => {
|
|
||||||
render(
|
|
||||||
setupWrapper({
|
|
||||||
...mockConsumerGroup,
|
|
||||||
state: ConsumerGroupState.STABLE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('row')).toHaveTextContent(
|
|
||||||
ConsumerGroupState.STABLE
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders item with dead status', () => {
|
|
||||||
render(
|
|
||||||
setupWrapper({
|
|
||||||
...mockConsumerGroup,
|
|
||||||
state: ConsumerGroupState.DEAD,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(getCell()).toHaveTextContent(ConsumerGroupState.DEAD);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders item with empty status', () => {
|
|
||||||
render(
|
|
||||||
setupWrapper({
|
|
||||||
...mockConsumerGroup,
|
|
||||||
state: ConsumerGroupState.EMPTY,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(getCell()).toHaveTextContent(ConsumerGroupState.EMPTY);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders item with empty-string status', () => {
|
|
||||||
render(
|
|
||||||
setupWrapper({
|
|
||||||
...mockConsumerGroup,
|
|
||||||
state: ConsumerGroupState.UNKNOWN,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(getCell()).toHaveTextContent(ConsumerGroupState.UNKNOWN);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,22 +1,27 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths';
|
|
||||||
import {
|
import {
|
||||||
act,
|
clusterConsumerGroupDetailsPath,
|
||||||
screen,
|
clusterConsumerGroupResetOffsetsPath,
|
||||||
waitFor,
|
clusterConsumerGroupsPath,
|
||||||
waitForElementToBeRemoved,
|
getNonExactPath,
|
||||||
} from '@testing-library/react';
|
} from 'lib/paths';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';
|
import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';
|
||||||
import {
|
|
||||||
consumerGroups,
|
|
||||||
noConsumerGroupsResponse,
|
|
||||||
} from 'redux/reducers/consumerGroups/__test__/fixtures';
|
|
||||||
import { render, WithRoute } from 'lib/testHelpers';
|
import { render, WithRoute } from 'lib/testHelpers';
|
||||||
import fetchMock from 'fetch-mock';
|
|
||||||
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
|
|
||||||
|
|
||||||
const clusterName = 'cluster1';
|
const clusterName = 'cluster1';
|
||||||
|
|
||||||
|
jest.mock('components/ConsumerGroups/List/ListContainer', () => () => (
|
||||||
|
<div>ListContainerMock</div>
|
||||||
|
));
|
||||||
|
jest.mock('components/ConsumerGroups/Details/Details', () => () => (
|
||||||
|
<div>DetailsMock</div>
|
||||||
|
));
|
||||||
|
jest.mock(
|
||||||
|
'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets',
|
||||||
|
() => () => <div>ResetOffsetsMock</div>
|
||||||
|
);
|
||||||
|
|
||||||
const renderComponent = (path?: string) =>
|
const renderComponent = (path?: string) =>
|
||||||
render(
|
render(
|
||||||
<WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>
|
<WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>
|
||||||
|
@ -28,104 +33,18 @@ const renderComponent = (path?: string) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('ConsumerGroups', () => {
|
describe('ConsumerGroups', () => {
|
||||||
it('renders with initial state', async () => {
|
it('renders ListContainer', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
expect(screen.getByText('ListContainerMock')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('renders ResetOffsets', async () => {
|
||||||
describe('Default Route and Fetching Consumer Groups', () => {
|
renderComponent(
|
||||||
const url = `/api/clusters/${clusterName}/consumer-groups/paged`;
|
clusterConsumerGroupResetOffsetsPath(clusterName, 'groupId1')
|
||||||
afterEach(() => {
|
);
|
||||||
fetchMock.reset();
|
expect(screen.getByText('ResetOffsetsMock')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('renders Details', async () => {
|
||||||
it('renders empty table on no consumer group response', async () => {
|
renderComponent(clusterConsumerGroupDetailsPath(clusterName, 'groupId1'));
|
||||||
fetchMock.getOnce(url, noConsumerGroupsResponse, {
|
expect(screen.getByText('DetailsMock')).toBeInTheDocument();
|
||||||
query: {
|
|
||||||
orderBy: ConsumerGroupOrdering.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await act(() => {
|
|
||||||
renderComponent();
|
|
||||||
});
|
|
||||||
expect(fetchMock.calls().length).toBe(1);
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with 404 from consumer groups', async () => {
|
|
||||||
const consumerGroupsMock = fetchMock.getOnce(url, 404, {
|
|
||||||
query: {
|
|
||||||
orderBy: ConsumerGroupOrdering.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());
|
|
||||||
|
|
||||||
expect(screen.queryByText('Consumers')).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole('table')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with 200 from consumer groups', async () => {
|
|
||||||
const consumerGroupsMock = fetchMock.getOnce(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
pagedCount: 1,
|
|
||||||
consumerGroups,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
orderBy: ConsumerGroupOrdering.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
|
|
||||||
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());
|
|
||||||
|
|
||||||
expect(screen.getByText('Consumers')).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(consumerGroups[0].groupId)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(consumerGroups[1].groupId)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with 200 from consumer groups with Searched Query ', async () => {
|
|
||||||
const searchResult = consumerGroups[0];
|
|
||||||
const searchText = searchResult.groupId;
|
|
||||||
|
|
||||||
const consumerGroupsMock = fetchMock.getOnce(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
pagedCount: 1,
|
|
||||||
consumerGroups: [searchResult],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: {
|
|
||||||
orderBy: ConsumerGroupOrdering.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
search: searchText,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
renderComponent(
|
|
||||||
`${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
|
|
||||||
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());
|
|
||||||
|
|
||||||
expect(screen.getByText(searchText)).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.queryByText(consumerGroups[1].groupId)
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources';
|
import { KsqlStreamDescription, KsqlTableDescription } from 'generated-sources';
|
||||||
import { useTableState } from 'lib/hooks/useTableState';
|
|
||||||
import { SmartTable } from 'components/common/SmartTable/SmartTable';
|
|
||||||
import { TableColumn } from 'components/common/SmartTable/TableColumn';
|
|
||||||
import { ksqlRowData } from 'components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData';
|
import { ksqlRowData } from 'components/KsqlDb/List/KsqlDbItem/utils/ksqlRowData';
|
||||||
|
import Table from 'components/common/NewTable';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
export enum KsqlDbItemType {
|
export enum KsqlDbItemType {
|
||||||
Tables = 'tables',
|
Tables = 'tables',
|
||||||
|
@ -31,27 +30,28 @@ export interface KsqlTableState {
|
||||||
|
|
||||||
const KsqlDbItem: React.FC<KsqlDbItemProps> = ({ type, fetching, rows }) => {
|
const KsqlDbItem: React.FC<KsqlDbItemProps> = ({ type, fetching, rows }) => {
|
||||||
const preparedRows = rows[type]?.map(ksqlRowData) || [];
|
const preparedRows = rows[type]?.map(ksqlRowData) || [];
|
||||||
const tableState = useTableState<KsqlTableState, string>(preparedRows, {
|
|
||||||
idSelector: ({ name }) => name,
|
const columns = React.useMemo<ColumnDef<KsqlTableState>[]>(
|
||||||
totalPages: 0,
|
() => [
|
||||||
});
|
{ header: 'Name', accessorKey: 'name' },
|
||||||
|
{ header: 'Topic', accessorKey: 'topic' },
|
||||||
|
{ header: 'Key Format', accessorKey: 'keyFormat' },
|
||||||
|
{ header: 'Value Format', accessorKey: 'valueFormat' },
|
||||||
|
{ header: 'Is Windowed', accessorKey: 'isWindowed' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
if (fetching) {
|
if (fetching) {
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SmartTable
|
<Table
|
||||||
tableState={tableState}
|
data={preparedRows}
|
||||||
isFullwidth
|
columns={columns}
|
||||||
placeholder="No tables or streams found"
|
emptyMessage="No tables or streams found"
|
||||||
hoverable
|
enableSorting={false}
|
||||||
>
|
/>
|
||||||
<TableColumn title="Name" field="name" />
|
|
||||||
<TableColumn title="Topic" field="topic" />
|
|
||||||
<TableColumn title="Key Format" field="keyFormat" />
|
|
||||||
<TableColumn title="Value Format" field="valueFormat" />
|
|
||||||
<TableColumn title="Is Windowed" field="isWindowed" />
|
|
||||||
</SmartTable>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,59 +7,56 @@ import KsqlDbItem, {
|
||||||
} from 'components/KsqlDb/List/KsqlDbItem/KsqlDbItem';
|
} from 'components/KsqlDb/List/KsqlDbItem/KsqlDbItem';
|
||||||
import { screen } from '@testing-library/dom';
|
import { screen } from '@testing-library/dom';
|
||||||
import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
|
import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
|
||||||
|
import { act } from '@testing-library/react';
|
||||||
|
|
||||||
describe('KsqlDbItem', () => {
|
describe('KsqlDbItem', () => {
|
||||||
const tablesPathname = clusterKsqlDbTablesPath();
|
const tablesPathname = clusterKsqlDbTablesPath();
|
||||||
|
const renderComponent = (props: Partial<KsqlDbItemProps> = {}) => {
|
||||||
const component = (props: Partial<KsqlDbItemProps> = {}) => (
|
|
||||||
<WithRoute path={tablesPathname}>
|
|
||||||
<KsqlDbItem
|
|
||||||
type={KsqlDbItemType.Tables}
|
|
||||||
fetching={false}
|
|
||||||
rows={{ tables: [], streams: [] }}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</WithRoute>
|
|
||||||
);
|
|
||||||
|
|
||||||
it('renders progressbar when fetching tables and streams', () => {
|
|
||||||
render(component({ fetching: true }), {
|
|
||||||
initialEntries: [clusterKsqlDbTablesPath()],
|
|
||||||
});
|
|
||||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('show no text if no data found', () => {
|
|
||||||
render(component({}), {
|
|
||||||
initialEntries: [clusterKsqlDbTablesPath()],
|
|
||||||
});
|
|
||||||
expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('renders with tables', () => {
|
|
||||||
render(
|
render(
|
||||||
component({
|
<WithRoute path={tablesPathname}>
|
||||||
rows: {
|
<KsqlDbItem
|
||||||
tables: fetchKsqlDbTablesPayload.tables,
|
type={KsqlDbItemType.Tables}
|
||||||
streams: [],
|
fetching={false}
|
||||||
},
|
rows={{ tables: [], streams: [] }}
|
||||||
}),
|
{...props}
|
||||||
|
/>
|
||||||
|
</WithRoute>,
|
||||||
{
|
{
|
||||||
initialEntries: [clusterKsqlDbTablesPath()],
|
initialEntries: [clusterKsqlDbTablesPath()],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders progressbar when fetching tables and streams', async () => {
|
||||||
|
await act(() => renderComponent({ fetching: true }));
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show no text if no data found', async () => {
|
||||||
|
await act(() => renderComponent({}));
|
||||||
|
expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with tables', async () => {
|
||||||
|
await act(() =>
|
||||||
|
renderComponent({
|
||||||
|
rows: {
|
||||||
|
tables: fetchKsqlDbTablesPayload.tables,
|
||||||
|
streams: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
|
expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
|
||||||
});
|
});
|
||||||
it('renders with streams', () => {
|
it('renders with streams', async () => {
|
||||||
render(
|
await act(() =>
|
||||||
component({
|
renderComponent({
|
||||||
type: KsqlDbItemType.Streams,
|
type: KsqlDbItemType.Streams,
|
||||||
rows: {
|
rows: {
|
||||||
tables: [],
|
tables: [],
|
||||||
streams: fetchKsqlDbTablesPayload.streams,
|
streams: fetchKsqlDbTablesPayload.streams,
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
{
|
|
||||||
initialEntries: [clusterKsqlDbTablesPath()],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
|
expect(screen.getByRole('table').querySelectorAll('td')).toHaveLength(10);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,15 +3,14 @@ import List from 'components/KsqlDb/List/List';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import { screen } from '@testing-library/dom';
|
import { screen } from '@testing-library/dom';
|
||||||
|
import { act } from '@testing-library/react';
|
||||||
const renderComponent = () => {
|
|
||||||
render(<List />);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('KsqlDb List', () => {
|
describe('KsqlDb List', () => {
|
||||||
afterEach(() => fetchMock.reset());
|
afterEach(() => fetchMock.reset());
|
||||||
it('renders List component with Tables and Streams tabs', async () => {
|
it('renders List component with Tables and Streams tabs', async () => {
|
||||||
renderComponent();
|
await act(() => {
|
||||||
|
render(<List />);
|
||||||
|
});
|
||||||
|
|
||||||
const Tables = screen.getByTitle('Tables');
|
const Tables = screen.getByTitle('Tables');
|
||||||
const Streams = screen.getByTitle('Streams');
|
const Streams = screen.getByTitle('Streams');
|
||||||
|
|
|
@ -2,22 +2,21 @@ import React from 'react';
|
||||||
import Select from 'components/common/Select/Select';
|
import Select from 'components/common/Select/Select';
|
||||||
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
||||||
import { useAppDispatch } from 'lib/hooks/redux';
|
import { useAppDispatch } from 'lib/hooks/redux';
|
||||||
import usePagination from 'lib/hooks/usePagination';
|
|
||||||
import useSearch from 'lib/hooks/useSearch';
|
|
||||||
import useAppParams from 'lib/hooks/useAppParams';
|
import useAppParams from 'lib/hooks/useAppParams';
|
||||||
import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
|
import { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
|
||||||
import { ClusterNameRoute } from 'lib/paths';
|
import { ClusterNameRoute } from 'lib/paths';
|
||||||
import { schemasApiClient } from 'lib/api';
|
import { schemasApiClient } from 'lib/api';
|
||||||
import { showServerError } from 'lib/errorHandling';
|
import { showServerError } from 'lib/errorHandling';
|
||||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { PER_PAGE } from 'lib/constants';
|
||||||
|
|
||||||
import * as S from './GlobalSchemaSelector.styled';
|
import * as S from './GlobalSchemaSelector.styled';
|
||||||
|
|
||||||
const GlobalSchemaSelector: React.FC = () => {
|
const GlobalSchemaSelector: React.FC = () => {
|
||||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [searchText] = useSearch();
|
const [searchParams] = useSearchParams();
|
||||||
const { page, perPage } = usePagination();
|
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
||||||
|
@ -61,7 +60,12 @@ const GlobalSchemaSelector: React.FC = () => {
|
||||||
});
|
});
|
||||||
setCurrentCompatibilityLevel(nextLevel);
|
setCurrentCompatibilityLevel(nextLevel);
|
||||||
dispatch(
|
dispatch(
|
||||||
fetchSchemas({ clusterName, page, perPage, search: searchText })
|
fetchSchemas({
|
||||||
|
clusterName,
|
||||||
|
page: Number(searchParams.get('page') || 1),
|
||||||
|
perPage: Number(searchParams.get('perPage') || PER_PAGE),
|
||||||
|
search: searchParams.get('q') || '',
|
||||||
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showServerError(e as Response);
|
showServerError(e as Response);
|
||||||
|
|
|
@ -82,7 +82,7 @@ describe('GlobalSchemaSelector', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const getSchemasMock = fetchMock.getOnce(
|
const getSchemasMock = fetchMock.getOnce(
|
||||||
`api/clusters/${clusterName}/schemas`,
|
`api/clusters/${clusterName}/schemas?page=1&perPage=25`,
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterNameRoute, clusterSchemaNewRelativePath } from 'lib/paths';
|
import {
|
||||||
|
ClusterNameRoute,
|
||||||
|
clusterSchemaNewRelativePath,
|
||||||
|
clusterSchemaPath,
|
||||||
|
} from 'lib/paths';
|
||||||
import ClusterContext from 'components/contexts/ClusterContext';
|
import ClusterContext from 'components/contexts/ClusterContext';
|
||||||
import * as C from 'components/common/table/Table/Table.styled';
|
|
||||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
|
||||||
import { Button } from 'components/common/Button/Button';
|
import { Button } from 'components/common/Button/Button';
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||||
|
@ -13,36 +15,53 @@ import {
|
||||||
getAreSchemasFulfilled,
|
getAreSchemasFulfilled,
|
||||||
SCHEMAS_FETCH_ACTION,
|
SCHEMAS_FETCH_ACTION,
|
||||||
} from 'redux/reducers/schemas/schemasSlice';
|
} from 'redux/reducers/schemas/schemasSlice';
|
||||||
import usePagination from 'lib/hooks/usePagination';
|
|
||||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
import Pagination from 'components/common/Pagination/Pagination';
|
|
||||||
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||||
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
||||||
import Search from 'components/common/Search/Search';
|
import Search from 'components/common/Search/Search';
|
||||||
import useSearch from 'lib/hooks/useSearch';
|
import useSearch from 'lib/hooks/useSearch';
|
||||||
import PlusIcon from 'components/common/Icons/PlusIcon';
|
import PlusIcon from 'components/common/Icons/PlusIcon';
|
||||||
|
import Table, { LinkCell } from 'components/common/NewTable';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { SchemaSubject } from 'generated-sources';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { PER_PAGE } from 'lib/constants';
|
||||||
|
|
||||||
import ListItem from './ListItem';
|
|
||||||
import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
|
import GlobalSchemaSelector from './GlobalSchemaSelector/GlobalSchemaSelector';
|
||||||
|
|
||||||
const List: React.FC = () => {
|
const List: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isReadOnly } = React.useContext(ClusterContext);
|
const { isReadOnly } = React.useContext(ClusterContext);
|
||||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||||
|
const navigate = useNavigate();
|
||||||
const schemas = useAppSelector(selectAllSchemas);
|
const schemas = useAppSelector(selectAllSchemas);
|
||||||
const isFetched = useAppSelector(getAreSchemasFulfilled);
|
const isFetched = useAppSelector(getAreSchemasFulfilled);
|
||||||
const totalPages = useAppSelector((state) => state.schemas.totalPages);
|
const totalPages = useAppSelector((state) => state.schemas.totalPages);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [searchText, handleSearchText] = useSearch();
|
const [searchText, handleSearchText] = useSearch();
|
||||||
const { page, perPage } = usePagination();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch(fetchSchemas({ clusterName, page, perPage, search: searchText }));
|
dispatch(
|
||||||
|
fetchSchemas({
|
||||||
|
clusterName,
|
||||||
|
page: Number(searchParams.get('page') || 1),
|
||||||
|
perPage: Number(searchParams.get('perPage') || PER_PAGE),
|
||||||
|
search: searchParams.get('q') || '',
|
||||||
|
})
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(resetLoaderById(SCHEMAS_FETCH_ACTION));
|
dispatch(resetLoaderById(SCHEMAS_FETCH_ACTION));
|
||||||
};
|
};
|
||||||
}, [clusterName, dispatch, page, perPage, searchText]);
|
}, [clusterName, dispatch, searchParams]);
|
||||||
|
|
||||||
|
const columns = React.useMemo<ColumnDef<SchemaSubject>[]>(
|
||||||
|
() => [
|
||||||
|
{ header: 'Subject', accessorKey: 'subject', cell: LinkCell },
|
||||||
|
{ header: 'Version', accessorKey: 'version' },
|
||||||
|
{ header: 'Compatibility', accessorKey: 'compatibilityLevel' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -68,31 +87,16 @@ const List: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</ControlPanelWrapper>
|
</ControlPanelWrapper>
|
||||||
{isFetched ? (
|
{isFetched ? (
|
||||||
<>
|
<Table
|
||||||
<C.Table isFullwidth>
|
columns={columns}
|
||||||
<thead>
|
data={schemas}
|
||||||
<tr>
|
pageCount={totalPages}
|
||||||
<TableHeaderCell title="Schema Name" />
|
emptyMessage="No schemas found"
|
||||||
<TableHeaderCell title="Version" />
|
onRowClick={(row) =>
|
||||||
<TableHeaderCell title="Compatibility" />
|
navigate(clusterSchemaPath(clusterName, row.original.subject))
|
||||||
</tr>
|
}
|
||||||
</thead>
|
serverSideProcessing
|
||||||
<tbody>
|
/>
|
||||||
{schemas.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={10}>No schemas found</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{schemas.map((subject) => (
|
|
||||||
<ListItem
|
|
||||||
key={[subject.id, subject.subject].join('-')}
|
|
||||||
subject={subject}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</C.Table>
|
|
||||||
<Pagination totalPages={totalPages} />
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<PageLoader />
|
<PageLoader />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { SchemaSubject } from 'generated-sources';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import * as S from 'components/common/table/Table/TableKeyLink.styled';
|
|
||||||
|
|
||||||
export interface ListItemProps {
|
|
||||||
subject: SchemaSubject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListItem: React.FC<ListItemProps> = ({
|
|
||||||
subject: { subject, version, compatibilityLevel },
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<S.TableKeyLink>
|
|
||||||
<NavLink to={subject} role="link">
|
|
||||||
{subject}
|
|
||||||
</NavLink>
|
|
||||||
</S.TableKeyLink>
|
|
||||||
<td role="cell">{version}</td>
|
|
||||||
<td role="cell">{compatibilityLevel}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ListItem;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import List from 'components/Schemas/List/List';
|
import List from 'components/Schemas/List/List';
|
||||||
import { render, WithRoute } from 'lib/testHelpers';
|
import { render, WithRoute } from 'lib/testHelpers';
|
||||||
import { clusterSchemasPath } from 'lib/paths';
|
import { clusterSchemaPath, clusterSchemasPath } from 'lib/paths';
|
||||||
import { act, screen } from '@testing-library/react';
|
import { act, screen } from '@testing-library/react';
|
||||||
import {
|
import {
|
||||||
schemasFulfilledState,
|
schemasFulfilledState,
|
||||||
|
@ -15,12 +15,20 @@ import ClusterContext, {
|
||||||
} from 'components/contexts/ClusterContext';
|
} from 'components/contexts/ClusterContext';
|
||||||
import { RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { schemasPayload, schemasEmptyPayload } from './fixtures';
|
import { schemasPayload, schemasEmptyPayload } from './fixtures';
|
||||||
|
|
||||||
|
const mockedUsedNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: () => mockedUsedNavigate,
|
||||||
|
}));
|
||||||
|
|
||||||
const clusterName = 'testClusterName';
|
const clusterName = 'testClusterName';
|
||||||
const schemasAPIUrl = `/api/clusters/${clusterName}/schemas`;
|
const schemasAPIUrl = `/api/clusters/${clusterName}/schemas?page=1&perPage=25`;
|
||||||
const schemasAPICompabilityUrl = `${schemasAPIUrl}/compatibility`;
|
const schemasAPICompabilityUrl = `/api/clusters/${clusterName}/schemas/compatibility`;
|
||||||
const renderComponent = (
|
const renderComponent = (
|
||||||
initialState: RootState['schemas'] = schemasInitialState,
|
initialState: RootState['schemas'] = schemasInitialState,
|
||||||
context: ContextProps = contextInitialValue
|
context: ContextProps = contextInitialValue
|
||||||
|
@ -101,6 +109,17 @@ describe('List', () => {
|
||||||
expect(screen.getByText(schemaVersion1.subject)).toBeInTheDocument();
|
expect(screen.getByText(schemaVersion1.subject)).toBeInTheDocument();
|
||||||
expect(screen.getByText(schemaVersion2.subject)).toBeInTheDocument();
|
expect(screen.getByText(schemaVersion2.subject)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('handles onRowClick', () => {
|
||||||
|
const { subject, version, compatibilityLevel } = schemaVersion2;
|
||||||
|
const row = screen.getByRole('row', {
|
||||||
|
name: `${subject} ${version} ${compatibilityLevel}`,
|
||||||
|
});
|
||||||
|
expect(row).toBeInTheDocument();
|
||||||
|
userEvent.click(row);
|
||||||
|
expect(mockedUsedNavigate).toHaveBeenCalledWith(
|
||||||
|
clusterSchemaPath(clusterName, subject)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('responded with readonly cluster schemas', () => {
|
describe('responded with readonly cluster schemas', () => {
|
||||||
|
@ -109,6 +128,7 @@ describe('List', () => {
|
||||||
schemasAPIUrl,
|
schemasAPIUrl,
|
||||||
schemasPayload
|
schemasPayload
|
||||||
);
|
);
|
||||||
|
fetchMock.getOnce(schemasAPICompabilityUrl, 200);
|
||||||
await act(() => {
|
await act(() => {
|
||||||
renderComponent(schemasFulfilledState, {
|
renderComponent(schemasFulfilledState, {
|
||||||
...contextInitialValue,
|
...contextInitialValue,
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ListItem, { ListItemProps } from 'components/Schemas/List/ListItem';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
|
|
||||||
import { schemas } from './fixtures';
|
|
||||||
|
|
||||||
describe('ListItem', () => {
|
|
||||||
const setupComponent = (props: ListItemProps = { subject: schemas[0] }) =>
|
|
||||||
render(
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<ListItem {...props} />
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
|
|
||||||
it('renders schemas', () => {
|
|
||||||
setupComponent();
|
|
||||||
expect(screen.getAllByRole('link').length).toEqual(1);
|
|
||||||
expect(screen.getAllByRole('cell').length).toEqual(3);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
schemaVersion2,
|
schemaVersion2,
|
||||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||||
|
|
||||||
export const schemas = [schemaVersion1, schemaVersion2];
|
const schemas = [schemaVersion1, schemaVersion2];
|
||||||
|
|
||||||
export const schemasPayload = {
|
export const schemasPayload = {
|
||||||
pageCount: 1,
|
pageCount: 1,
|
||||||
|
|
|
@ -44,7 +44,7 @@ const TopicConsumerGroups: React.FC = () => {
|
||||||
<td>{consumer.coordinator?.id}</td>
|
<td>{consumer.coordinator?.id}</td>
|
||||||
<td>
|
<td>
|
||||||
{consumer.state && (
|
{consumer.state && (
|
||||||
<Tag color={getTagColor(consumer)}>{`${consumer.state
|
<Tag color={getTagColor(consumer.state)}>{`${consumer.state
|
||||||
.charAt(0)
|
.charAt(0)
|
||||||
.toUpperCase()}${consumer.state
|
.toUpperCase()}${consumer.state
|
||||||
.slice(1)
|
.slice(1)
|
||||||
|
|
|
@ -69,22 +69,6 @@ export const MetadataMeta = styled.p`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PaginationButton = styled.button`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 12px;
|
|
||||||
height: 32px;
|
|
||||||
border: 1px solid ${({ theme }) => theme.pagination.borderColor.normal};
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: ${({ theme }) => theme.pagination.color.normal};
|
|
||||||
background: none;
|
|
||||||
font-family: Inter;
|
|
||||||
margin-right: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Tab = styled.button<{ $active?: boolean }>(
|
export const Tab = styled.button<{ $active?: boolean }>(
|
||||||
({ theme, $active }) => css`
|
({ theme, $active }) => css`
|
||||||
background-color: ${theme.secondaryTab.backgroundColor[
|
background-color: ${theme.secondaryTab.backgroundColor[
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import * as S from 'components/Topics/Topic/Details/Messages/MessageContent/MessageContent.styled';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
import theme from 'theme/theme';
|
|
||||||
|
|
||||||
describe('MessageContent Styled Components', () => {
|
|
||||||
describe('PaginationComponent', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
render(<S.PaginationButton />);
|
|
||||||
});
|
|
||||||
it('should test the Pagination Button theme related Props', () => {
|
|
||||||
const button = screen.getByRole('button');
|
|
||||||
expect(button).toHaveStyle(`color: ${theme.pagination.color.normal}`);
|
|
||||||
expect(button).toHaveStyle(
|
|
||||||
`border: 1px solid ${theme.pagination.borderColor.normal}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -3,8 +3,7 @@ import React from 'react';
|
||||||
|
|
||||||
import * as S from './Table.styled';
|
import * as S from './Table.styled';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const ExpanderCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (
|
||||||
const ExpanderCell: React.FC<CellContext<any, unknown>> = ({ row }) => (
|
|
||||||
<S.ExpaderButton
|
<S.ExpaderButton
|
||||||
width="16"
|
width="16"
|
||||||
height="20"
|
height="20"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const LinkCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => {
|
||||||
|
const value = `${getValue<string | number>()}`;
|
||||||
|
const handleClick: React.MouseEventHandler = (e) => e.stopPropagation();
|
||||||
|
return (
|
||||||
|
<NavLink to={value} title={value} onClick={handleClick}>
|
||||||
|
{value}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkCell;
|
|
@ -2,8 +2,7 @@ import { CellContext } from '@tanstack/react-table';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const SelectRowCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (
|
||||||
const SelectRowCell: React.FC<CellContext<any, unknown>> = ({ row }) => (
|
|
||||||
<IndeterminateCheckbox
|
<IndeterminateCheckbox
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
disabled={!row.getCanSelect()}
|
disabled={!row.getCanSelect()}
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { HeaderContext } from '@tanstack/react-table';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const SelectRowHeader: React.FC<HeaderContext<unknown, unknown>> = ({
|
||||||
const SelectRowHeader: React.FC<HeaderContext<any, unknown>> = ({ table }) => (
|
table,
|
||||||
|
}) => (
|
||||||
<IndeterminateCheckbox
|
<IndeterminateCheckbox
|
||||||
checked={table.getIsAllPageRowsSelected()}
|
checked={table.getIsAllPageRowsSelected()}
|
||||||
indeterminate={table.getIsSomePageRowsSelected()}
|
indeterminate={table.getIsSomePageRowsSelected()}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { CellContext } from '@tanstack/react-table';
|
||||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const SizeCell: React.FC<CellContext<any, any>> = ({ getValue }) => (
|
const SizeCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
|
||||||
<BytesFormatted value={getValue()} />
|
<BytesFormatted value={getValue<string | number>()} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export default SizeCell;
|
export default SizeCell;
|
||||||
|
|
|
@ -99,13 +99,13 @@ export const Th = styled.th<ThProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
expandable?: boolean;
|
clickable?: boolean;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Row = styled.tr<RowProps>(
|
export const Row = styled.tr<RowProps>(
|
||||||
({ theme: { table }, expanded, expandable }) => `
|
({ theme: { table }, expanded, clickable }) => `
|
||||||
cursor: ${expandable ? 'pointer' : 'default'};
|
cursor: ${clickable ? 'pointer' : 'default'};
|
||||||
background-color: ${table.tr.backgroundColor[expanded ? 'hover' : 'normal']};
|
background-color: ${table.tr.backgroundColor[expanded ? 'hover' : 'normal']};
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${table.tr.backgroundColor.hover};
|
background-color: ${table.tr.backgroundColor.hover};
|
||||||
|
|
|
@ -28,13 +28,26 @@ export interface TableProps<TData> {
|
||||||
data: TData[];
|
data: TData[];
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
columns: ColumnDef<TData>[];
|
columns: ColumnDef<TData>[];
|
||||||
renderSubComponent?: React.FC<{ row: Row<TData> }>;
|
|
||||||
getRowCanExpand?: (row: Row<TData>) => boolean;
|
// Server-side processing: sorting, pagination
|
||||||
serverSideProcessing?: boolean;
|
serverSideProcessing?: boolean;
|
||||||
enableSorting?: boolean;
|
|
||||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
// Expandeble rows
|
||||||
batchActionsBar?: React.FC<{ rows: Row<TData>[]; resetRowSelection(): void }>;
|
getRowCanExpand?: (row: Row<TData>) => boolean; // Enables the ability to expand row. Use `() => true` when want to expand all rows.
|
||||||
|
renderSubComponent?: React.FC<{ row: Row<TData> }>; // Component to render expanded row.
|
||||||
|
|
||||||
|
// Selectable rows
|
||||||
|
enableRowSelection?: boolean | ((row: Row<TData>) => boolean); // Enables the ability to select row.
|
||||||
|
batchActionsBar?: React.FC<{ rows: Row<TData>[]; resetRowSelection(): void }>; // Component to render batch actions bar for slected rows
|
||||||
|
|
||||||
|
// Sorting.
|
||||||
|
enableSorting?: boolean; // Enables sorting for table.
|
||||||
|
|
||||||
|
// Placeholder for empty table
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
|
|
||||||
|
// Handles row click. Can not be combined with `enableRowSelection` && expandable rows.
|
||||||
|
onRowClick?: (row: Row<TData>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdaterFn<T> = (previousState: T) => T;
|
type UpdaterFn<T> = (previousState: T) => T;
|
||||||
|
@ -72,6 +85,7 @@ const getSortingFromSearchParams = (searchParams: URLSearchParams) => {
|
||||||
* `enableSorting = false` to the column def.
|
* `enableSorting = false` to the column def.
|
||||||
* - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection`
|
* - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection`
|
||||||
* search param to set default sortings.
|
* search param to set default sortings.
|
||||||
|
* - use `id` property of the column def to set the sortBy for server side sorting.
|
||||||
*
|
*
|
||||||
* 2. Pagination
|
* 2. Pagination
|
||||||
* - pagination enabled by default.
|
* - pagination enabled by default.
|
||||||
|
@ -107,6 +121,7 @@ const Table: React.FC<TableProps<any>> = ({
|
||||||
enableRowSelection = false,
|
enableRowSelection = false,
|
||||||
batchActionsBar,
|
batchActionsBar,
|
||||||
emptyMessage,
|
emptyMessage,
|
||||||
|
onRowClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
@ -157,6 +172,24 @@ const Table: React.FC<TableProps<any>> = ({
|
||||||
|
|
||||||
const Bar = batchActionsBar;
|
const Bar = batchActionsBar;
|
||||||
|
|
||||||
|
const handleRowClick = (row: Row<typeof data>) => (e: React.MouseEvent) => {
|
||||||
|
// If row selection is enabled do not handle row click.
|
||||||
|
if (enableRowSelection) return undefined;
|
||||||
|
|
||||||
|
// If row can be expanded do not handle row click.
|
||||||
|
if (row.getCanExpand()) {
|
||||||
|
e.stopPropagation();
|
||||||
|
return row.toggleExpanded();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onRowClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
return onRowClick(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{table.getSelectedRowModel().flatRows.length > 0 && Bar && (
|
{table.getSelectedRowModel().flatRows.length > 0 && Bar && (
|
||||||
|
@ -205,9 +238,12 @@ const Table: React.FC<TableProps<any>> = ({
|
||||||
{table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows.map((row) => (
|
||||||
<React.Fragment key={row.id}>
|
<React.Fragment key={row.id}>
|
||||||
<S.Row
|
<S.Row
|
||||||
expandable={row.getCanExpand()}
|
|
||||||
expanded={row.getIsExpanded()}
|
expanded={row.getIsExpanded()}
|
||||||
onClick={() => row.getCanExpand() && row.toggleExpanded()}
|
onClick={handleRowClick(row)}
|
||||||
|
clickable={
|
||||||
|
!enableRowSelection &&
|
||||||
|
(row.getCanExpand() || onRowClick !== undefined)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{!!enableRowSelection && (
|
{!!enableRowSelection && (
|
||||||
<td key={`${row.id}-select`}>
|
<td key={`${row.id}-select`}>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import React from 'react';
|
||||||
|
import getTagColor from 'components/common/Tag/getTagColor';
|
||||||
|
import { Tag } from 'components/common/Tag/Tag.styled';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const TagCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => {
|
||||||
|
const value = getValue<string>();
|
||||||
|
return <Tag color={getTagColor(value)}>{value}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagCell;
|
|
@ -5,8 +5,8 @@ import React from 'react';
|
||||||
import * as S from './Table.styled';
|
import * as S from './Table.styled';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const TimestampCell: React.FC<CellContext<any, any>> = ({ getValue }) => (
|
const TimestampCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
|
||||||
<S.Nowrap>{formatTimestamp(getValue())}</S.Nowrap>
|
<S.Nowrap>{formatTimestamp(getValue<string | number>())}</S.Nowrap>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default TimestampCell;
|
export default TimestampCell;
|
||||||
|
|
|
@ -4,20 +4,54 @@ import Table, {
|
||||||
TableProps,
|
TableProps,
|
||||||
TimestampCell,
|
TimestampCell,
|
||||||
SizeCell,
|
SizeCell,
|
||||||
|
LinkCell,
|
||||||
|
TagCell,
|
||||||
} from 'components/common/NewTable';
|
} from 'components/common/NewTable';
|
||||||
import { screen, waitFor } from '@testing-library/dom';
|
import { screen, waitFor } from '@testing-library/dom';
|
||||||
import { ColumnDef, Row } from '@tanstack/react-table';
|
import { ColumnDef, Row } from '@tanstack/react-table';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { formatTimestamp } from 'lib/dateTimeHelpers';
|
import { formatTimestamp } from 'lib/dateTimeHelpers';
|
||||||
import { act } from '@testing-library/react';
|
import { act } from '@testing-library/react';
|
||||||
|
import { ConnectorState, ConsumerGroupState } from 'generated-sources';
|
||||||
|
|
||||||
|
const mockedUsedNavigate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useNavigate: () => mockedUsedNavigate,
|
||||||
|
}));
|
||||||
|
|
||||||
type Datum = typeof data[0];
|
type Datum = typeof data[0];
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ timestamp: 1660034383725, text: 'lorem', selectable: false, size: 1234 },
|
{
|
||||||
{ timestamp: 1660034399999, text: 'ipsum', selectable: true, size: 3 },
|
timestamp: 1660034383725,
|
||||||
{ timestamp: 1660034399922, text: 'dolor', selectable: true, size: 50000 },
|
text: 'lorem',
|
||||||
{ timestamp: 1660034199922, text: 'sit', selectable: false, size: 1_312_323 },
|
selectable: false,
|
||||||
|
size: 1234,
|
||||||
|
tag: ConnectorState.RUNNING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: 1660034399999,
|
||||||
|
text: 'ipsum',
|
||||||
|
selectable: true,
|
||||||
|
size: 3,
|
||||||
|
tag: ConnectorState.FAILED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: 1660034399922,
|
||||||
|
text: 'dolor',
|
||||||
|
selectable: true,
|
||||||
|
size: 50000,
|
||||||
|
tag: ConsumerGroupState.EMPTY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: 1660034199922,
|
||||||
|
text: 'sit',
|
||||||
|
selectable: false,
|
||||||
|
size: 1_312_323,
|
||||||
|
tag: 'some_string',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const columns: ColumnDef<Datum>[] = [
|
const columns: ColumnDef<Datum>[] = [
|
||||||
|
@ -29,12 +63,18 @@ const columns: ColumnDef<Datum>[] = [
|
||||||
{
|
{
|
||||||
header: 'Text',
|
header: 'Text',
|
||||||
accessorKey: 'text',
|
accessorKey: 'text',
|
||||||
|
cell: LinkCell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Size',
|
header: 'Size',
|
||||||
accessorKey: 'size',
|
accessorKey: 'size',
|
||||||
cell: SizeCell,
|
cell: SizeCell,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Tag',
|
||||||
|
accessorKey: 'tag',
|
||||||
|
cell: TagCell,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ExpandedRow: React.FC = () => <div>I am expanded row</div>;
|
const ExpandedRow: React.FC = () => <div>I am expanded row</div>;
|
||||||
|
@ -45,7 +85,7 @@ interface Props extends TableProps<Datum> {
|
||||||
|
|
||||||
const renderComponent = (props: Partial<Props> = {}) => {
|
const renderComponent = (props: Partial<Props> = {}) => {
|
||||||
render(
|
render(
|
||||||
<WithRoute path="/">
|
<WithRoute path="/*">
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -94,6 +134,21 @@ describe('Table', () => {
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('LinkCell', () => {
|
||||||
|
it('renders link', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByRole('link', { name: 'lorem' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('link click stops propagation', () => {
|
||||||
|
const onRowClick = jest.fn();
|
||||||
|
renderComponent({ onRowClick });
|
||||||
|
const link = screen.getByRole('link', { name: 'lorem' });
|
||||||
|
userEvent.click(link);
|
||||||
|
expect(onRowClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('ExpanderCell', () => {
|
describe('ExpanderCell', () => {
|
||||||
it('renders button', () => {
|
it('renders button', () => {
|
||||||
renderComponent({ getRowCanExpand: () => true });
|
renderComponent({ getRowCanExpand: () => true });
|
||||||
|
@ -116,6 +171,14 @@ describe('Table', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders TagCell', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByText(data[0].tag)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(data[1].tag)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(data[2].tag)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(data[3].tag)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
describe('Pagination', () => {
|
describe('Pagination', () => {
|
||||||
it('does not render page buttons', () => {
|
it('does not render page buttons', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
@ -240,4 +303,34 @@ describe('Table', () => {
|
||||||
expect(screen.getByText('I am Action Bar')).toBeInTheDocument();
|
expect(screen.getByText('I am Action Bar')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('Clickable Row', () => {
|
||||||
|
const onRowClick = jest.fn();
|
||||||
|
it('handles onRowClick', () => {
|
||||||
|
renderComponent({ onRowClick });
|
||||||
|
const rows = screen.getAllByRole('row');
|
||||||
|
expect(rows.length).toEqual(data.length + 1);
|
||||||
|
userEvent.click(rows[1]);
|
||||||
|
expect(onRowClick).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
it('does nothing unless onRowClick is provided', () => {
|
||||||
|
renderComponent();
|
||||||
|
const rows = screen.getAllByRole('row');
|
||||||
|
expect(rows.length).toEqual(data.length + 1);
|
||||||
|
userEvent.click(rows[1]);
|
||||||
|
});
|
||||||
|
it('does not handle onRowClick if enableRowSelection', () => {
|
||||||
|
renderComponent({ onRowClick, enableRowSelection: true });
|
||||||
|
const rows = screen.getAllByRole('row');
|
||||||
|
expect(rows.length).toEqual(data.length + 1);
|
||||||
|
userEvent.click(rows[1]);
|
||||||
|
expect(onRowClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('does not handle onRowClick if expandable rows', () => {
|
||||||
|
renderComponent({ onRowClick, getRowCanExpand: () => true });
|
||||||
|
const rows = screen.getAllByRole('row');
|
||||||
|
expect(rows.length).toEqual(data.length + 1);
|
||||||
|
userEvent.click(rows[1]);
|
||||||
|
expect(onRowClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import Table, { TableProps } from './Table';
|
import Table, { TableProps } from './Table';
|
||||||
import TimestampCell from './TimestampCell';
|
import TimestampCell from './TimestampCell';
|
||||||
import SizeCell from './SizeCell';
|
import SizeCell from './SizeCell';
|
||||||
|
import LinkCell from './LinkCell';
|
||||||
|
import TagCell from './TagCell';
|
||||||
|
|
||||||
export type { TableProps };
|
export type { TableProps };
|
||||||
|
|
||||||
export { TimestampCell, SizeCell };
|
export { TimestampCell, SizeCell, LinkCell, TagCell };
|
||||||
|
|
||||||
export default Table;
|
export default Table;
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { PaginationLink } from './Pagination.styled';
|
|
||||||
|
|
||||||
export interface PageControlProps {
|
|
||||||
current: boolean;
|
|
||||||
url: string;
|
|
||||||
page: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageControl: React.FC<PageControlProps> = ({ current, url, page }) => {
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<PaginationLink
|
|
||||||
to={url}
|
|
||||||
aria-label={`Goto page ${page}`}
|
|
||||||
$isCurrent={current}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageControl;
|
|
|
@ -1,87 +0,0 @@
|
||||||
import styled from 'styled-components';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import theme from 'theme/theme';
|
|
||||||
|
|
||||||
export const Wrapper = styled.nav`
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding: 25px 16px;
|
|
||||||
gap: 15px;
|
|
||||||
|
|
||||||
& > ul {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
|
|
||||||
& > li:not(:last-child) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const PaginationLink = styled(Link)<{ $isCurrent: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
height: 32px;
|
|
||||||
width: 33px;
|
|
||||||
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid
|
|
||||||
${({ $isCurrent }) =>
|
|
||||||
$isCurrent
|
|
||||||
? theme.pagination.currentPage
|
|
||||||
: theme.pagination.borderColor.normal};
|
|
||||||
background-color: ${({ $isCurrent }) =>
|
|
||||||
$isCurrent
|
|
||||||
? theme.pagination.currentPage
|
|
||||||
: theme.pagination.backgroundColor};
|
|
||||||
color: ${theme.pagination.color.normal};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid
|
|
||||||
${({ $isCurrent }) =>
|
|
||||||
$isCurrent
|
|
||||||
? theme.pagination.currentPage
|
|
||||||
: theme.pagination.borderColor.hover};
|
|
||||||
color: ${(props) => props.theme.pagination.color.hover};
|
|
||||||
cursor: ${({ $isCurrent }) => ($isCurrent ? 'default' : 'pointer')};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const PaginationButton = styled(Link)`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 12px;
|
|
||||||
height: 32px;
|
|
||||||
border: 1px solid ${theme.pagination.borderColor.normal};
|
|
||||||
border-radius: 4px;
|
|
||||||
color: ${theme.pagination.color.normal};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid ${theme.pagination.borderColor.hover};
|
|
||||||
color: ${theme.pagination.color.hover};
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border: 1px solid ${theme.pagination.borderColor.active};
|
|
||||||
color: ${theme.pagination.color.active};
|
|
||||||
}
|
|
||||||
&:disabled {
|
|
||||||
border: 1px solid ${theme.pagination.borderColor.disabled};
|
|
||||||
color: ${theme.pagination.color.disabled};
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DisabledButton = styled.button`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 12px;
|
|
||||||
height: 32px;
|
|
||||||
border: 1px solid ${theme.pagination.borderColor.disabled};
|
|
||||||
background-color: ${theme.pagination.backgroundColor};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${theme.pagination.color.disabled};
|
|
||||||
`;
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { PER_PAGE } from 'lib/constants';
|
|
||||||
import usePagination from 'lib/hooks/usePagination';
|
|
||||||
import range from 'lodash/range';
|
|
||||||
import React from 'react';
|
|
||||||
import PageControl from 'components/common/Pagination/PageControl';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import * as S from './Pagination.styled';
|
|
||||||
|
|
||||||
export interface PaginationProps {
|
|
||||||
totalPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NEIGHBOURS = 2;
|
|
||||||
|
|
||||||
const Pagination: React.FC<PaginationProps> = ({ totalPages }) => {
|
|
||||||
const { page, perPage, pathname } = usePagination();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const currentPage = page || 1;
|
|
||||||
const currentPerPage = perPage || PER_PAGE;
|
|
||||||
|
|
||||||
const getPath = (newPage: number) => {
|
|
||||||
searchParams.set('page', Math.max(newPage, 1).toString());
|
|
||||||
searchParams.set('perPage', currentPerPage.toString());
|
|
||||||
return `${pathname}?${searchParams.toString()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pages = React.useMemo(() => {
|
|
||||||
// Total visible numbers: neighbours, current, first & last
|
|
||||||
const totalNumbers = NEIGHBOURS * 2 + 3;
|
|
||||||
// totalNumbers + `...`*2
|
|
||||||
const totalBlocks = totalNumbers + 2;
|
|
||||||
|
|
||||||
if (totalPages <= totalBlocks) {
|
|
||||||
return range(1, totalPages + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPage = Math.max(
|
|
||||||
2,
|
|
||||||
Math.min(currentPage - NEIGHBOURS, totalPages)
|
|
||||||
);
|
|
||||||
const endPage = Math.min(
|
|
||||||
totalPages - 1,
|
|
||||||
Math.min(currentPage + NEIGHBOURS, totalPages)
|
|
||||||
);
|
|
||||||
|
|
||||||
let p = range(startPage, endPage + 1);
|
|
||||||
|
|
||||||
const hasLeftSpill = startPage > 2;
|
|
||||||
const hasRightSpill = totalPages - endPage > 1;
|
|
||||||
const spillOffset = totalNumbers - (p.length + 1);
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case hasLeftSpill && !hasRightSpill: {
|
|
||||||
p = [...range(startPage - spillOffset - 1, startPage - 1), ...p];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case !hasLeftSpill && hasRightSpill: {
|
|
||||||
p = [...p, ...range(endPage + 1, endPage + spillOffset + 1)];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}, [currentPage, totalPages]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<S.Wrapper role="navigation" aria-label="pagination">
|
|
||||||
{currentPage > 1 ? (
|
|
||||||
<S.PaginationButton to={getPath(currentPage - 1)}>
|
|
||||||
Previous
|
|
||||||
</S.PaginationButton>
|
|
||||||
) : (
|
|
||||||
<S.DisabledButton disabled>Previous</S.DisabledButton>
|
|
||||||
)}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<ul>
|
|
||||||
{!pages.includes(1) && (
|
|
||||||
<PageControl
|
|
||||||
page={1}
|
|
||||||
current={currentPage === 1}
|
|
||||||
url={getPath(1)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!pages.includes(2) && (
|
|
||||||
<li>
|
|
||||||
<span>…</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{pages.map((p) => (
|
|
||||||
<PageControl
|
|
||||||
key={`page-${p}`}
|
|
||||||
page={p}
|
|
||||||
current={p === currentPage}
|
|
||||||
url={getPath(p)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{!pages.includes(totalPages - 1) && (
|
|
||||||
<li>
|
|
||||||
<span>…</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{!pages.includes(totalPages) && (
|
|
||||||
<PageControl
|
|
||||||
page={totalPages}
|
|
||||||
current={currentPage === totalPages}
|
|
||||||
url={getPath(totalPages)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{currentPage < totalPages ? (
|
|
||||||
<S.PaginationButton to={getPath(currentPage + 1)}>
|
|
||||||
Next
|
|
||||||
</S.PaginationButton>
|
|
||||||
) : (
|
|
||||||
<S.DisabledButton disabled>Next</S.DisabledButton>
|
|
||||||
)}
|
|
||||||
</S.Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Pagination;
|
|
|
@ -1,35 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PageControl, {
|
|
||||||
PageControlProps,
|
|
||||||
} from 'components/common/Pagination/PageControl';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
import theme from 'theme/theme';
|
|
||||||
|
|
||||||
const page = 138;
|
|
||||||
|
|
||||||
describe('PageControl', () => {
|
|
||||||
const setupComponent = (props: Partial<PageControlProps> = {}) =>
|
|
||||||
render(<PageControl url="/test" page={page} current {...props} />);
|
|
||||||
|
|
||||||
const getButton = () => screen.getByRole('button');
|
|
||||||
|
|
||||||
it('renders current page', () => {
|
|
||||||
setupComponent({ current: true });
|
|
||||||
expect(getButton()).toHaveStyle(
|
|
||||||
`background-color: ${theme.pagination.currentPage}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders non-current page', () => {
|
|
||||||
setupComponent({ current: false });
|
|
||||||
expect(getButton()).toHaveStyle(
|
|
||||||
`background-color: ${theme.pagination.backgroundColor}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders page number', () => {
|
|
||||||
setupComponent({ current: false });
|
|
||||||
expect(getButton()).toHaveTextContent(String(page));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,91 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import Pagination, {
|
|
||||||
PaginationProps,
|
|
||||||
} from 'components/common/Pagination/Pagination';
|
|
||||||
import theme from 'theme/theme';
|
|
||||||
import { render } from 'lib/testHelpers';
|
|
||||||
import { screen } from '@testing-library/react';
|
|
||||||
|
|
||||||
describe('Pagination', () => {
|
|
||||||
const setupComponent = (
|
|
||||||
search = '',
|
|
||||||
props: Partial<PaginationProps> = {}
|
|
||||||
) => {
|
|
||||||
const defaultPath = '/my/test/path/23';
|
|
||||||
const pathName = search ? `${defaultPath}${search}` : defaultPath;
|
|
||||||
return render(<Pagination totalPages={11} {...props} />, {
|
|
||||||
initialEntries: [pathName],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('next & prev buttons', () => {
|
|
||||||
it('renders disable prev button and enabled next link', () => {
|
|
||||||
setupComponent('?page=1');
|
|
||||||
expect(screen.getByText('Previous')).toBeDisabled();
|
|
||||||
expect(screen.getByText('Next')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders disable next button and enabled prev link', () => {
|
|
||||||
setupComponent('?page=11');
|
|
||||||
expect(screen.getByText('Previous')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Next')).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders next & prev links with correct path', () => {
|
|
||||||
setupComponent('?page=5&perPage=20');
|
|
||||||
expect(screen.getByText('Previous')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Next')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Previous')).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
'/my/test/path/23?page=4&perPage=20'
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Next')).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
'/my/test/path/23?page=6&perPage=20'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('spread', () => {
|
|
||||||
it('renders 1 spread element after first page control', () => {
|
|
||||||
setupComponent('?page=8');
|
|
||||||
expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders 1 spread element before last spread control', () => {
|
|
||||||
setupComponent('?page=2');
|
|
||||||
expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders 2 spread elements', () => {
|
|
||||||
setupComponent('?page=6');
|
|
||||||
expect(screen.getAllByText('…').length).toEqual(2);
|
|
||||||
expect(screen.getAllByRole('listitem')[0]).toHaveTextContent('1');
|
|
||||||
expect(screen.getAllByRole('listitem')[1]).toHaveTextContent('…');
|
|
||||||
expect(screen.getAllByRole('listitem')[7]).toHaveTextContent('…');
|
|
||||||
expect(screen.getAllByRole('listitem')[8]).toHaveTextContent('11');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders 0 spread elements', () => {
|
|
||||||
setupComponent('?page=2', { totalPages: 8 });
|
|
||||||
expect(screen.queryAllByText('…').length).toEqual(0);
|
|
||||||
expect(screen.getAllByRole('listitem').length).toEqual(8);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('current page', () => {
|
|
||||||
it('check if it sets page 8 as current when page param is set', () => {
|
|
||||||
setupComponent('?page=8');
|
|
||||||
expect(screen.getByText('8')).toHaveStyle(
|
|
||||||
`background-color: ${theme.pagination.currentPage}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('check if it sets first page as current when page param not set', () => {
|
|
||||||
setupComponent('', { totalPages: 8 });
|
|
||||||
expect(screen.getByText('1')).toHaveStyle(
|
|
||||||
`background-color: ${theme.pagination.currentPage}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,134 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import Pagination from 'components/common/Pagination/Pagination';
|
|
||||||
import { Table } from 'components/common/table/Table/Table.styled';
|
|
||||||
import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';
|
|
||||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
|
||||||
import { TableState } from 'lib/hooks/useTableState';
|
|
||||||
|
|
||||||
import {
|
|
||||||
isColumnElement,
|
|
||||||
SelectCell,
|
|
||||||
TableHeaderCellProps,
|
|
||||||
} from './TableColumn';
|
|
||||||
import { TableRow } from './TableRow';
|
|
||||||
|
|
||||||
interface SmartTableProps<T, TId extends IdType> {
|
|
||||||
tableState: TableState<T, TId>;
|
|
||||||
allSelectable?: boolean;
|
|
||||||
selectable?: boolean;
|
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
isFullwidth?: boolean;
|
|
||||||
paginated?: boolean;
|
|
||||||
hoverable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SmartTable = <T, TId extends IdType>({
|
|
||||||
children,
|
|
||||||
tableState,
|
|
||||||
selectable = false,
|
|
||||||
allSelectable = false,
|
|
||||||
placeholder = 'No Data Found',
|
|
||||||
isFullwidth = false,
|
|
||||||
paginated = false,
|
|
||||||
hoverable = false,
|
|
||||||
}: React.PropsWithChildren<SmartTableProps<T, TId>>) => {
|
|
||||||
const handleRowSelection = (row: T, checked: boolean) => {
|
|
||||||
tableState.setRowsSelection([row], checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerRow = React.useMemo(() => {
|
|
||||||
const headerCells = React.Children.map(children, (child) => {
|
|
||||||
if (!isColumnElement<T, TId>(child)) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { headerCell, title, orderValue } = child.props;
|
|
||||||
|
|
||||||
const HeaderCell = headerCell as React.FC<TableHeaderCellProps<T, TId>>;
|
|
||||||
return HeaderCell ? (
|
|
||||||
<S.TableHeaderCell>
|
|
||||||
<HeaderCell
|
|
||||||
orderValue={orderValue}
|
|
||||||
orderable={tableState.orderable}
|
|
||||||
tableState={tableState}
|
|
||||||
/>
|
|
||||||
</S.TableHeaderCell>
|
|
||||||
) : (
|
|
||||||
// TODO types will be changed after fixing TableHeaderCell
|
|
||||||
<TableHeaderCell
|
|
||||||
{...tableState.orderable}
|
|
||||||
orderValue={orderValue}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
let checkboxElement = null;
|
|
||||||
|
|
||||||
if (selectable) {
|
|
||||||
checkboxElement = allSelectable ? (
|
|
||||||
<SelectCell
|
|
||||||
rowIndex={-1}
|
|
||||||
el="th"
|
|
||||||
selectable
|
|
||||||
selected={tableState.selectedCount === tableState.data.length}
|
|
||||||
onChange={tableState.toggleSelection}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<S.TableHeaderCell />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
{checkboxElement}
|
|
||||||
{headerCells}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}, [children, allSelectable, tableState]);
|
|
||||||
|
|
||||||
const bodyRows = React.useMemo(() => {
|
|
||||||
if (tableState.data.length === 0) {
|
|
||||||
const colspan = React.Children.count(children) + +selectable;
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={colspan}>{placeholder}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return tableState.data.map((dataItem, index) => {
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={tableState.idSelector(dataItem)}
|
|
||||||
index={index}
|
|
||||||
hoverable={hoverable}
|
|
||||||
dataItem={dataItem}
|
|
||||||
tableState={tableState}
|
|
||||||
selectable={selectable}
|
|
||||||
onSelectChange={handleRowSelection}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
children,
|
|
||||||
handleRowSelection,
|
|
||||||
hoverable,
|
|
||||||
placeholder,
|
|
||||||
selectable,
|
|
||||||
tableState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Table isFullwidth={isFullwidth}>
|
|
||||||
<thead>{headerRow}</thead>
|
|
||||||
<tbody>{bodyRows}</tbody>
|
|
||||||
</Table>
|
|
||||||
{paginated && tableState.totalPages !== undefined && (
|
|
||||||
<Pagination totalPages={tableState.totalPages} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,102 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { TableState } from 'lib/hooks/useTableState';
|
|
||||||
import { SortOrder } from 'generated-sources';
|
|
||||||
import * as S from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';
|
|
||||||
import { DefaultTheme, StyledComponent } from 'styled-components';
|
|
||||||
|
|
||||||
export interface OrderableProps {
|
|
||||||
orderBy: string | null;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
handleOrderBy: (orderBy: string | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableCellPropsBase<T, TId extends IdType> {
|
|
||||||
tableState: TableState<T, TId>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableHeaderCellProps<T, TId extends IdType>
|
|
||||||
extends TableCellPropsBase<T, TId> {
|
|
||||||
orderable?: OrderableProps;
|
|
||||||
orderValue?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableCellProps<T, TId extends IdType>
|
|
||||||
extends TableCellPropsBase<T, TId> {
|
|
||||||
rowIndex: number;
|
|
||||||
dataItem: T;
|
|
||||||
hovered?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableColumnProps<T, TId extends IdType> {
|
|
||||||
cell?: React.FC<TableCellProps<T, TId>>;
|
|
||||||
children?: React.ReactElement;
|
|
||||||
headerCell?: React.FC<TableHeaderCellProps<T, TId>>;
|
|
||||||
field?: string;
|
|
||||||
title?: string;
|
|
||||||
maxWidth?: string;
|
|
||||||
className?: string;
|
|
||||||
orderValue?: string;
|
|
||||||
customTd?: typeof S.Td;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableColumn = <T, TId extends IdType>(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
_props: React.PropsWithChildren<TableColumnProps<T, TId>>
|
|
||||||
): React.ReactElement => {
|
|
||||||
return <td />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isColumnElement<T, TId extends IdType>(
|
|
||||||
element: React.ReactNode
|
|
||||||
): element is React.ReactElement<TableColumnProps<T, TId>> {
|
|
||||||
if (!React.isValidElement(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementType = (element as React.ReactElement).type;
|
|
||||||
return (
|
|
||||||
elementType === TableColumn ||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(elementType as any).originalType === TableColumn
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectCellProps {
|
|
||||||
selected: boolean;
|
|
||||||
selectable: boolean;
|
|
||||||
el: 'td' | 'th';
|
|
||||||
rowIndex: number;
|
|
||||||
onChange: (checked: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SelectCell: React.FC<SelectCellProps> = ({
|
|
||||||
selected,
|
|
||||||
selectable,
|
|
||||||
rowIndex,
|
|
||||||
onChange,
|
|
||||||
el,
|
|
||||||
}) => {
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange(e.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
let El: 'td' | StyledComponent<'th', DefaultTheme>;
|
|
||||||
if (el === 'th') {
|
|
||||||
El = S.TableHeaderCell;
|
|
||||||
} else {
|
|
||||||
El = el;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<El>
|
|
||||||
{selectable && (
|
|
||||||
<input
|
|
||||||
data-row={rowIndex}
|
|
||||||
onChange={handleChange}
|
|
||||||
type="checkbox"
|
|
||||||
checked={selected}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</El>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,86 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { propertyLookup } from 'lib/propertyLookup';
|
|
||||||
import { TableState } from 'lib/hooks/useTableState';
|
|
||||||
import { Td } from 'components/common/table/TableHeaderCell/TableHeaderCell.styled';
|
|
||||||
|
|
||||||
import { isColumnElement, SelectCell, TableCellProps } from './TableColumn';
|
|
||||||
|
|
||||||
interface TableRowProps<T, TId extends IdType = never> {
|
|
||||||
index: number;
|
|
||||||
id?: TId;
|
|
||||||
hoverable?: boolean;
|
|
||||||
tableState: TableState<T, TId>;
|
|
||||||
dataItem: T;
|
|
||||||
selectable: boolean;
|
|
||||||
onSelectChange?: (row: T, checked: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableRow = <T, TId extends IdType>({
|
|
||||||
children,
|
|
||||||
hoverable = false,
|
|
||||||
id,
|
|
||||||
index,
|
|
||||||
dataItem,
|
|
||||||
selectable,
|
|
||||||
tableState,
|
|
||||||
onSelectChange,
|
|
||||||
}: React.PropsWithChildren<TableRowProps<T, TId>>): React.ReactElement => {
|
|
||||||
const [hovered, setHovered] = React.useState(false);
|
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
|
||||||
setHovered(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
|
||||||
setHovered(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectChange = (checked: boolean) => {
|
|
||||||
onSelectChange?.(dataItem, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
tabIndex={index}
|
|
||||||
id={id as string}
|
|
||||||
onMouseEnter={hoverable ? handleMouseEnter : undefined}
|
|
||||||
onMouseLeave={hoverable ? handleMouseLeave : undefined}
|
|
||||||
>
|
|
||||||
{selectable && (
|
|
||||||
<SelectCell
|
|
||||||
rowIndex={index}
|
|
||||||
el="td"
|
|
||||||
selectable={tableState.isRowSelectable(dataItem)}
|
|
||||||
selected={tableState.selectedIds.has(tableState.idSelector(dataItem))}
|
|
||||||
onChange={handleSelectChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{React.Children.map(children, (child) => {
|
|
||||||
if (!isColumnElement<T, TId>(child)) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
const { cell, field, maxWidth, customTd } = child.props;
|
|
||||||
|
|
||||||
const Cell = cell as React.FC<TableCellProps<T, TId>> | undefined;
|
|
||||||
const TdComponent = customTd || Td;
|
|
||||||
|
|
||||||
const content = Cell ? (
|
|
||||||
<Cell
|
|
||||||
tableState={tableState}
|
|
||||||
hovered={hovered}
|
|
||||||
rowIndex={index}
|
|
||||||
dataItem={dataItem}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
field && propertyLookup(field, dataItem)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TdComponent maxWidth={maxWidth}>
|
|
||||||
{content as React.ReactNode}
|
|
||||||
</TdComponent>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -4,7 +4,7 @@ interface Props {
|
||||||
color: 'green' | 'gray' | 'yellow' | 'red' | 'white' | 'blue';
|
color: 'green' | 'gray' | 'yellow' | 'red' | 'white' | 'blue';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tag = styled.p<Props>`
|
export const Tag = styled.span.attrs({ role: 'widget' })<Props>`
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
@ -17,4 +17,5 @@ export const Tag = styled.p<Props>`
|
||||||
padding-right: 0.75em;
|
padding-right: 0.75em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
margin: 2px 0;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import {
|
import { ConnectorState, ConsumerGroupState } from 'generated-sources';
|
||||||
ConnectorState,
|
|
||||||
ConnectorStatus,
|
|
||||||
ConsumerGroup,
|
|
||||||
ConsumerGroupState,
|
|
||||||
TaskStatus,
|
|
||||||
} from 'generated-sources';
|
|
||||||
|
|
||||||
const getTagColor = ({
|
const getTagColor = (state?: string) => {
|
||||||
state,
|
|
||||||
}: ConnectorStatus | TaskStatus | ConsumerGroup) => {
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case ConnectorState.RUNNING:
|
case ConnectorState.RUNNING:
|
||||||
case ConsumerGroupState.STABLE:
|
case ConsumerGroupState.STABLE:
|
||||||
|
|
|
@ -21,7 +21,3 @@ const tableLinkMixin = css(
|
||||||
export const TableKeyLink = styled.td`
|
export const TableKeyLink = styled.td`
|
||||||
${tableLinkMixin}
|
${tableLinkMixin}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SmartTableKeyLink = styled.div`
|
|
||||||
${tableLinkMixin}
|
|
||||||
`;
|
|
||||||
|
|
|
@ -67,12 +67,6 @@ const DESCMixin = css(
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Td = styled.td<{ maxWidth?: string }>`
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: ${(props) => props.maxWidth};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Title = styled.span<TitleProps>(
|
export const Title = styled.span<TitleProps>(
|
||||||
({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`
|
({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`
|
||||||
font-family: Inter, sans-serif;
|
font-family: Inter, sans-serif;
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { propertyLookup } from 'lib/propertyLookup';
|
|
||||||
|
|
||||||
describe('Property Lookup', () => {
|
|
||||||
const entityObject = {
|
|
||||||
prop: {
|
|
||||||
nestedProp: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
it('returns undefined if property not found', () => {
|
|
||||||
expect(
|
|
||||||
propertyLookup('prop.nonExistingProp', entityObject)
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns value of nested property if it exists', () => {
|
|
||||||
expect(propertyLookup('prop.nestedProp', entityObject)).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
const usePagination = () => {
|
|
||||||
const { search, pathname } = useLocation();
|
|
||||||
const params = new URLSearchParams(search);
|
|
||||||
|
|
||||||
const page = params.get('page');
|
|
||||||
const perPage = params.get('perPage');
|
|
||||||
|
|
||||||
return {
|
|
||||||
page: page ? Number(page) : undefined,
|
|
||||||
perPage: perPage ? Number(perPage) : undefined,
|
|
||||||
pathname,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default usePagination;
|
|
|
@ -1,78 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { OrderableProps } from 'components/common/SmartTable/TableColumn';
|
|
||||||
|
|
||||||
export interface TableState<T, TId extends IdType> {
|
|
||||||
data: T[];
|
|
||||||
selectedIds: Set<TId>;
|
|
||||||
totalPages?: number;
|
|
||||||
idSelector: (row: T) => TId;
|
|
||||||
isRowSelectable: (row: T) => boolean;
|
|
||||||
selectedCount: number;
|
|
||||||
setRowsSelection: (rows: T[], selected: boolean) => void;
|
|
||||||
toggleSelection: (selected: boolean) => void;
|
|
||||||
orderable?: OrderableProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTableState = <T, TId extends IdType>(
|
|
||||||
data: T[],
|
|
||||||
options: {
|
|
||||||
totalPages: number;
|
|
||||||
isRowSelectable?: (row: T) => boolean;
|
|
||||||
idSelector: (row: T) => TId;
|
|
||||||
},
|
|
||||||
orderable?: OrderableProps
|
|
||||||
): TableState<T, TId> => {
|
|
||||||
const [selectedIds, setSelectedIds] = React.useState(new Set<TId>());
|
|
||||||
|
|
||||||
const { idSelector, totalPages, isRowSelectable = () => true } = options;
|
|
||||||
|
|
||||||
const selectedCount = selectedIds.size;
|
|
||||||
|
|
||||||
const setRowsSelection = useCallback(
|
|
||||||
(rows: T[], selected: boolean) => {
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const id = idSelector(row);
|
|
||||||
const newSet = new Set(selectedIds);
|
|
||||||
if (selected) {
|
|
||||||
newSet.add(id);
|
|
||||||
} else {
|
|
||||||
newSet.delete(id);
|
|
||||||
}
|
|
||||||
setSelectedIds(newSet);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[idSelector, selectedIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleSelection = useCallback(
|
|
||||||
(selected: boolean) => {
|
|
||||||
const newSet = new Set(selected ? data.map((r) => idSelector(r)) : []);
|
|
||||||
setSelectedIds(newSet);
|
|
||||||
},
|
|
||||||
[data, idSelector]
|
|
||||||
);
|
|
||||||
|
|
||||||
return React.useMemo<TableState<T, TId>>(() => {
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
totalPages,
|
|
||||||
selectedIds,
|
|
||||||
orderable,
|
|
||||||
selectedCount,
|
|
||||||
idSelector,
|
|
||||||
isRowSelectable,
|
|
||||||
setRowsSelection,
|
|
||||||
toggleSelection,
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
data,
|
|
||||||
orderable,
|
|
||||||
selectedIds,
|
|
||||||
totalPages,
|
|
||||||
selectedCount,
|
|
||||||
idSelector,
|
|
||||||
isRowSelectable,
|
|
||||||
setRowsSelection,
|
|
||||||
toggleSelection,
|
|
||||||
]);
|
|
||||||
};
|
|
|
@ -1,9 +0,0 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function propertyLookup<T extends { [key: string]: any }>(
|
|
||||||
path: string,
|
|
||||||
obj: T
|
|
||||||
) {
|
|
||||||
return path.split('.').reduce((prev, curr) => {
|
|
||||||
return prev ? prev[curr] : null;
|
|
||||||
}, obj);
|
|
||||||
}
|
|
|
@ -25,11 +25,6 @@ export const consumerGroups = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const noConsumerGroupsResponse = {
|
|
||||||
pageCount: 1,
|
|
||||||
consumerGroups: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const consumerGroupPayload = {
|
export const consumerGroupPayload = {
|
||||||
groupId: 'amazon.msk.canary.group.broker-1',
|
groupId: 'amazon.msk.canary.group.broker-1',
|
||||||
members: 0,
|
members: 0,
|
||||||
|
|
|
@ -447,22 +447,6 @@ const theme = {
|
||||||
},
|
},
|
||||||
color: Colors.neutral[90],
|
color: Colors.neutral[90],
|
||||||
},
|
},
|
||||||
pagination: {
|
|
||||||
backgroundColor: Colors.neutral[0],
|
|
||||||
currentPage: Colors.neutral[10],
|
|
||||||
borderColor: {
|
|
||||||
normal: Colors.neutral[30],
|
|
||||||
hover: Colors.neutral[50],
|
|
||||||
active: Colors.neutral[70],
|
|
||||||
disabled: Colors.neutral[20],
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
normal: Colors.neutral[90],
|
|
||||||
hover: Colors.neutral[90],
|
|
||||||
active: Colors.neutral[90],
|
|
||||||
disabled: Colors.neutral[20],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
switch: {
|
switch: {
|
||||||
unchecked: Colors.brand[20],
|
unchecked: Colors.brand[20],
|
||||||
checked: Colors.brand[50],
|
checked: Colors.brand[50],
|
||||||
|
|
Loading…
Add table
Reference in a new issue