1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

refactor: port tokens list to react-table (#1026)

* refactor: extract ApiTokenDocs component

* refactor: extract CreateApiTokenButton component

* refactor: extract RemoveApiTokenButton component

* refactor: extract CopyApiTokenButton component

* refactor: port tokens list to react-table

* refactor: remove unused imports

* fix: api token table default sort order

* fix: updates to table of api tokens

* fix: add highlighting when searching

Co-authored-by: Tymoteusz Czech <tymek+gpg@getunleash.ai>
Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
olav 2022-05-27 09:48:01 +02:00 committed by GitHub
parent 504a4af274
commit 25c25c9206
17 changed files with 423 additions and 415 deletions

View File

@ -1,11 +1,10 @@
import { useMemo } from 'react';
import { Table, TableBody, TableCell, TableRow } from 'component/common/Table'; import { Table, TableBody, TableCell, TableRow } from 'component/common/Table';
import { useMemo, useState, useCallback } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import useAddons from 'hooks/api/getters/useAddons/useAddons'; import useAddons from 'hooks/api/getters/useAddons/useAddons';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
import { useState, useCallback } from 'react';
import { IAddon } from 'interfaces/addons'; import { IAddon } from 'interfaces/addons';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';

View File

@ -0,0 +1,27 @@
import { Alert } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
export const ApiTokenDocs = () => {
const { uiConfig } = useUiConfig();
return (
<Alert severity="info">
<p>
Read the{' '}
<a
href="https://docs.getunleash.io/docs"
target="_blank"
rel="noreferrer"
>
Getting started guide
</a>{' '}
to learn how to connect to the Unleash API from your application
or programmatically. Please note it can take up to 1 minute
before a new API key is activated.
</p>
<br />
<strong>API URL: </strong>{' '}
<pre style={{ display: 'inline' }}>{uiConfig.unleashUrl}/api/</pre>
</Alert>
);
};

View File

@ -7,7 +7,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { useStyles } from './ApiTokenForm.styles'; import { useStyles } from './ApiTokenForm.styles';
import { SelectProjectInput } from './SelectProjectInput/SelectProjectInput'; import { SelectProjectInput } from './SelectProjectInput/SelectProjectInput';
import { ApiTokenFormErrorType } from '../hooks/useApiTokenForm'; import { ApiTokenFormErrorType } from 'component/admin/apiToken/ApiTokenForm/useApiTokenForm';
interface IApiTokenFormProps { interface IApiTokenFormProps {
username: string; username: string;
type: string; type: string;

View File

@ -1,45 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
tableRow: {
'&:hover': {
backgroundColor: theme.palette.grey[200],
},
},
container: {
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.down('sm')]: {
justifyContent: 'center',
},
},
center: {
textAlign: 'center',
},
actionsContainer: {
textAlign: 'center',
display: 'flex-inline',
flexWrap: 'nowrap',
},
hideSM: {
[theme.breakpoints.down('md')]: {
display: 'none',
},
},
hideMD: {
[theme.breakpoints.down('lg')]: {
display: 'none',
},
},
hideXS: {
[theme.breakpoints.down('sm')]: {
display: 'none',
},
},
token: {
textAlign: 'left',
[theme.breakpoints.up('sm')]: {
display: 'none',
},
},
}));

View File

