From 25c25c9206d5d7ded834d2f5777754cddbbb1138 Mon Sep 17 00:00:00 2001 From: olav Date: Fri, 27 May 2022 09:48:01 +0200 Subject: [PATCH] refactor: port tokens list to react-table (#1026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Nuno Góis --- .../ConfiguredAddons/ConfiguredAddons.tsx | 3 +- .../apiToken/ApiTokenDocs/ApiTokenDocs.tsx | 27 ++ .../apiToken/ApiTokenForm/ApiTokenForm.tsx | 2 +- .../useApiTokenForm.ts | 0 .../ApiTokenList/ApiTokenList.styles.ts | 45 --- .../apiToken/ApiTokenList/ApiTokenList.tsx | 264 ------------------ .../ApiTokenPage/ApiTokenPage.styles.ts | 7 - .../apiToken/ApiTokenPage/ApiTokenPage.tsx | 73 +---- .../apiToken/ApiTokenTable/ApiTokenTable.tsx | 219 +++++++++++++++ .../CopyApiTokenButton/CopyApiTokenButton.tsx | 30 ++ .../CreateApiToken/CreateApiToken.tsx | 2 +- .../CreateApiTokenButton.tsx | 21 ++ .../ProjectsList/ProjectsList.test.tsx | 2 +- .../ProjectsList/ProjectsList.tsx | 16 +- .../RemoveApiTokenButton.tsx | 70 +++++ .../api/getters/useApiTokens/useApiTokens.ts | 50 ++-- .../api/getters/useUiConfig/useUiConfig.ts | 7 +- 17 files changed, 423 insertions(+), 415 deletions(-) create mode 100644 frontend/src/component/admin/apiToken/ApiTokenDocs/ApiTokenDocs.tsx rename frontend/src/component/admin/apiToken/{hooks => ApiTokenForm}/useApiTokenForm.ts (100%) delete mode 100644 frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.styles.ts delete mode 100644 frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.tsx delete mode 100644 frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.styles.ts create mode 100644 frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx create mode 100644 frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx create mode 100644 frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx rename frontend/src/component/admin/apiToken/{ApiTokenList => }/ProjectsList/ProjectsList.test.tsx (95%) rename frontend/src/component/admin/apiToken/{ApiTokenList => }/ProjectsList/ProjectsList.tsx (52%) create mode 100644 frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx index f7ddb64cee..b80b2d2a08 100644 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx @@ -1,11 +1,10 @@ -import { useMemo } from 'react'; import { Table, TableBody, TableCell, TableRow } from 'component/common/Table'; +import { useMemo, useState, useCallback } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; import useAddons from 'hooks/api/getters/useAddons/useAddons'; import useToast from 'hooks/useToast'; import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; -import { useState, useCallback } from 'react'; import { IAddon } from 'interfaces/addons'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { formatUnknownError } from 'utils/formatUnknownError'; diff --git a/frontend/src/component/admin/apiToken/ApiTokenDocs/ApiTokenDocs.tsx b/frontend/src/component/admin/apiToken/ApiTokenDocs/ApiTokenDocs.tsx new file mode 100644 index 0000000000..5867df4ace --- /dev/null +++ b/frontend/src/component/admin/apiToken/ApiTokenDocs/ApiTokenDocs.tsx @@ -0,0 +1,27 @@ +import { Alert } from '@mui/material'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; + +export const ApiTokenDocs = () => { + const { uiConfig } = useUiConfig(); + + return ( + +

+ Read the{' '} + + Getting started guide + {' '} + 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. +

+
+ API URL: {' '} +
{uiConfig.unleashUrl}/api/
+
+ ); +}; diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/ApiTokenForm.tsx b/frontend/src/component/admin/apiToken/ApiTokenForm/ApiTokenForm.tsx index 6b14065814..ac28e641fb 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenForm/ApiTokenForm.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenForm/ApiTokenForm.tsx @@ -7,7 +7,7 @@ import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import Input from 'component/common/Input/Input'; import { useStyles } from './ApiTokenForm.styles'; import { SelectProjectInput } from './SelectProjectInput/SelectProjectInput'; -import { ApiTokenFormErrorType } from '../hooks/useApiTokenForm'; +import { ApiTokenFormErrorType } from 'component/admin/apiToken/ApiTokenForm/useApiTokenForm'; interface IApiTokenFormProps { username: string; type: string; diff --git a/frontend/src/component/admin/apiToken/hooks/useApiTokenForm.ts b/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts similarity index 100% rename from frontend/src/component/admin/apiToken/hooks/useApiTokenForm.ts rename to frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts diff --git a/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.styles.ts b/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.styles.ts deleted file mode 100644 index 4b86ac8b9d..0000000000 --- a/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.styles.ts +++ /dev/null @@ -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', - }, - }, -})); diff --git a/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.tsx b/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.tsx deleted file mode 100644 index 99694f8569..0000000000 --- a/frontend/src/component/admin/apiToken/ApiTokenList/ApiTokenList.tsx +++ /dev/null @@ -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(); - const { locationSettings } = useLocationSettings(); - const { setToastData } = useToast(); - const { tokens, loading, refetch, error } = useApiTokens(); - const { deleteToken } = useApiTokensApi(); - const ref = useLoading(loading); - - const renderError = () => { - return ; - }; - - 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 ( - - - - Created - - Username - - - Type - - - - Projects - - - Environment - - - } - /> - Secret - Token - - Actions - - - - - {tokens.map(item => { - return ( - - - {formatDateYMD( - item.createdAt, - locationSettings.locale - )} - - - {item.username} - - - {item.type} - - - - - - - {item.environment} - - - Type: {item.type} -
- Env: {item.environment} -
- Projects:{' '} - -
- - } - elseShow={ - <> - - Type: {item.type} -
- Username: {item.username} -
- - } - /> - - - ************************************ - - - - - { - copyToken(item.secret); - }} - size="large" - > - - - - - { - setDeleteToken(item); - setShowDelete(true); - }} - size="large" - > - - - - } - /> - -
- ); - })} -
-
- ); - }; - - return ( -
- -
- No API tokens available.
} - elseShow={renderApiTokens(tokens)} - /> -
- { - setShowDelete(false); - setDeleteToken(undefined); - }} - title="Confirm deletion" - > -
- Are you sure you want to delete the following API token? -
-
    -
  • - username:{' '} - {delToken?.username} -
  • -
  • - type: {delToken?.type} -
  • -
