1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-14 01:16:17 +02:00

API tokens - virtualized table (#7531)

API tokens table in both main list and project API tokens list can now
support more items - it doesn't slow the browser down if there is >500
items.
This commit is contained in:
Tymoteusz Czech 2024-07-09 13:22:55 +02:00 committed by GitHub
parent 2aea6e688c
commit f6c05eb877
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 65 additions and 91 deletions

View File

@ -25,8 +25,6 @@ export const ApiTokenPage = () => {
const { deleteToken } = useApiTokensApi(); const { deleteToken } = useApiTokensApi();
const { const {
getTableProps,
getTableBodyProps,
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
@ -103,8 +101,6 @@ export const ApiTokenPage = () => {
headerGroups={headerGroups} headerGroups={headerGroups}
setHiddenColumns={setHiddenColumns} setHiddenColumns={setHiddenColumns}
prepareRow={prepareRow} prepareRow={prepareRow}
getTableBodyProps={getTableBodyProps}
getTableProps={getTableProps}
rows={rows} rows={rows}
columns={columns} columns={columns}
globalFilter={globalFilter} globalFilter={globalFilter}

View File

@ -22,13 +22,13 @@ describe('ProjectsList', () => {
expect(links[0]).toHaveTextContent('project'); expect(links[0]).toHaveTextContent('project');
}); });
it('should render asterisk if no projects are passed', async () => { it('should render "*" if no projects are passed', async () => {
const { container } = render(<ProjectsList />); const { container } = render(<ProjectsList />);
expect(container.textContent).toEqual('*'); expect(container.textContent).toEqual('*');
}); });
it('should render asterisk if empty projects array is passed', async () => { it('should render "*" if empty projects array is passed', async () => {
const { container } = render(<ProjectsList projects={[]} />); const { container } = render(<ProjectsList projects={[]} />);
expect(container.textContent).toEqual('*'); expect(container.textContent).toEqual('*');
@ -43,4 +43,16 @@ describe('ProjectsList', () => {
expect(container.textContent).toContain('4 projects'); expect(container.textContent).toContain('4 projects');
}); });
it('should render "*" if project is "*" and no projects are passed', async () => {
const { container } = render(<ProjectsList project='*' />);
expect(container.textContent).toEqual('*');
});
it('should render "*" if projects has only "*"', async () => {
const { container } = render(<ProjectsList projects={['*']} />);
expect(container.textContent).toEqual('*');
});
}); });

View File

@ -59,10 +59,14 @@ export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
} }
if ( if (
(projectsList.length === 1 && projectsList[0] === '*') || (projectsList.length === 1 && projectsList[0] !== '*') ||
project === '*' || (project && project !== '*')
(!project && (!projectsList || projectsList.length === 0))
) { ) {
const item = project || projectsList[0];
return <LinkCell to={`/projects/${item}`} title={item} />;
}
return ( return (
<TextCell> <TextCell>
<HtmlTooltip <HtmlTooltip
@ -76,13 +80,4 @@ export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
</HtmlTooltip> </HtmlTooltip>
</TextCell> </TextCell>
); );
}
if (projectsList.length === 1 || project) {
const item = project || projectsList[0];
return <LinkCell to={`/projects/${item}`} title={item} />;
}
return null;
}; };

View File