@ -1,264 +0,0 @@
import { useContext, useState } from 'react';
import {
Box,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
} from '@mui/material';
import AccessContext from 'contexts/AccessContext';
import useToast from 'hooks/useToast';
import useLoading from 'hooks/useLoading';
import useApiTokens from 'hooks/api/getters/useApiTokens/useApiTokens';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import ApiError from 'component/common/ApiError/ApiError';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DELETE_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { Delete, FileCopy } from '@mui/icons-material';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import copy from 'copy-to-clipboard';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate';
import { ProjectsList } from './ProjectsList/ProjectsList';
import { useStyles } from './ApiTokenList.styles';
interface IApiToken {
createdAt: Date;
username: string;
secret: string;
type: string;
project?: string;
projects?: string | string[];
environment: string;
}
export const ApiTokenList = () => {
const { classes: styles } = useStyles();
const { hasAccess } = useContext(AccessContext);
const { uiConfig } = useUiConfig();
const [showDelete, setShowDelete] = useState(false);
const [delToken, setDeleteToken] = useState<IApiToken>();
const { locationSettings } = useLocationSettings();
const { setToastData } = useToast();
const { tokens, loading, refetch, error } = useApiTokens();
const { deleteToken } = useApiTokensApi();
const ref = useLoading(loading);
const renderError = () => {
return <ApiError onClick={refetch} text="Error fetching api tokens" />;
};
const copyToken = (value: string) => {
if (copy(value)) {
setToastData({
type: 'success',
title: 'Token copied',
text: `Token is copied to clipboard`,
});
}
};
const onDeleteToken = async () => {
if (delToken) {
await deleteToken(delToken.secret);
}
setDeleteToken(undefined);
setShowDelete(false);
refetch();
setToastData({
type: 'success',
title: 'Deleted successfully',
text: 'Successfully deleted API token.',
});
};
const renderApiTokens = (tokens: IApiToken[]) => {
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell className={styles.hideSM}>Created</TableCell>
<TableCell className={styles.hideSM}>
Username
</TableCell>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
Type
</TableCell>
<ConditionallyRender
condition={uiConfig.flags.E}
show={
<>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
Projects
</TableCell>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
Environment
</TableCell>
</>
}
/>
<TableCell className={styles.hideMD}>Secret</TableCell>
<TableCell className={styles.token}>Token</TableCell>
<TableCell className={styles.actionsContainer}>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tokens.map(item => {
return (
<TableRow
key={item.secret}
className={styles.tableRow}
>
<TableCell
align="left"
className={styles.hideSM}
>
{formatDateYMD(
item.createdAt,
locationSettings.locale
)}
</TableCell>
<TableCell
align="left"
className={styles.hideSM}
>
{item.username}
</TableCell>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
{item.type}
</TableCell>
<ConditionallyRender
condition={uiConfig.flags.E}
show={
<>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
<ProjectsList
project={item.project}
projects={item.projects}
/>
</TableCell>
<TableCell
className={`${styles.center} ${styles.hideXS}`}
>
{item.environment}
</TableCell>
<TableCell className={styles.token}>
<b>Type:</b> {item.type}
<br />
<b>Env:</b> {item.environment}
<br />
<b>Projects:</b>{' '}
<ProjectsList
project={item.project}
projects={item.projects}
/>
</TableCell>
</>
}
elseShow={
<>
<TableCell className={styles.token}>
<b>Type:</b> {item.type}
<br />
<b>Username:</b> {item.username}
</TableCell>
</>
}
/>
<TableCell className={styles.hideMD}>
<Box
component="span"
display="inline-block"
width="250px"
>
************************************
</Box>
</TableCell>
<TableCell className={styles.actionsContainer}>
<Tooltip title="Copy token" arrow>
<IconButton
onClick={() => {
copyToken(item.secret);
}}
size="large"
>
<FileCopy />
</IconButton>
</Tooltip>
<ConditionallyRender
condition={hasAccess(DELETE_API_TOKEN)}
show={
<Tooltip title="Delete token" arrow>
<IconButton
onClick={() => {
setDeleteToken(item);
setShowDelete(true);
}}
size="large"
>
<Delete />
</IconButton>
</Tooltip>
}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
return (
<div ref={ref}>
<ConditionallyRender condition={error} show={renderError()} />
<div className={styles.container}>
<ConditionallyRender
condition={tokens.length < 1 && !loading}
show={<div>No API tokens available.</div>}
elseShow={renderApiTokens(tokens)}
/>
</div>
<Dialogue
open={showDelete}
onClick={onDeleteToken}
onClose={() => {
setShowDelete(false);
setDeleteToken(undefined);
}}
title="Confirm deletion"
>
<div>
Are you sure you want to delete the following API token?
<br />
<ul>
<li>
<strong>username</strong>:{' '}
<code>{delToken?.username}</code>
</li>
<li>
<strong>type</strong>: <code>{delToken?.type}</code>
</li>
</ul>
</div>
</Dialogue>
</div>
);
};

View File

@ -1,7 +0,0 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
infoBoxContainer: {
marginBottom: 40,
},
}));

View File

@ -1,77 +1,18 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@mui/material';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { import { READ_API_TOKEN } from 'component/providers/AccessProvider/permissions';
CREATE_API_TOKEN,
READ_API_TOKEN,
} from 'component/providers/AccessProvider/permissions';
import { useStyles } from './ApiTokenPage.styles';
import { CREATE_API_TOKEN_BUTTON } from 'utils/testIds';
import { Alert } from '@mui/material';
import { ApiTokenList } from 'component/admin/apiToken/ApiTokenList/ApiTokenList';
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
import { ApiTokenTable } from 'component/admin/apiToken/ApiTokenTable/ApiTokenTable';
export const ApiTokenPage = () => { export const ApiTokenPage = () => {
const { classes: styles } = useStyles();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { uiConfig } = useUiConfig();
const navigate = useNavigate();
return ( return (
<PageContent <ConditionallyRender
header={ condition={hasAccess(READ_API_TOKEN)}
<PageHeader show={() => <ApiTokenTable />}
title="API Access" elseShow={() => <AdminAlert />}
actions={ />
<ConditionallyRender
condition={hasAccess(CREATE_API_TOKEN)}
show={
<Button
variant="contained"
color="primary"
onClick={() =>
navigate('/admin/api/create-token')
}
data-testid={CREATE_API_TOKEN_BUTTON}
>
New API token
</Button>
}
/>
}
/>
}
>
<Alert severity="info" className={styles.infoBoxContainer}>
<p>
Read the{' '}
<a
href="https://docs.getunleash.io/docs"
target="_blank"
rel="noreferrer"
>
Getting started guide
</a>{' '}
to learn how to connect to the Unleash API from your
application or programmatically. Please note it can take up
to 1 minute before a new API key is activated.
</p>
<br />
<strong>API URL: </strong>{' '}
<pre style={{ display: 'inline' }}>
{uiConfig.unleashUrl}/api/
</pre>
</Alert>
<ConditionallyRender
condition={hasAccess(READ_API_TOKEN)}
show={() => <ApiTokenList />}
elseShow={() => <AdminAlert />}
/>
</PageContent>
); );
}; };

