mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +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:
parent
2aea6e688c
commit
f6c05eb877
@ -25,8 +25,6 @@ export const ApiTokenPage = () => {
|
||||
const { deleteToken } = useApiTokensApi();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
@ -103,8 +101,6 @@ export const ApiTokenPage = () => {
|
||||
headerGroups={headerGroups}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
prepareRow={prepareRow}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
getTableProps={getTableProps}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
globalFilter={globalFilter}
|
||||
|
@ -22,13 +22,13 @@ describe('ProjectsList', () => {
|
||||
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 />);
|
||||
|
||||
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={[]} />);
|
||||
|
||||
expect(container.textContent).toEqual('*');
|
||||
@ -43,4 +43,16 @@ describe('ProjectsList', () => {
|
||||
|
||||
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('*');
|
||||
});
|
||||
});
|
||||
|
@ -59,30 +59,25 @@ export const ProjectsList: FC<IProjectsListProps> = ({ projects, project }) => {
|
||||
}
|
||||
|
||||
if (
|
||||
(projectsList.length === 1 && projectsList[0] === '*') ||
|
||||
project === '*' ||
|
||||
(!project && (!projectsList || projectsList.length === 0))
|
||||
(projectsList.length === 1 && projectsList[0] !== '*') ||
|
||||
(project && project !== '*')
|
||||
) {
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title='ALL current and future projects'
|
||||
placement='bottom'
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<Highlighter search={searchQuery}>*</Highlighter>
|
||||
</span>
|
||||
</HtmlTooltip>
|
||||
</TextCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (projectsList.length === 1 || project) {
|
||||
const item = project || projectsList[0];
|
||||
|
||||
return <LinkCell to={`/projects/${item}`} title={item} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title='ALL current and future projects'
|
||||
placement='bottom'
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<Highlighter search={searchQuery}>*</Highlighter>
|
||||
</span>
|
||||
</HtmlTooltip>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
@ -1,24 +1,6 @@
|
||||
import type {
|
||||
Row,
|
||||
TablePropGetter,
|
||||
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 type { Row, HeaderGroup } from 'react-table';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { Box, useMediaQuery, Link } from '@mui/material';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { ApiTokenDocs } from 'component/admin/apiToken/ApiTokenDocs/ApiTokenDocs';
|
||||
|
||||
@ -27,7 +9,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
|
||||
const hiddenColumnsSmall = ['Icon', 'createdAt'];
|
||||
const hiddenColumnsNotExtraLarge = ['Icon', 'createdAt', 'seenAt'];
|
||||
const hiddenColumnsCompact = ['Icon', 'project', 'seenAt'];
|
||||
|
||||
interface IApiTokenTableProps {
|
||||
@ -37,34 +19,27 @@ interface IApiTokenTableProps {
|
||||
columns: any[];
|
||||
rows: Row<object>[];
|
||||
prepareRow: (row: Row<object>) => void;
|
||||
getTableProps: (
|
||||
propGetter?: TablePropGetter<object> | undefined,
|
||||
) => TableProps;
|
||||
getTableBodyProps: (
|
||||
propGetter?: TableBodyPropGetter<object> | undefined,
|
||||
) => TableBodyProps;
|
||||
headerGroups: HeaderGroup<object>[];
|
||||
globalFilter: any;
|
||||
}
|
||||
|
||||
export const ApiTokenTable = ({
|
||||
compact = false,
|
||||
setHiddenColumns,
|
||||
columns,
|
||||
loading,
|
||||
rows,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
globalFilter,
|
||||
prepareRow,
|
||||
}: IApiTokenTableProps) => {
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isNotExtraLarge = useMediaQuery(theme.breakpoints.down('xl'));
|
||||
|
||||
useConditionallyHiddenColumns(
|
||||
[
|
||||
{
|
||||
condition: isSmallScreen,
|
||||
columns: hiddenColumnsSmall,
|
||||
condition: isNotExtraLarge,
|
||||
columns: hiddenColumnsNotExtraLarge,
|
||||
},
|
||||
{
|
||||
condition: compact,
|
||||
@ -87,25 +62,11 @@ export const ApiTokenTable = ({
|
||||
/>
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<SearchHighlightProvider value={globalFilter}>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader
|
||||
headerGroups={headerGroups as any}
|
||||
/>
|
||||
<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>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
</Box>
|
||||
<ConditionallyRender
|
||||
|
@ -4,7 +4,12 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||
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 { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
|
||||
import Key from '@mui/icons-material/Key';
|
||||
@ -22,15 +27,16 @@ export const useApiTokenTable = (
|
||||
return [
|
||||
{
|
||||
id: 'Icon',
|
||||
width: '1%',
|
||||
Cell: () => <IconCell icon={<Key color='disabled' />} />,
|
||||
disableSortBy: true,
|
||||
disableGlobalFilter: true,
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
Header: 'Username',
|
||||
accessor: 'username',
|
||||
Cell: HighlightCell,
|
||||
minWidth: 35,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
@ -43,9 +49,10 @@ export const useApiTokenTable = (
|
||||
<HighlightCell
|
||||
value={tokenDescriptions[value.toLowerCase()].label}
|
||||
subtitle={tokenDescriptions[value.toLowerCase()].title}
|
||||
subtitleTooltip
|
||||
/>
|
||||
),
|
||||
minWidth: 280,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
Header: 'Project',
|
||||
@ -56,33 +63,33 @@ export const useApiTokenTable = (
|
||||
projects={props.row.original.projects}
|
||||
/>
|
||||
),
|
||||
minWidth: 120,
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
Header: 'Environment',
|
||||
accessor: 'environment',
|
||||
Cell: HighlightCell,
|
||||
minWidth: 120,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
minWidth: 150,
|
||||
width: 150,
|
||||
disableGlobalFilter: true,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'seenAt',
|
||||
Cell: TimeAgoCell,
|
||||
minWidth: 150,
|
||||
width: 140,
|
||||
disableGlobalFilter: true,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
width: 120,
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
width: '1%',
|
||||
disableSortBy: true,
|
||||
disableGlobalFilter: true,
|
||||
Cell: getActionCell,
|
||||
@ -110,6 +117,7 @@ export const useApiTokenTable = (
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useFlexLayout,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type React from 'react';
|
||||
import type { VFC } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { Box, styled } from '@mui/material';
|
||||
@ -10,6 +10,7 @@ interface IHighlightCellProps {
|
||||
value: string;
|
||||
subtitle?: string;
|
||||
afterTitle?: React.ReactNode;
|
||||
subtitleTooltip?: boolean;
|
||||
}
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
@ -40,16 +41,19 @@ const StyledSubtitle = styled('span')(({ theme }) => ({
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}));
|
||||
|
||||
export const HighlightCell: VFC<IHighlightCellProps> = ({
|
||||
export const HighlightCell: FC<IHighlightCellProps> = ({
|
||||
value,
|
||||
subtitle,
|
||||
afterTitle,
|
||||
subtitleTooltip,
|
||||
}) => {
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const renderSubtitle = (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(subtitle && subtitle.length > 40)}
|
||||
condition={Boolean(
|
||||
subtitle && (subtitle.length > 40 || subtitleTooltip),
|
||||
)}
|
||||
show={
|
||||
<HtmlTooltip title={subtitle} placement='bottom-start' arrow>
|
||||
<StyledSubtitle data-loading>
|
||||
|
@ -117,8 +117,6 @@ export const ProjectApiAccess = () => {
|
||||
headerGroups={headerGroups}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
prepareRow={prepareRow}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
getTableProps={getTableProps}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
globalFilter={globalFilter}
|
||||
|
Loading…
Reference in New Issue
Block a user