@ -1,24 +1,6 @@
import type { import type { Row, HeaderGroup } from 'react-table';
Row, import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
TablePropGetter, import { Box, useMediaQuery, Link } from '@mui/material';
TableProps,
TableBodyPropGetter,
TableBodyProps,
HeaderGroup,
} from 'react-table';
import {
SortableTableHeader,
TableCell,
TablePlaceholder,
} from 'component/common/Table';
import {
Box,
Table,
TableBody,
TableRow,
useMediaQuery,
Link,
} from '@mui/material';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { ApiTokenDocs } from 'component/admin/apiToken/ApiTokenDocs/ApiTokenDocs'; import { ApiTokenDocs } from 'component/admin/apiToken/ApiTokenDocs/ApiTokenDocs';
@ -27,7 +9,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
const hiddenColumnsSmall = ['Icon', 'createdAt']; const hiddenColumnsNotExtraLarge = ['Icon', 'createdAt', 'seenAt'];
const hiddenColumnsCompact = ['Icon', 'project', 'seenAt']; const hiddenColumnsCompact = ['Icon', 'project', 'seenAt'];
interface IApiTokenTableProps { interface IApiTokenTableProps {
@ -37,34 +19,27 @@ interface IApiTokenTableProps {
columns: any[]; columns: any[];
rows: Row<object>[]; rows: Row<object>[];
prepareRow: (row: Row<object>) => void; prepareRow: (row: Row<object>) => void;
getTableProps: (
propGetter?: TablePropGetter<object> | undefined,
) => TableProps;
getTableBodyProps: (
propGetter?: TableBodyPropGetter<object> | undefined,
) => TableBodyProps;
headerGroups: HeaderGroup<object>[]; headerGroups: HeaderGroup<object>[];
globalFilter: any; globalFilter: any;
} }
export const ApiTokenTable = ({ export const ApiTokenTable = ({
compact = false, compact = false,
setHiddenColumns, setHiddenColumns,
columns, columns,
loading, loading,
rows, rows,
getTableProps,
getTableBodyProps,
headerGroups, headerGroups,
globalFilter, globalFilter,
prepareRow, prepareRow,
}: IApiTokenTableProps) => { }: IApiTokenTableProps) => {
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isNotExtraLarge = useMediaQuery(theme.breakpoints.down('xl'));
useConditionallyHiddenColumns( useConditionallyHiddenColumns(
[ [
{ {
condition: isSmallScreen, condition: isNotExtraLarge,
columns: hiddenColumnsSmall, columns: hiddenColumnsNotExtraLarge,
}, },
{ {
condition: compact, condition: compact,
@ -87,25 +62,11 @@ export const ApiTokenTable = ({
/> />
<Box sx={{ overflowX: 'auto' }}> <Box sx={{ overflowX: 'auto' }}>
<SearchHighlightProvider value={globalFilter}> <SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}> <VirtualizedTable
<SortableTableHeader rows={rows}
headerGroups={headerGroups as any} headerGroups={headerGroups}
prepareRow={prepareRow}
/> />
<TableBody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider> </SearchHighlightProvider>
</Box> </Box>
<ConditionallyRender <ConditionallyRender

View File

@ -4,7 +4,12 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import {
useTable,
useGlobalFilter,
useSortBy,
useFlexLayout,
} from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
import Key from '@mui/icons-material/Key'; import Key from '@mui/icons-material/Key';
@ -22,15 +27,16 @@ export const useApiTokenTable = (
return [ return [
{ {
id: 'Icon', id: 'Icon',
width: '1%',
Cell: () => <IconCell icon={<Key color='disabled' />} />, Cell: () => <IconCell icon={<Key color='disabled' />} />,
disableSortBy: true, disableSortBy: true,
disableGlobalFilter: true, disableGlobalFilter: true,
width: 50,
}, },
{ {
Header: 'Username', Header: 'Username',
accessor: 'username', accessor: 'username',
Cell: HighlightCell, Cell: HighlightCell,
minWidth: 35,
}, },
{ {
Header: 'Type', Header: 'Type',
@ -43,9 +49,10 @@ export const useApiTokenTable = (
<HighlightCell <HighlightCell
value={tokenDescriptions[value.toLowerCase()].label} value={tokenDescriptions[value.toLowerCase()].label}
subtitle={tokenDescriptions[value.toLowerCase()].title} subtitle={tokenDescriptions[value.toLowerCase()].title}
subtitleTooltip
/> />
), ),
minWidth: 280, width: 180,
}, },
{ {
Header: 'Project', Header: 'Project',
@ -56,33 +63,33 @@ export const useApiTokenTable = (
projects={props.row.original.projects} projects={props.row.original.projects}
/> />
), ),
minWidth: 120, width: 160,
}, },
{ {
Header: 'Environment', Header: 'Environment',
accessor: 'environment', accessor: 'environment',
Cell: HighlightCell, Cell: HighlightCell,
minWidth: 120, width: 120,
}, },
{ {
Header: 'Created', Header: 'Created',
accessor: 'createdAt', accessor: 'createdAt',
Cell: DateCell, Cell: DateCell,
minWidth: 150, width: 150,
disableGlobalFilter: true, disableGlobalFilter: true,
}, },
{ {
Header: 'Last seen', Header: 'Last seen',
accessor: 'seenAt', accessor: 'seenAt',
Cell: TimeAgoCell, Cell: TimeAgoCell,
minWidth: 150, width: 140,
disableGlobalFilter: true, disableGlobalFilter: true,
}, },
{ {
Header: 'Actions', Header: 'Actions',
width: 120,
id: 'Actions', id: 'Actions',
align: 'center', align: 'center',
width: '1%',
disableSortBy: true, disableSortBy: true,
disableGlobalFilter: true, disableGlobalFilter: true,
Cell: getActionCell, Cell: getActionCell,
@ -110,6 +117,7 @@ export const useApiTokenTable = (
}, },
useGlobalFilter, useGlobalFilter,
useSortBy, useSortBy,
useFlexLayout,
); );
return { return {

View File

@ -1,5 +1,5 @@
import type React from 'react'; import type React from 'react';
import type { VFC } from 'react'; import type { FC } from 'react';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { Box, styled } from '@mui/material'; import { Box, styled } from '@mui/material';
@ -10,6 +10,7 @@ interface IHighlightCellProps {
value: string; value: string;
subtitle?: string; subtitle?: string;
afterTitle?: React.ReactNode; afterTitle?: React.ReactNode;
subtitleTooltip?: boolean;
} }
const StyledContainer = styled(Box)(({ theme }) => ({ const StyledContainer = styled(Box)(({ theme }) => ({
@ -40,16 +41,19 @@ const StyledSubtitle = styled('span')(({ theme }) => ({
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
})); }));
export const HighlightCell: VFC<IHighlightCellProps> = ({ export const HighlightCell: FC<IHighlightCellProps> = ({
value, value,
subtitle, subtitle,
afterTitle, afterTitle,
subtitleTooltip,
}) => { }) => {
const { searchQuery } = useSearchHighlightContext(); const { searchQuery } = useSearchHighlightContext();
const renderSubtitle = ( const renderSubtitle = (
<ConditionallyRender <ConditionallyRender
condition={Boolean(subtitle && subtitle.length > 40)} condition={Boolean(
subtitle && (subtitle.length > 40 || subtitleTooltip),
)}
show={ show={
<HtmlTooltip title={subtitle} placement='bottom-start' arrow> <HtmlTooltip title={subtitle} placement='bottom-start' arrow>
<StyledSubtitle data-loading> <StyledSubtitle data-loading>

View File

@ -117,8 +117,6 @@ export const ProjectApiAccess = () => {
headerGroups={headerGroups} headerGroups={headerGroups}
setHiddenColumns={setHiddenColumns} setHiddenColumns={setHiddenColumns}
prepareRow={prepareRow} prepareRow={prepareRow}
getTableBodyProps={getTableBodyProps}
getTableProps={getTableProps}
rows={rows} rows={rows}
columns={columns} columns={columns}
globalFilter={globalFilter} globalFilter={globalFilter}