View File

@ -0,0 +1,219 @@
import { useApiTokens } from 'hooks/api/getters/useApiTokens/useApiTokens';
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { PageContent } from 'component/common/PageContent/PageContent';
import {
SortableTableHeader,
TableSearch,
TableCell,
TablePlaceholder,
} from 'component/common/Table';
import { Table, TableBody, Box, TableRow, useMediaQuery } from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { ApiTokenDocs } from 'component/admin/apiToken/ApiTokenDocs/ApiTokenDocs';
import { CreateApiTokenButton } from 'component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Key } from '@mui/icons-material';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { CopyApiTokenButton } from 'component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton';
import { RemoveApiTokenButton } from 'component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { sortTypes } from 'utils/sortTypes';
import { useEffect, useMemo } from 'react';
import theme from 'themes/theme';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
export const ApiTokenTable = () => {
const { tokens } = useApiTokens();
const hiddenColumns = useHiddenColumns();
const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state: { globalFilter },
setGlobalFilter,
setHiddenColumns,
} = useTable(
{
columns: COLUMNS as any,
data: tokens as any,
initialState,
sortTypes,
disableSortRemove: true,
},
useGlobalFilter,
useSortBy
);
useEffect(() => {
setHiddenColumns(hiddenColumns);
}, [setHiddenColumns, hiddenColumns]);
const headerSearch = (
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
);
const headerActions = (
<>
{headerSearch}
<PageHeader.Divider />
<CreateApiTokenButton />
</>
);
if (!tokens.length) {
return null;
}
return (
<PageContent
header={
<PageHeader
title={`API access (${rows.length})`}
actions={headerActions}
/>
}
>
<Box sx={{ mb: 4 }}>
<ApiTokenDocs />
</Box>
<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>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No tokens found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No tokens available. Get started by adding one.
</TablePlaceholder>
}
/>
}
/>
</PageContent>
);
};
const useHiddenColumns = (): string[] => {
const { uiConfig } = useUiConfig();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
return useMemo(() => {
const hidden: string[] = [];
if (!uiConfig.flags.E) {
hidden.push('projects');
hidden.push('environment');
}
if (isMediumScreen) {
hidden.push('Icon');
hidden.push('createdAt');
}
if (isSmallScreen) {
hidden.push('username');
}
return hidden;
}, [uiConfig, isSmallScreen, isMediumScreen]);
};
const COLUMNS = [
{
id: 'Icon',
width: '1%',
Cell: () => <IconCell icon={<Key color="disabled" />} />,
disableSortBy: true,
disableGlobalFilter: true,
},
{
Header: 'Username',
accessor: 'username',
Cell: HighlightCell,
width: '60%',
},
{
Header: 'Type',
accessor: 'type',
Cell: ({ value }: { value: string }) => (
<HighlightCell value={value.toUpperCase()} />
),
minWidth: 100,
},
{
Header: 'Project',
accessor: 'project',
Cell: (props: any) => (
<TextCell>
<ProjectsList
project={props.row.original.project}
projects={props.row.original.projects}
/>
</TextCell>
),
minWidth: 120,
},
{
Header: 'Environment',
accessor: 'environment',
Cell: HighlightCell,
minWidth: 120,
},
{
Header: 'Created',
accessor: 'createdAt',
Cell: DateCell,
minWidth: 150,
disableGlobalFilter: true,
},
{
Header: 'Actions',
id: 'Actions',
align: 'center',
width: '1%',
disableSortBy: true,
disableGlobalFilter: true,
Cell: (props: any) => (
<ActionCell>
<CopyApiTokenButton token={props.row.original} />
<RemoveApiTokenButton token={props.row.original} />
</ActionCell>
),
},
];

