mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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