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:
parent
2aea6e688c
commit
f6c05eb877
@ -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}
|
||||||
|
@ -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('*');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user