View File

@ -0,0 +1,30 @@
import { IconButton, Tooltip } from '@mui/material';
import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens';
import useToast from 'hooks/useToast';
import copy from 'copy-to-clipboard';
import { FileCopy } from '@mui/icons-material';
interface ICopyApiTokenButtonProps {
token: IApiToken;
}
export const CopyApiTokenButton = ({ token }: ICopyApiTokenButtonProps) => {
const { setToastData } = useToast();
const copyToken = (value: string) => {
if (copy(value)) {
setToastData({
type: 'success',
title: `Token copied to clipboard`,
});
}
};
return (
<Tooltip title="Copy token" arrow>
<IconButton onClick={() => copyToken(token.secret)} size="large">
<FileCopy />
</IconButton>
</Tooltip>
);
};

View File

@ -5,7 +5,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi'; import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { useApiTokenForm } from '../hooks/useApiTokenForm'; import { useApiTokenForm } from 'component/admin/apiToken/ApiTokenForm/useApiTokenForm';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConfirmToken } from '../ConfirmToken/ConfirmToken'; import { ConfirmToken } from '../ConfirmToken/ConfirmToken';
import { useState } from 'react'; import { useState } from 'react';

View File

@ -0,0 +1,21 @@
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { CREATE_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { CREATE_API_TOKEN_BUTTON } from 'utils/testIds';
import { useNavigate } from 'react-router-dom';
import { Add } from '@mui/icons-material';
export const CreateApiTokenButton = () => {
const navigate = useNavigate();
return (
<ResponsiveButton
Icon={Add}
onClick={() => navigate('/admin/api/create-token')}
data-testid={CREATE_API_TOKEN_BUTTON}
permission={CREATE_API_TOKEN}
maxWidth="700px"
>
New API token
</ResponsiveButton>
);
};

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { ProjectsList } from './ProjectsList'; import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
describe('ProjectsList', () => { describe('ProjectsList', () => {
it('should prioritize new "projects" array over deprecated "project"', async () => { it('should prioritize new "projects" array over deprecated "project"', async () => {

View File

@ -1,4 +1,6 @@
import React, { Fragment, VFC } from 'react'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { Fragment, VFC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface IProjectsListProps { interface IProjectsListProps {
@ -10,6 +12,8 @@ export const ProjectsList: VFC<IProjectsListProps> = ({
projects, projects,
project, project,
}) => { }) => {
const { searchQuery } = useSearchHighlightContext();
let fields: string[] = let fields: string[] =
projects && Array.isArray(projects) projects && Array.isArray(projects)
? projects ? projects
@ -18,7 +22,7 @@ export const ProjectsList: VFC<IProjectsListProps> = ({
: []; : [];
if (fields.length === 0) { if (fields.length === 0) {
return <>*</>; return <Highlighter search={searchQuery}>*</Highlighter>;
} }
return ( return (
@ -27,9 +31,13 @@ export const ProjectsList: VFC<IProjectsListProps> = ({
<Fragment key={item}> <Fragment key={item}>
{index > 0 && ', '} {index > 0 && ', '}
{!item || item === '*' ? ( {!item || item === '*' ? (
'*' <Highlighter search={searchQuery}>*</Highlighter>
) : ( ) : (
<Link to={`/projects/${item}`}>{item}</Link> <Link to={`/projects/${item}`}>
<Highlighter search={searchQuery}>
{item}
</Highlighter>
</Link>
)} )}
</Fragment> </Fragment>
))} ))}

View File

@ -0,0 +1,70 @@
import { DELETE_API_TOKEN } from 'component/providers/AccessProvider/permissions';
import { Delete } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IconButton, Tooltip } from '@mui/material';
import {
IApiToken,
useApiTokens,
} from 'hooks/api/getters/useApiTokens/useApiTokens';
import AccessContext from 'contexts/AccessContext';
import { useContext, useState } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useToast from 'hooks/useToast';
import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
interface IRemoveApiTokenButtonProps {
token: IApiToken;
}
export const RemoveApiTokenButton = ({ token }: IRemoveApiTokenButtonProps) => {
const { hasAccess } = useContext(AccessContext);
const { deleteToken } = useApiTokensApi();
const [open, setOpen] = useState(false);
const { setToastData } = useToast();
const { refetch } = useApiTokens();
const onRemove = async () => {
await deleteToken(token.secret);
setOpen(false);
refetch();
setToastData({
type: 'success',
title: 'API token removed',
});
};
return (
<>
<ConditionallyRender
condition={hasAccess(DELETE_API_TOKEN)}
show={
<Tooltip title="Delete token" arrow>
<IconButton onClick={() => setOpen(true)} size="large">
<Delete />
</IconButton>
</Tooltip>
}
/>
<Dialogue
open={open}
onClick={onRemove}
onClose={() => setOpen(false)}
title="Confirm deletion"
>
<div>
Are you sure you want to delete the following API token?
<br />
<ul>
<li>
<strong>username</strong>:{' '}
<code>{token.username}</code>
</li>
<li>
<strong>type</strong>: <code>{token.type}</code>
</li>
</ul>
</div>
</Dialogue>
</>
);
};

View File

@ -1,36 +1,40 @@
import useSWR, { mutate, SWRConfiguration } from 'swr'; import useSWR, { SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react'; import { useCallback, useMemo } from 'react';
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
const useApiTokens = (options: SWRConfiguration = {}) => { export interface IApiToken {
const fetcher = async () => { createdAt: Date;
const path = formatApiPath(`api/admin/api-tokens`); username: string;
const res = await fetch(path, { secret: string;
method: 'GET', type: string;
}).then(handleErrorResponses('Api tokens')); project?: string;
return res.json(); projects?: string | string[];
}; environment: string;
}
const KEY = `api/admin/api-tokens`; export const useApiTokens = (options: SWRConfiguration = {}) => {
const path = formatApiPath(`api/admin/api-tokens`);
const { data, error, mutate } = useSWR<IApiToken[]>(path, fetcher, options);
const { data, error } = useSWR(KEY, fetcher, options); const tokens = useMemo(() => {
const [loading, setLoading] = useState(!error && !data); return data ?? [];
}, [data]);
const refetch = () => { const refetch = useCallback(() => {
mutate(KEY); mutate().catch(console.warn);
}; }, [mutate]);
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return { return {
tokens: data?.tokens || [], tokens,
error, error,
loading, loading: !error && !data,
refetch, refetch,
}; };
}; };
export default useApiTokens; const fetcher = async (path: string): Promise<IApiToken[]> => {
const res = await fetch(path).then(handleErrorResponses('Api tokens'));
const data = await res.json();
return data.tokens;
};

View File

@ -3,6 +3,7 @@ import { formatApiPath } from 'utils/formatPath';
import { defaultValue } from './defaultValue'; import { defaultValue } from './defaultValue';
import { IUiConfig } from 'interfaces/uiConfig'; import { IUiConfig } from 'interfaces/uiConfig';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { useMemo } from 'react';
const REQUEST_KEY = 'api/admin/ui-config'; const REQUEST_KEY = 'api/admin/ui-config';
@ -33,8 +34,12 @@ const useUiConfig = (options: SWRConfiguration = {}) => {
return true; return true;
}; };
const uiConfig = useMemo(() => {
return { ...defaultValue, ...data };
}, [data]);
return { return {
uiConfig: { ...defaultValue, ...data }, uiConfig,
loading: !error && !data, loading: !error && !data,
error, error,
refetch, refetch,