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 { ClusterName } from 'redux/interfaces';
|
||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
import { useBrokers } from 'lib/hooks/api/brokers';
|
||||
import { useClusterStats } from 'lib/hooks/api/clusters';
|
||||
|
||||
import { ClickableRow } from './BrokersList.style';
|
||||
import Table, { LinkCell, SizeCell } from 'components/common/NewTable';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { clusterBrokerPath } from 'lib/paths';
|
||||
|
||||
const BrokersList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
|
||||
const { data: clusterStats } = useClusterStats(clusterName);
|
||||
const { data: clusterStats = {} } = useClusterStats(clusterName);
|
||||
const { data: brokers } = useBrokers(clusterName);
|
||||
|
||||
if (!clusterStats) return null;
|
||||
|
||||
const {
|
||||
brokerCount,
|
||||
activeControllers,
|
||||
|
@ -32,6 +28,32 @@ const BrokersList: React.FC = () => {
|
|||
version,
|
||||
} = 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 areAllInSync = inSyncReplicasCount && replicas === inSyncReplicasCount;
|
||||
const partitionIsOffline = offlinePartitionCount && offlinePartitionCount > 0;
|
||||
|
@ -95,49 +117,15 @@ const BrokersList: React.FC = () => {
|
|||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Broker" />
|
||||
<TableHeaderCell title="Segment Size" />
|
||||
<TableHeaderCell title="Segment Count" />
|
||||
<TableHeaderCell title="Port" />
|
||||
<TableHeaderCell title="Host" />
|
||||
</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>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={rows}
|
||||
enableSorting
|
||||
onRowClick={({ original: { brokerId } }) =>
|
||||
navigate(clusterBrokerPath(clusterName, brokerId))
|
||||
}
|
||||
emptyMessage="Disk usage data not available"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { render, WithRoute } from 'lib/testHelpers';
|
||||
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 BrokersList from 'components/Brokers/BrokersList/BrokersList';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
@ -41,6 +41,7 @@ describe('BrokersList Component', () => {
|
|||
);
|
||||
|
||||
describe('BrokersList', () => {
|
||||
describe('when the brokers are loaded', () => {
|
||||
beforeEach(() => {
|
||||
(useBrokers as jest.Mock).mockImplementation(() => ({
|
||||
data: brokersPayload,
|
||||
|
@ -49,19 +50,21 @@ describe('BrokersList Component', () => {
|
|||
data: clusterStatsPayload,
|
||||
}));
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
renderComponent();
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
const rows = screen.getAllByRole('row');
|
||||
expect(rows.length).toEqual(3);
|
||||
expect(screen.getAllByRole('row').length).toEqual(3);
|
||||
});
|
||||
it('opens broker when row clicked', async () => {
|
||||
renderComponent();
|
||||
await act(() => {
|
||||
userEvent.click(screen.getByRole('cell', { name: '0' }));
|
||||
});
|
||||
await waitFor(() => expect(mockedUsedNavigate).toBeCalledWith('0'));
|
||||
await waitFor(() =>
|
||||
expect(mockedUsedNavigate).toBeCalledWith(
|
||||
clusterBrokerPath(clusterName, '0')
|
||||
)
|
||||
);
|
||||
});
|
||||
it('shows warning when offlinePartitionCount > 0', async () => {
|
||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||
|
@ -93,8 +96,7 @@ describe('BrokersList Component', () => {
|
|||
expect(onlineWidgetDef).toBeInTheDocument();
|
||||
expect(onlineWidget).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows right count when inSyncReplicasCount: undefined outOfSyncReplicasCount: 1', async () => {
|
||||
it('shows right count when inSyncReplicasCount: undefined && outOfSyncReplicasCount: 1', async () => {
|
||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||
data: {
|
||||
...clusterStatsPayload,
|
||||
|
@ -103,7 +105,9 @@ describe('BrokersList Component', () => {
|
|||
},
|
||||
}));
|
||||
renderComponent();
|
||||
const onlineWidget = screen.getByText(`of ${testOutOfSyncReplicasCount}`);
|
||||
const onlineWidget = screen.getByText(
|
||||
`of ${testOutOfSyncReplicasCount}`
|
||||
);
|
||||
expect(onlineWidget).toBeInTheDocument();
|
||||
});
|
||||
it(`shows right count when inSyncReplicasCount: ${testInSyncReplicasCount} outOfSyncReplicasCount: undefined`, async () => {
|
||||
|
@ -121,4 +125,24 @@ describe('BrokersList Component', () => {
|
|||
expect(onlineWidget).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when diskUsage is empty', () => {
|
||||
beforeEach(() => {
|
||||
(useBrokers as jest.Mock).mockImplementation(() => ({
|
||||
data: brokersPayload,
|
||||
}));
|
||||
(useClusterStats as jest.Mock).mockImplementation(() => ({
|
||||
data: { ...clusterStatsPayload, diskUsage: undefined },
|
||||
}));
|
||||
});
|
||||
|
||||
it('renders empty table', async () => {
|
||||
renderComponent();
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('row', { name: 'Disk usage data not available' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ const Overview: React.FC = () => {
|
|||
</Metrics.Indicator>
|
||||
)}
|
||||
<Metrics.Indicator label="State">
|
||||
<C.Tag color={getTagColor(connector.status)}>
|
||||
<C.Tag color={getTagColor(connector.status.state)}>
|
||||
{connector.status.state}
|
||||
</C.Tag>
|
||||
</Metrics.Indicator>
|
||||
|
|
|
@ -43,7 +43,9 @@ const Tasks: React.FC = () => {
|
|||
<td>{task.status?.id}</td>
|
||||
<td>{task.status?.workerId}</td>
|
||||
<td>
|
||||
<Tag color={getTagColor(task.status)}>{task.status.state}</Tag>
|
||||
<Tag color={getTagColor(task.status.state)}>
|
||||
{task.status.state}
|
||||
</Tag>
|
||||
</td>
|
||||
<td>{task.status.trace || 'null'}</td>
|
||||
<td style={{ width: '5%' }}>
|
||||
|
|
|
@ -72,7 +72,9 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
))}
|
||||
</S.TagsWrapper>
|
||||
</td>
|
||||
<td>{status && <Tag color={getTagColor(status)}>{status.state}</Tag>}</td>
|
||||
<td>
|
||||
{status && <Tag color={getTagColor(status.state)}>{status.state}</Tag>}
|
||||
</td>
|
||||
<td>
|
||||
{runningTasks && (
|
||||
<span>
|
||||
|
|
|
@ -82,7 +82,9 @@ const Details: React.FC = () => {
|
|||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
<Metrics.Indicator label="State">
|
||||
<Tag color={getTagColor(consumerGroup)}>{consumerGroup.state}</Tag>
|
||||
<Tag color={getTagColor(consumerGroup.state)}>
|
||||
{consumerGroup.state}
|
||||
</Tag>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="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,
|
||||
SortOrder,
|
||||
} 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 { useAppDispatch } from 'lib/hooks/redux';
|
||||
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 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 {
|
||||
consumerGroups: ConsumerGroupDetails[];
|
||||
orderBy: string | null;
|
||||
sortOrder: SortOrder;
|
||||
totalPages: number;
|
||||
isFetched: boolean;
|
||||
setConsumerGroupsSortOrderBy(orderBy: string | null): void;
|
||||
}
|
||||
|
||||
const List: React.FC<Props> = ({
|
||||
consumerGroups,
|
||||
sortOrder,
|
||||
orderBy,
|
||||
totalPages,
|
||||
isFetched,
|
||||
setConsumerGroupsSortOrderBy,
|
||||
}) => {
|
||||
const { page, perPage } = usePagination();
|
||||
const List: React.FC<Props> = ({ consumerGroups, totalPages }) => {
|
||||
const [searchText, handleSearchText] = useSearch();
|
||||
const dispatch = useAppDispatch();
|
||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(
|
||||
fetchConsumerGroupsPaged({
|
||||
clusterName,
|
||||
orderBy: (orderBy as ConsumerGroupOrdering) || undefined,
|
||||
sortOrder,
|
||||
page,
|
||||
perPage,
|
||||
orderBy:
|
||||
(searchParams.get('sortBy') as ConsumerGroupOrdering) || undefined,
|
||||
sortOrder:
|
||||
(searchParams.get('sortDirection')?.toUpperCase() as SortOrder) ||
|
||||
undefined,
|
||||
page: Number(searchParams.get('page') || 1),
|
||||
perPage: Number(searchParams.get('perPage') || PER_PAGE),
|
||||
search: searchText,
|
||||
})
|
||||
);
|
||||
}, [clusterName, orderBy, searchText, sortOrder, page, perPage, dispatch]);
|
||||
}, [clusterName, searchText, dispatch, searchParams]);
|
||||
|
||||
const tableState = useTableState<ConsumerGroupDetails, string>(
|
||||
consumerGroups,
|
||||
const columns = React.useMemo<ColumnDef<ConsumerGroupDetails>[]>(
|
||||
() => [
|
||||
{
|
||||
totalPages,
|
||||
idSelector: (consumerGroup) => consumerGroup.groupId,
|
||||
id: ConsumerGroupOrdering.NAME,
|
||||
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 (
|
||||
<div>
|
||||
<>
|
||||
<PageHeading text="Consumers" />
|
||||
<ControlPanelWrapper hasInput>
|
||||
<Search
|
||||
|
@ -84,33 +93,20 @@ const List: React.FC<Props> = ({
|
|||
handleSearch={handleSearchText}
|
||||
/>
|
||||
</ControlPanelWrapper>
|
||||
<SmartTable
|
||||
tableState={tableState}
|
||||
isFullwidth
|
||||
placeholder="No active consumer groups"
|
||||
hoverable
|
||||
paginated
|
||||
>
|
||||
<TableColumn
|
||||
title="Consumer Group ID"
|
||||
cell={GroupIDCell}
|
||||
orderValue={ConsumerGroupOrdering.NAME}
|
||||
<Table
|
||||
columns={columns}
|
||||
pageCount={totalPages}
|
||||
data={consumerGroups}
|
||||
emptyMessage="No active consumer groups found"
|
||||
serverSideProcessing
|
||||
enableSorting
|
||||
onRowClick={({ original }) =>
|
||||
navigate(
|
||||
clusterConsumerGroupDetailsPath(clusterName, original.groupId)
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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 {
|
||||
getConsumerGroupsOrderBy,
|
||||
getConsumerGroupsSortOrder,
|
||||
getConsumerGroupsTotalPages,
|
||||
sortBy,
|
||||
selectAll,
|
||||
getAreConsumerGroupsPagedFulfilled,
|
||||
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
import List from 'components/ConsumerGroups/List/List';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
consumerGroups: selectAll(state),
|
||||
orderBy: getConsumerGroupsOrderBy(state),
|
||||
sortOrder: getConsumerGroupsSortOrder(state),
|
||||
totalPages: getConsumerGroupsTotalPages(state),
|
||||
isFetched: getAreConsumerGroupsPagedFulfilled(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setConsumerGroupsSortOrderBy: sortBy,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(List);
|
||||
export default connect(mapStateToProps)(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 List, { Props } from 'components/ConsumerGroups/List/List';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { consumerGroups as consumerGroupMock } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
|
||||
import theme from 'theme/theme';
|
||||
import { clusterConsumerGroupDetailsPath } from 'lib/paths';
|
||||
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', () => {
|
||||
const setUpComponent = (props: Partial<Props> = {}) => {
|
||||
const {
|
||||
consumerGroups,
|
||||
orderBy,
|
||||
sortOrder,
|
||||
totalPages,
|
||||
setConsumerGroupsSortOrderBy,
|
||||
} = props;
|
||||
const renderComponent = (props: Partial<Props> = {}) => {
|
||||
const { consumerGroups, totalPages } = props;
|
||||
return render(
|
||||
<List
|
||||
consumerGroups={consumerGroups || []}
|
||||
orderBy={orderBy || ConsumerGroupOrdering.NAME}
|
||||
sortOrder={sortOrder || SortOrder.ASC}
|
||||
setConsumerGroupsSortOrderBy={setConsumerGroupsSortOrderBy || jest.fn()}
|
||||
totalPages={totalPages || 1}
|
||||
isFetched={'isFetched' in props ? !!props.isFetched : true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders empty table', () => {
|
||||
setUpComponent();
|
||||
renderComponent();
|
||||
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', () => {
|
||||
beforeEach(() => setUpComponent({ consumerGroups: consumerGroupMock }));
|
||||
beforeEach(() => renderComponent({ consumerGroups: consumerGroupMock }));
|
||||
|
||||
it('renders all rows with consumers', () => {
|
||||
expect(screen.getByText('groupId1')).toBeInTheDocument();
|
||||
expect(screen.getByText('groupId2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Testing the Ordering', () => {
|
||||
it('should test the sort order functionality', async () => {
|
||||
const thElement = screen.getByText(/consumer group id/i);
|
||||
expect(thElement).toBeInTheDocument();
|
||||
expect(thElement).toHaveStyle(`color:${theme.table.th.color.active}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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}`
|
||||
it('handles onRowClick', () => {
|
||||
const row = screen.getByRole('row', { name: 'groupId1 0 1 1' });
|
||||
expect(row).toBeInTheDocument();
|
||||
userEvent.click(row);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(
|
||||
clusterConsumerGroupDetailsPath(':clusterName', 'groupId1')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 { clusterConsumerGroupsPath, getNonExactPath } from 'lib/paths';
|
||||
import {
|
||||
act,
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
clusterConsumerGroupDetailsPath,
|
||||
clusterConsumerGroupResetOffsetsPath,
|
||||
clusterConsumerGroupsPath,
|
||||
getNonExactPath,
|
||||
} from 'lib/paths';
|
||||
import { screen } from '@testing-library/react';
|
||||
import ConsumerGroups from 'components/ConsumerGroups/ConsumerGroups';
|
||||
import {
|
||||
consumerGroups,
|
||||
noConsumerGroupsResponse,
|
||||
} from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||
import { render, WithRoute } from 'lib/testHelpers';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { ConsumerGroupOrdering, SortOrder } from 'generated-sources';
|
||||
|
||||
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) =>
|
||||
render(
|
||||
<WithRoute path={getNonExactPath(clusterConsumerGroupsPath())}>
|
||||
|
@ -28,104 +33,18 @@ const renderComponent = (path?: string) =>
|
|||
);
|
||||
|
||||
describe('ConsumerGroups', () => {
|
||||
it('renders with initial state', async () => {
|
||||
it('renders ListContainer', async () => {
|
||||
renderComponent();
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('ListContainerMock')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Default Route and Fetching Consumer Groups', () => {
|
||||
const url = `/api/clusters/${clusterName}/consumer-groups/paged`;
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('renders empty table on no consumer group response', async () => {
|
||||
fetchMock.getOnce(url, noConsumerGroupsResponse, {
|
||||
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,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
it('renders ResetOffsets', async () => {
|
||||
renderComponent(
|
||||
`${clusterConsumerGroupsPath(clusterName)}?q=${searchText}`
|
||||
clusterConsumerGroupResetOffsetsPath(clusterName, 'groupId1')
|
||||
);
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
|
||||
await waitFor(() => expect(consumerGroupsMock.called()).toBeTruthy());
|
||||
|
||||
expect(screen.getByText(searchText)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(consumerGroups[1].groupId)
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText('ResetOffsetsMock')).toBeInTheDocument();
|
||||
});
|
||||
it('renders Details', async () => {
|
||||
renderComponent(clusterConsumerGroupDetailsPath(clusterName, 'groupId1'));
|
||||
expect(screen.getByText('DetailsMock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
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 Table from 'components/common/NewTable';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
export enum KsqlDbItemType {
|
||||
Tables = 'tables',
|
||||
|
@ -31,27 +30,28 @@ export interface KsqlTableState {
|
|||
|
||||
const KsqlDbItem: React.FC<KsqlDbItemProps> = ({ type, fetching, rows }) => {
|
||||
const preparedRows = rows[type]?.map(ksqlRowData) || [];
|
||||
const tableState = useTableState<KsqlTableState, string>(preparedRows, {
|
||||
idSelector: ({ name }) => name,
|
||||
totalPages: 0,
|
||||
});
|
||||
|
||||
const columns = React.useMemo<ColumnDef<KsqlTableState>[]>(
|
||||
() => [
|
||||
{ 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) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
return (
|
||||
<SmartTable
|
||||
tableState={tableState}
|
||||
isFullwidth
|
||||
placeholder="No tables or streams found"
|
||||
hoverable
|
||||
>
|
||||
<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>
|
||||
<Table
|
||||
data={preparedRows}
|
||||
columns={columns}
|
||||
emptyMessage="No tables or streams found"
|
||||
enableSorting={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,11 +7,12 @@ import KsqlDbItem, {
|
|||
} from 'components/KsqlDb/List/KsqlDbItem/KsqlDbItem';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
describe('KsqlDbItem', () => {
|
||||
const tablesPathname = clusterKsqlDbTablesPath();
|
||||
|
||||
const component = (props: Partial<KsqlDbItemProps> = {}) => (
|
||||
const renderComponent = (props: Partial<KsqlDbItemProps> = {}) => {
|
||||
render(
|
||||
<WithRoute path={tablesPathname}>
|
||||
<KsqlDbItem
|
||||
type={KsqlDbItemType.Tables}
|
||||
|
@ -19,47 +20,43 @@ describe('KsqlDbItem', () => {
|
|||
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(
|
||||
component({
|
||||
rows: {
|
||||
tables: fetchKsqlDbTablesPayload.tables,
|
||||
streams: [],
|
||||
},
|
||||
}),
|
||||
</WithRoute>,
|
||||
{
|
||||
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);
|
||||
});
|
||||
it('renders with streams', () => {
|
||||
render(
|
||||
component({
|
||||
it('renders with streams', async () => {
|
||||
await act(() =>
|
||||
renderComponent({
|
||||
type: KsqlDbItemType.Streams,
|
||||
rows: {
|
||||
tables: [],
|
||||
streams: fetchKsqlDbTablesPayload.streams,
|
||||
},
|
||||
}),
|
||||
{
|
||||
initialEntries: [clusterKsqlDbTablesPath()],
|
||||
}
|
||||
})
|
||||
);
|
||||
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 fetchMock from 'fetch-mock';
|
||||
import { screen } from '@testing-library/dom';
|
||||
|
||||
const renderComponent = () => {
|
||||
render(<List />);
|
||||
};
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
describe('KsqlDb List', () => {
|
||||
afterEach(() => fetchMock.reset());
|
||||
it('renders List component with Tables and Streams tabs', async () => {
|
||||
renderComponent();
|
||||
await act(() => {
|
||||
render(<List />);
|
||||
});
|
||||
|
||||
const Tables = screen.getByTitle('Tables');
|
||||
const Streams = screen.getByTitle('Streams');
|
||||
|
|
|
@ -2,22 +2,21 @@ import React from 'react';
|
|||
import Select from 'components/common/Select/Select';
|
||||
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
||||
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 { fetchSchemas } from 'redux/reducers/schemas/schemasSlice';
|
||||
import { ClusterNameRoute } from 'lib/paths';
|
||||
import { schemasApiClient } from 'lib/api';
|
||||
import { showServerError } from 'lib/errorHandling';
|
||||
import { useConfirm } from 'lib/hooks/useConfirm';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { PER_PAGE } from 'lib/constants';
|
||||
|
||||
import * as S from './GlobalSchemaSelector.styled';
|
||||
|
||||
const GlobalSchemaSelector: React.FC = () => {
|
||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||
const dispatch = useAppDispatch();
|
||||
const [searchText] = useSearch();
|
||||
const { page, perPage } = usePagination();
|
||||
const [searchParams] = useSearchParams();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const [currentCompatibilityLevel, setCurrentCompatibilityLevel] =
|
||||
|
@ -61,7 +60,12 @@ const GlobalSchemaSelector: React.FC = () => {
|
|||
});
|
||||
setCurrentCompatibilityLevel(nextLevel);
|
||||
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) {
|
||||
showServerError(e as Response);
|
||||
|
|
|
@ -82,7 +82,7 @@ describe('GlobalSchemaSelector', () => {
|
|||
}
|
||||
);
|
||||
const getSchemasMock = fetchMock.getOnce(
|
||||
`api/clusters/${clusterName}/schemas`,
|
||||
`api/clusters/${clusterName}/schemas?page=1&perPage=25`,
|
||||
200
|
||||
);
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import { ClusterNameRoute, clusterSchemaNewRelativePath } from 'lib/paths';
|
||||
import {
|
||||
ClusterNameRoute,
|
||||
clusterSchemaNewRelativePath,
|
||||
clusterSchemaPath,
|
||||
} from 'lib/paths';
|
||||
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 PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
|
@ -13,36 +15,53 @@ import {
|
|||
getAreSchemasFulfilled,
|
||||
SCHEMAS_FETCH_ACTION,
|
||||
} from 'redux/reducers/schemas/schemasSlice';
|
||||
import usePagination from 'lib/hooks/usePagination';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import Pagination from 'components/common/Pagination/Pagination';
|
||||
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
||||
import Search from 'components/common/Search/Search';
|
||||
import useSearch from 'lib/hooks/useSearch';
|
||||
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';
|
||||
|
||||
const List: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const schemas = useAppSelector(selectAllSchemas);
|
||||
const isFetched = useAppSelector(getAreSchemasFulfilled);
|
||||
const totalPages = useAppSelector((state) => state.schemas.totalPages);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchText, handleSearchText] = useSearch();
|
||||
const { page, perPage } = usePagination();
|
||||
|
||||
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 () => {
|
||||
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 (
|
||||
<>
|
||||
|
@ -68,31 +87,16 @@ const List: React.FC = () => {
|
|||
/>
|
||||
</ControlPanelWrapper>
|
||||
{isFetched ? (
|
||||
<>
|
||||
<C.Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Schema Name" />
|
||||
<TableHeaderCell title="Version" />
|
||||
<TableHeaderCell title="Compatibility" />
|
||||
</tr>
|
||||
</thead>
|
||||
<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}
|
||||
<Table
|
||||
columns={columns}
|
||||
data={schemas}
|
||||
pageCount={totalPages}
|
||||
emptyMessage="No schemas found"
|
||||
onRowClick={(row) =>
|
||||
navigate(clusterSchemaPath(clusterName, row.original.subject))
|
||||
}
|
||||
serverSideProcessing
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</C.Table>
|
||||
<Pagination totalPages={totalPages} />
|
||||
</>
|
||||
) : (
|
||||
<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 List from 'components/Schemas/List/List';
|
||||
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 {
|
||||
schemasFulfilledState,
|
||||
|
@ -15,12 +15,20 @@ import ClusterContext, {
|
|||
} from 'components/contexts/ClusterContext';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
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 schemasAPIUrl = `/api/clusters/${clusterName}/schemas`;
|
||||
const schemasAPICompabilityUrl = `${schemasAPIUrl}/compatibility`;
|
||||
const schemasAPIUrl = `/api/clusters/${clusterName}/schemas?page=1&perPage=25`;
|
||||
const schemasAPICompabilityUrl = `/api/clusters/${clusterName}/schemas/compatibility`;
|
||||
const renderComponent = (
|
||||
initialState: RootState['schemas'] = schemasInitialState,
|
||||
context: ContextProps = contextInitialValue
|
||||
|
@ -101,6 +109,17 @@ describe('List', () => {
|
|||
expect(screen.getByText(schemaVersion1.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', () => {
|
||||
|
@ -109,6 +128,7 @@ describe('List', () => {
|
|||
schemasAPIUrl,
|
||||
schemasPayload
|
||||
);
|
||||
fetchMock.getOnce(schemasAPICompabilityUrl, 200);
|
||||
await act(() => {
|
||||
renderComponent(schemasFulfilledState, {
|
||||
...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,
|
||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
|
||||
export const schemas = [schemaVersion1, schemaVersion2];
|
||||
const schemas = [schemaVersion1, schemaVersion2];
|
||||
|
||||
export const schemasPayload = {
|
||||
pageCount: 1,
|
||||
|
|
|
@ -44,7 +44,7 @@ const TopicConsumerGroups: React.FC = () => {
|
|||
<td>{consumer.coordinator?.id}</td>
|
||||
<td>
|
||||
{consumer.state && (
|
||||
<Tag color={getTagColor(consumer)}>{`${consumer.state
|
||||
<Tag color={getTagColor(consumer.state)}>{`${consumer.state
|
||||
.charAt(0)
|
||||
.toUpperCase()}${consumer.state
|
||||
.slice(1)
|
||||
|
|
|
@ -69,22 +69,6 @@ export const MetadataMeta = styled.p`
|
|||
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 }>(
|
||||
({ theme, $active }) => css`
|
||||
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';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ExpanderCell: React.FC<CellContext<any, unknown>> = ({ row }) => (
|
||||
const ExpanderCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (
|
||||
<S.ExpaderButton
|
||||
width="16"
|
||||
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 IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const SelectRowCell: React.FC<CellContext<any, unknown>> = ({ row }) => (
|
||||
const SelectRowCell: React.FC<CellContext<unknown, unknown>> = ({ row }) => (
|
||||
<IndeterminateCheckbox
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
|
|
|
@ -2,8 +2,9 @@ import { HeaderContext } from '@tanstack/react-table';
|
|||
import React from 'react';
|
||||
import IndeterminateCheckbox from 'components/common/IndeterminateCheckbox/IndeterminateCheckbox';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const SelectRowHeader: React.FC<HeaderContext<any, unknown>> = ({ table }) => (
|
||||
const SelectRowHeader: React.FC<HeaderContext<unknown, unknown>> = ({
|
||||
table,
|
||||
}) => (
|
||||
<IndeterminateCheckbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
indeterminate={table.getIsSomePageRowsSelected()}
|
||||
|
|
|
@ -3,8 +3,8 @@ import { CellContext } from '@tanstack/react-table';
|
|||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const SizeCell: React.FC<CellContext<any, any>> = ({ getValue }) => (
|
||||
<BytesFormatted value={getValue()} />
|
||||
const SizeCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
|
||||
<BytesFormatted value={getValue<string | number>()} />
|
||||
);
|
||||
|
||||
export default SizeCell;
|
||||
|
|
|
@ -99,13 +99,13 @@ export const Th = styled.th<ThProps>(
|
|||
);
|
||||
|
||||
interface RowProps {
|
||||
expandable?: boolean;
|
||||
clickable?: boolean;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export const Row = styled.tr<RowProps>(
|
||||
({ theme: { table }, expanded, expandable }) => `
|
||||
cursor: ${expandable ? 'pointer' : 'default'};
|
||||
({ theme: { table }, expanded, clickable }) => `
|
||||
cursor: ${clickable ? 'pointer' : 'default'};
|
||||
background-color: ${table.tr.backgroundColor[expanded ? 'hover' : 'normal']};
|
||||
&:hover {
|
||||
background-color: ${table.tr.backgroundColor.hover};
|
||||
|
|
|
@ -28,13 +28,26 @@ export interface TableProps<TData> {
|
|||
data: TData[];
|
||||
pageCount?: number;
|
||||
columns: ColumnDef<TData>[];
|
||||
renderSubComponent?: React.FC<{ row: Row<TData> }>;
|
||||
getRowCanExpand?: (row: Row<TData>) => boolean;
|
||||
|
||||
// Server-side processing: sorting, pagination
|
||||
serverSideProcessing?: boolean;
|
||||
enableSorting?: boolean;
|
||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
|
||||
batchActionsBar?: React.FC<{ rows: Row<TData>[]; resetRowSelection(): void }>;
|
||||
|
||||
// Expandeble rows
|
||||
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;
|
||||
|
||||
// Handles row click. Can not be combined with `enableRowSelection` && expandable rows.
|
||||
onRowClick?: (row: Row<TData>) => void;
|
||||
}
|
||||
|
||||
type UpdaterFn<T> = (previousState: T) => T;
|
||||
|
@ -72,6 +85,7 @@ const getSortingFromSearchParams = (searchParams: URLSearchParams) => {
|
|||
* `enableSorting = false` to the column def.
|
||||
* - table component stores the sorting state in URLSearchParams. Use `sortBy` and `sortDirection`
|
||||
* search param to set default sortings.
|
||||
* - use `id` property of the column def to set the sortBy for server side sorting.
|
||||
*
|
||||
* 2. Pagination
|
||||
* - pagination enabled by default.
|
||||
|
@ -107,6 +121,7 @@ const Table: React.FC<TableProps<any>> = ({
|
|||
enableRowSelection = false,
|
||||
batchActionsBar,
|
||||
emptyMessage,
|
||||
onRowClick,
|
||||
}) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
|
@ -157,6 +172,24 @@ const Table: React.FC<TableProps<any>> = ({
|
|||
|
||||
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 (
|
||||
<>
|
||||
{table.getSelectedRowModel().flatRows.length > 0 && Bar && (
|
||||
|
@ -205,9 +238,12 @@ const Table: React.FC<TableProps<any>> = ({
|
|||
{table.getRowModel().rows.map((row) => (
|
||||
<React.Fragment key={row.id}>
|
||||
<S.Row
|
||||
expandable={row.getCanExpand()}
|
||||
expanded={row.getIsExpanded()}
|
||||
onClick={() => row.getCanExpand() && row.toggleExpanded()}
|
||||
onClick={handleRowClick(row)}
|
||||
clickable={
|
||||
!enableRowSelection &&
|
||||
(row.getCanExpand() || onRowClick !== undefined)
|
||||
}
|
||||
>
|
||||
{!!enableRowSelection && (
|
||||
<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';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const TimestampCell: React.FC<CellContext<any, any>> = ({ getValue }) => (
|
||||
<S.Nowrap>{formatTimestamp(getValue())}</S.Nowrap>
|
||||
const TimestampCell: React.FC<CellContext<any, unknown>> = ({ getValue }) => (
|
||||
<S.Nowrap>{formatTimestamp(getValue<string | number>())}</S.Nowrap>
|
||||
);
|
||||
|
||||
export default TimestampCell;
|
||||
|
|
|
@ -4,20 +4,54 @@ import Table, {
|
|||
TableProps,
|
||||
TimestampCell,
|
||||
SizeCell,
|
||||
LinkCell,
|
||||
TagCell,
|
||||
} from 'components/common/NewTable';
|
||||
import { screen, waitFor } from '@testing-library/dom';
|
||||
import { ColumnDef, Row } from '@tanstack/react-table';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { formatTimestamp } from 'lib/dateTimeHelpers';
|
||||
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];
|
||||
|
||||
const data = [
|
||||
{ timestamp: 1660034383725, text: 'lorem', selectable: false, size: 1234 },
|
||||
{ timestamp: 1660034399999, text: 'ipsum', selectable: true, size: 3 },
|
||||
{ timestamp: 1660034399922, text: 'dolor', selectable: true, size: 50000 },
|
||||
{ timestamp: 1660034199922, text: 'sit', selectable: false, size: 1_312_323 },
|
||||
{
|
||||
timestamp: 1660034383725,
|
||||
text: 'lorem',
|
||||
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>[] = [
|
||||
|
@ -29,12 +63,18 @@ const columns: ColumnDef<Datum>[] = [
|
|||
{
|
||||
header: 'Text',
|
||||
accessorKey: 'text',
|
||||
cell: LinkCell,
|
||||
},
|
||||
{
|
||||
header: 'Size',
|
||||
accessorKey: 'size',
|
||||
cell: SizeCell,
|
||||
},
|
||||
{
|
||||
header: 'Tag',
|
||||
accessorKey: 'tag',
|
||||
cell: TagCell,
|
||||
},
|
||||
];
|
||||
|
||||
const ExpandedRow: React.FC = () => <div>I am expanded row</div>;
|
||||
|
@ -45,7 +85,7 @@ interface Props extends TableProps<Datum> {
|
|||
|
||||
const renderComponent = (props: Partial<Props> = {}) => {
|
||||
render(
|
||||
<WithRoute path="/">
|
||||
<WithRoute path="/*">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data}
|
||||
|
@ -94,6 +134,21 @@ describe('Table', () => {
|
|||
).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', () => {
|
||||
it('renders button', () => {
|
||||
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', () => {
|
||||
it('does not render page buttons', () => {
|
||||
renderComponent();
|
||||
|
@ -240,4 +303,34 @@ describe('Table', () => {
|
|||
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 TimestampCell from './TimestampCell';
|
||||
import SizeCell from './SizeCell';
|
||||
import LinkCell from './LinkCell';
|
||||
import TagCell from './TagCell';
|
||||
|
||||
export type { TableProps };
|
||||
|
||||
export { TimestampCell, SizeCell };
|
||||
export { TimestampCell, SizeCell, LinkCell, TagCell };
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
export const Tag = styled.p<Props>`
|
||||
export const Tag = styled.span.attrs({ role: 'widget' })<Props>`
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
height: 20px;
|
||||
|
@ -17,4 +17,5 @@ export const Tag = styled.p<Props>`
|
|||
padding-right: 0.75em;
|
||||
text-align: center;
|
||||
width: max-content;
|
||||
margin: 2px 0;
|
||||
`;
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import {
|
||||
ConnectorState,
|
||||
ConnectorStatus,
|
||||
ConsumerGroup,
|
||||
ConsumerGroupState,
|
||||
TaskStatus,
|
||||
} from 'generated-sources';
|
||||
import { ConnectorState, ConsumerGroupState } from 'generated-sources';
|
||||
|
||||
const getTagColor = ({
|
||||
state,
|
||||
}: ConnectorStatus | TaskStatus | ConsumerGroup) => {
|
||||
const getTagColor = (state?: string) => {
|
||||
switch (state) {
|
||||
case ConnectorState.RUNNING:
|
||||
case ConsumerGroupState.STABLE:
|
||||
|
|
|
@ -21,7 +21,3 @@ const tableLinkMixin = css(
|
|||
export const TableKeyLink = styled.td`
|
||||
${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>(
|
||||
({ isOrderable, isOrdered, sortOrder, theme: { table } }) => css`
|
||||
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 = {
|
||||
groupId: 'amazon.msk.canary.group.broker-1',
|
||||
members: 0,
|
||||
|
|
|
@ -447,22 +447,6 @@ const theme = {
|
|||
},
|
||||
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: {
|
||||
unchecked: Colors.brand[20],
|
||||
checked: Colors.brand[50],
|
||||
|
|
Loading…
Add table
Reference in a new issue