-
-
- - ); -}; diff --git a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.styles.ts b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.styles.ts deleted file mode 100644 index eb0c802bc2..0000000000 --- a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.styles.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { makeStyles } from 'tss-react/mui'; - -export const useStyles = makeStyles()(theme => ({ - infoBoxContainer: { - marginBottom: 40, - }, -})); diff --git a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx index 7ff0ca1aa0..f5d20b261d 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx @@ -1,77 +1,18 @@ import { useContext } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Button } from '@mui/material'; 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 { - 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 { READ_API_TOKEN } from 'component/providers/AccessProvider/permissions'; import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; +import { ApiTokenTable } from 'component/admin/apiToken/ApiTokenTable/ApiTokenTable'; export const ApiTokenPage = () => { - const { classes: styles } = useStyles(); const { hasAccess } = useContext(AccessContext); - const { uiConfig } = useUiConfig(); - const navigate = useNavigate(); return ( - - navigate('/admin/api/create-token') - } - data-testid={CREATE_API_TOKEN_BUTTON} - > - New API token - - } - /> - } - /> - } - > - -

- Read the{' '} - - Getting started guide - {' '} - 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. -

-
- API URL: {' '} -
-                    {uiConfig.unleashUrl}/api/
-                
-
- } - elseShow={() => } - /> -
+ } + elseShow={() => } + /> ); }; diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx new file mode 100644 index 0000000000..48ecb219fc --- /dev/null +++ b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx @@ -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 = ( + + ); + + const headerActions = ( + <> + {headerSearch} + + + + ); + + if (!tokens.length) { + return null; + } + + return ( + + } + > + + + + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+ 0} + show={ + + No tokens found matching “ + {globalFilter} + ” + + } + elseShow={ + + No tokens available. Get started by adding one. + + } + /> + } + /> +
+ ); +}; + +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: () => } />, + disableSortBy: true, + disableGlobalFilter: true, + }, + { + Header: 'Username', + accessor: 'username', + Cell: HighlightCell, + width: '60%', + }, + { + Header: 'Type', + accessor: 'type', + Cell: ({ value }: { value: string }) => ( + + ), + minWidth: 100, + }, + { + Header: 'Project', + accessor: 'project', + Cell: (props: any) => ( + + + + ), + 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) => ( + + + + + ), + }, +]; diff --git a/frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx b/frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx new file mode 100644 index 0000000000..e34e3501fb --- /dev/null +++ b/frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx @@ -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 ( + + copyToken(token.secret)} size="large"> + + + + ); +}; diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx index fa9dafb73e..03430547e2 100644 --- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx +++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx @@ -5,7 +5,7 @@ import { CreateButton } from 'component/common/CreateButton/CreateButton'; import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; 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 { ConfirmToken } from '../ConfirmToken/ConfirmToken'; import { useState } from 'react'; diff --git a/frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx b/frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx new file mode 100644 index 0000000000..f1236fec3b --- /dev/null +++ b/frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx @@ -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 ( + navigate('/admin/api/create-token')} + data-testid={CREATE_API_TOKEN_BUTTON} + permission={CREATE_API_TOKEN} + maxWidth="700px" + > + New API token + + ); +}; diff --git a/frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.test.tsx b/frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.test.tsx similarity index 95% rename from frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.test.tsx rename to frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.test.tsx index 1e5eee6299..5341609dc7 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.test.tsx +++ b/frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render } from 'utils/testRenderer'; import { screen } from '@testing-library/react'; -import { ProjectsList } from './ProjectsList'; +import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; describe('ProjectsList', () => { it('should prioritize new "projects" array over deprecated "project"', async () => { diff --git a/frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.tsx b/frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.tsx similarity index 52% rename from frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.tsx rename to frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.tsx index 7cca5b4b52..cbac134731 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenList/ProjectsList/ProjectsList.tsx +++ b/frontend/src/component/admin/apiToken/ProjectsList/ProjectsList.tsx @@ -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'; interface IProjectsListProps { @@ -10,6 +12,8 @@ export const ProjectsList: VFC = ({ projects, project, }) => { + const { searchQuery } = useSearchHighlightContext(); + let fields: string[] = projects && Array.isArray(projects) ? projects @@ -18,7 +22,7 @@ export const ProjectsList: VFC = ({ : []; if (fields.length === 0) { - return <>*; + return *; } return ( @@ -27,9 +31,13 @@ export const ProjectsList: VFC = ({ {index > 0 && ', '} {!item || item === '*' ? ( - '*' + * ) : ( - {item} + + + {item} + + )} ))} diff --git a/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx b/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx new file mode 100644 index 0000000000..38d47099b6 --- /dev/null +++ b/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx @@ -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 ( + <> + + setOpen(true)} size="large"> + + + + } + /> + setOpen(false)} + title="Confirm deletion" + > +
+ Are you sure you want to delete the following API token? +
+
    +
  • + username:{' '} + {token.username} +
  • +
  • + type: {token.type} +
  • +
+
+
+ + ); +}; diff --git a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts index 3cd5fbc5a3..962d6535fa 100644 --- a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts +++ b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts @@ -1,36 +1,40 @@ -import useSWR, { mutate, SWRConfiguration } from 'swr'; -import { useState, useEffect } from 'react'; +import useSWR, { SWRConfiguration } from 'swr'; +import { useCallback, useMemo } from 'react'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; -const useApiTokens = (options: SWRConfiguration = {}) => { - const fetcher = async () => { - const path = formatApiPath(`api/admin/api-tokens`); - const res = await fetch(path, { - method: 'GET', - }).then(handleErrorResponses('Api tokens')); - return res.json(); - }; +export interface IApiToken { + createdAt: Date; + username: string; + secret: string; + type: string; + project?: string; + 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(path, fetcher, options); - const { data, error } = useSWR(KEY, fetcher, options); - const [loading, setLoading] = useState(!error && !data); + const tokens = useMemo(() => { + return data ?? []; + }, [data]); - const refetch = () => { - mutate(KEY); - }; - - useEffect(() => { - setLoading(!error && !data); - }, [data, error]); + const refetch = useCallback(() => { + mutate().catch(console.warn); + }, [mutate]); return { - tokens: data?.tokens || [], + tokens, error, - loading, + loading: !error && !data, refetch, }; }; -export default useApiTokens; +const fetcher = async (path: string): Promise => { + const res = await fetch(path).then(handleErrorResponses('Api tokens')); + const data = await res.json(); + return data.tokens; +}; diff --git a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts index 17eda8bfa7..e5fa78c01c 100644 --- a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts +++ b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts @@ -3,6 +3,7 @@ import { formatApiPath } from 'utils/formatPath'; import { defaultValue } from './defaultValue'; import { IUiConfig } from 'interfaces/uiConfig'; import handleErrorResponses from '../httpErrorResponseHandler'; +import { useMemo } from 'react'; const REQUEST_KEY = 'api/admin/ui-config'; @@ -33,8 +34,12 @@ const useUiConfig = (options: SWRConfiguration = {}) => { return true; }; + const uiConfig = useMemo(() => { + return { ...defaultValue, ...data }; + }, [data]); + return { - uiConfig: { ...defaultValue, ...data }, + uiConfig, loading: !error && !data, error, refetch,