From f8c826450ecc981a52f8e1ed69e23f878c6ecc06 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 21 Feb 2023 14:27:46 +0100 Subject: [PATCH] Fix/decouple api token list (#3171) Decouples the API token list and adds tracking. --- frontend/src/component/admin/Admin.tsx | 4 +- frontend/src/component/admin/api/index.tsx | 11 - .../apiToken/ApiTokenPage/ApiTokenPage.tsx | 82 +++++- .../apiToken/ApiTokenTable/ApiTokenTable.tsx | 264 ------------------ .../RemoveApiTokenButton.tsx | 106 ------- .../common/ApiTokenTable/ApiTokenTable.tsx | 136 +++++++++ .../CopyApiTokenButton/CopyApiTokenButton.tsx | 35 +-- .../CreateApiTokenButton.tsx | 25 +- .../RemoveApiTokenButton.tsx | 77 +++++ .../common/ApiTokenTable/useApiTokenTable.tsx | 140 ++++++++++ .../ProjectApiAccess/ProjectApiAccess.tsx | 106 ++++++- .../ProjectSettings/ProjectSettings.tsx | 2 +- 12 files changed, 550 insertions(+), 438 deletions(-) delete mode 100644 frontend/src/component/admin/api/index.tsx delete mode 100644 frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx delete mode 100644 frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx create mode 100644 frontend/src/component/common/ApiTokenTable/ApiTokenTable.tsx rename frontend/src/component/{admin/apiToken => common/ApiTokenTable}/CopyApiTokenButton/CopyApiTokenButton.tsx (57%) rename frontend/src/component/{admin/apiToken => common/ApiTokenTable}/CreateApiTokenButton/CreateApiTokenButton.tsx (52%) create mode 100644 frontend/src/component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton.tsx create mode 100644 frontend/src/component/common/ApiTokenTable/useApiTokenTable.tsx diff --git a/frontend/src/component/admin/Admin.tsx b/frontend/src/component/admin/Admin.tsx index dce5b15f0f..f8ec1a544c 100644 --- a/frontend/src/component/admin/Admin.tsx +++ b/frontend/src/component/admin/Admin.tsx @@ -1,5 +1,5 @@ import { Routes, Route } from 'react-router-dom'; -import { ApiPage } from './api'; +import { ApiTokenPage } from './apiToken/ApiTokenPage/ApiTokenPage'; import { CreateApiToken } from './apiToken/CreateApiToken/CreateApiToken'; import { AuthSettings } from './auth/AuthSettings'; import { Billing } from './billing/Billing'; @@ -29,7 +29,7 @@ export const Admin = () => ( } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/component/admin/api/index.tsx b/frontend/src/component/admin/api/index.tsx deleted file mode 100644 index 65ddf60ed5..0000000000 --- a/frontend/src/component/admin/api/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiTokenPage } from 'component/admin/apiToken/ApiTokenPage/ApiTokenPage'; - -export const ApiPage = () => { - return ( -
- -
- ); -}; - -export default ApiPage; diff --git a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx index 6db09abf5d..83b583af44 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx @@ -1,19 +1,93 @@ import { useContext } from 'react'; import AccessContext from 'contexts/AccessContext'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { READ_API_TOKEN } from 'component/providers/AccessProvider/permissions'; +import { + CREATE_API_TOKEN, + DELETE_API_TOKEN, + READ_API_TOKEN, +} from 'component/providers/AccessProvider/permissions'; import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; -import { ApiTokenTable } from 'component/admin/apiToken/ApiTokenTable/ApiTokenTable'; +import { ApiTokenTable } from 'component/common/ApiTokenTable/ApiTokenTable'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import { CreateApiTokenButton } from 'component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton'; +import { Search } from 'component/common/Search/Search'; +import { useApiTokenTable } from 'component/common/ApiTokenTable/useApiTokenTable'; import { useApiTokens } from 'hooks/api/getters/useApiTokens/useApiTokens'; +import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; +import { CopyApiTokenButton } from 'component/common/ApiTokenTable/CopyApiTokenButton/CopyApiTokenButton'; +import { RemoveApiTokenButton } from 'component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton'; +import useApiTokensApi from 'hooks/api/actions/useApiTokensApi/useApiTokensApi'; export const ApiTokenPage = () => { const { hasAccess } = useContext(AccessContext); - const { tokens, loading } = useApiTokens(); + const { tokens, loading, refetch } = useApiTokens(); + const { deleteToken } = useApiTokensApi(); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { globalFilter }, + setGlobalFilter, + setHiddenColumns, + columns, + } = useApiTokenTable(tokens, props => ( + + + { + await deleteToken(props.row.original.secret); + refetch(); + }} + /> + + )); return ( } + show={() => ( + + + + + + } + /> + } + > + + + )} elseShow={() => } /> ); diff --git a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx deleted file mode 100644 index b63896ac7c..0000000000 --- a/frontend/src/component/admin/apiToken/ApiTokenTable/ApiTokenTable.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; -import { useGlobalFilter, useSortBy, useTable } from 'react-table'; -import { PageContent } from 'component/common/PageContent/PageContent'; -import { - SortableTableHeader, - TableCell, - TablePlaceholder, -} from 'component/common/Table'; -import { Box, Table, TableBody, 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 { 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 { useMemo } from 'react'; -import theme from 'themes/theme'; -import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; -import { Search } from 'component/common/Search/Search'; -import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; -import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; - -const hiddenColumnsSmall = ['Icon', 'createdAt']; -const hiddenColumnsCompact = ['Icon', 'project', 'seenAt']; - -interface IApiTokenTableProps { - compact?: boolean; - filterForProject?: string; - tokens: IApiToken[]; - loading: boolean; -} -export const ApiTokenTable = ({ - compact = false, - filterForProject, - tokens, - loading, -}: IApiTokenTableProps) => { - const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - - const COLUMNS = useMemo(() => { - return [ - { - id: 'Icon', - width: '1%', - Cell: () => } />, - disableSortBy: true, - disableGlobalFilter: true, - }, - { - Header: 'Username', - accessor: 'username', - Cell: HighlightCell, - }, - { - Header: 'Type', - accessor: 'type', - Cell: ({ - value, - }: { - value: 'client' | 'admin' | 'frontend'; - }) => ( - - ), - minWidth: 280, - }, - { - 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: 'Last seen', - accessor: 'seenAt', - Cell: TimeAgoCell, - minWidth: 150, - disableGlobalFilter: true, - }, - { - Header: 'Actions', - id: 'Actions', - align: 'center', - width: '1%', - disableSortBy: true, - disableGlobalFilter: true, - Cell: (props: any) => ( - - - - - ), - }, - ]; - }, [filterForProject]); - - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - state: { globalFilter }, - setGlobalFilter, - setHiddenColumns, - } = useTable( - { - columns: COLUMNS as any, - data: tokens as any, - initialState, - sortTypes, - autoResetHiddenColumns: false, - disableSortRemove: true, - }, - useGlobalFilter, - useSortBy - ); - - useConditionallyHiddenColumns( - [ - { - condition: isSmallScreen, - columns: hiddenColumnsSmall, - }, - { - condition: compact, - columns: hiddenColumnsCompact, - }, - ], - setHiddenColumns, - COLUMNS - ); - - return ( - - - - - - } - /> - } - > - 0} - show={ - - - - } - /> - - - - - - {rows.map(row => { - prepareRow(row); - return ( - - {row.cells.map(cell => ( - - {cell.render('Cell')} - - ))} - - ); - })} - -
-
-
- 0} - show={ - - No tokens found matching “ - {globalFilter} - ” - - } - elseShow={ - - - {'No tokens available. Read '} - - API How-to guides - {' '} - {' to learn more.'} - - - } - /> - } - /> -
- ); -}; -const tokenDescriptions: { [index: string]: { label: string; title: string } } = - { - client: { - label: 'CLIENT', - title: 'Connect server-side SDK or Unleash Proxy', - }, - frontend: { - label: 'FRONTEND', - title: 'Connect web and mobile SDK', - }, - admin: { - label: 'ADMIN', - title: 'Full access for managing Unleash', - }, - }; diff --git a/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx b/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx deleted file mode 100644 index d34a3b9320..0000000000 --- a/frontend/src/component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { - DELETE_API_TOKEN, - DELETE_PROJECT_API_TOKEN, -} from 'component/providers/AccessProvider/permissions'; -import { Delete } from '@mui/icons-material'; -import { styled } 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'; -import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; -import useProjectApiTokensApi from '../../../../hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi'; - -const StyledUl = styled('ul')({ - marginBottom: 0, -}); - -interface IRemoveApiTokenButtonProps { - token: IApiToken; - project?: string; -} - -export const RemoveApiTokenButton = ({ - token, - project, -}: IRemoveApiTokenButtonProps) => { - const { hasAccess, isAdmin } = useContext(AccessContext); - const { deleteToken } = useApiTokensApi(); - const { deleteToken: deleteProjectToken } = useProjectApiTokensApi(); - const [open, setOpen] = useState(false); - const { setToastData } = useToast(); - const { refetch } = useApiTokens(); - - const permission = Boolean(project) - ? DELETE_PROJECT_API_TOKEN - : DELETE_API_TOKEN; - - const canRemove = () => { - if (isAdmin) { - return true; - } - if (token && token.projects && project && permission) { - const { projects } = token; - for (const tokenProject of projects) { - if (!hasAccess(permission, tokenProject)) { - return false; - } - } - return true; - } - }; - - const onRemove = async () => { - if (project) { - await deleteProjectToken(token.secret, project); - } else { - await deleteToken(token.secret); - } - setOpen(false); - refetch(); - setToastData({ - type: 'success', - title: 'API token removed', - }); - }; - - return ( - <> - setOpen(true)} - size="large" - disabled={!canRemove()} - > - - - 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/component/common/ApiTokenTable/ApiTokenTable.tsx b/frontend/src/component/common/ApiTokenTable/ApiTokenTable.tsx new file mode 100644 index 0000000000..fa48c50764 --- /dev/null +++ b/frontend/src/component/common/ApiTokenTable/ApiTokenTable.tsx @@ -0,0 +1,136 @@ +import { + Row, + TablePropGetter, + TableProps, + TableBodyPropGetter, + TableBodyProps, + HeaderGroup, +} from 'react-table'; +import { + SortableTableHeader, + TableCell, + TablePlaceholder, +} from 'component/common/Table'; +import { Box, Table, TableBody, TableRow, useMediaQuery } from '@mui/material'; +import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { ApiTokenDocs } from 'component/admin/apiToken/ApiTokenDocs/ApiTokenDocs'; + +import theme from 'themes/theme'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; + +const hiddenColumnsSmall = ['Icon', 'createdAt']; +const hiddenColumnsCompact = ['Icon', 'project', 'seenAt']; + +interface IApiTokenTableProps { + compact?: boolean; + loading: boolean; + setHiddenColumns: (param: any) => void; + columns: any[]; + rows: Row[]; + prepareRow: (row: Row) => void; + getTableProps: ( + propGetter?: TablePropGetter | undefined + ) => TableProps; + getTableBodyProps: ( + propGetter?: TableBodyPropGetter | undefined + ) => TableBodyProps; + headerGroups: HeaderGroup[]; + globalFilter: any; +} +export const ApiTokenTable = ({ + compact = false, + setHiddenColumns, + columns, + loading, + rows, + getTableProps, + getTableBodyProps, + headerGroups, + globalFilter, + prepareRow, +}: IApiTokenTableProps) => { + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); + + useConditionallyHiddenColumns( + [ + { + condition: isSmallScreen, + columns: hiddenColumnsSmall, + }, + { + condition: compact, + columns: hiddenColumnsCompact, + }, + ], + setHiddenColumns, + columns + ); + + return ( + <> + 0} + show={ + + + + } + /> + + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
    +
    +
    + 0} + show={ + + No tokens found matching “ + {globalFilter} + ” + + } + elseShow={ + + + {'No tokens available. Read '} + + API How-to guides + {' '} + {' to learn more.'} + + + } + /> + } + /> + + ); +}; diff --git a/frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx b/frontend/src/component/common/ApiTokenTable/CopyApiTokenButton/CopyApiTokenButton.tsx similarity index 57% rename from frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx rename to frontend/src/component/common/ApiTokenTable/CopyApiTokenButton/CopyApiTokenButton.tsx index 62fa07cf0d..046e11dfa6 100644 --- a/frontend/src/component/admin/apiToken/CopyApiTokenButton/CopyApiTokenButton.tsx +++ b/frontend/src/component/common/ApiTokenTable/CopyApiTokenButton/CopyApiTokenButton.tsx @@ -2,51 +2,33 @@ 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'; -import { - READ_API_TOKEN, - READ_PROJECT_API_TOKEN, -} from 'component/providers/AccessProvider/permissions'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; -import { useContext } from 'react'; -import AccessContext from 'contexts/AccessContext'; interface ICopyApiTokenButtonProps { token: IApiToken; + permission: string; project?: string; + track?: () => void; } export const CopyApiTokenButton = ({ token, project, + permission, + track, }: ICopyApiTokenButtonProps) => { - const { hasAccess, isAdmin } = useContext(AccessContext); const { setToastData } = useToast(); - const permission = Boolean(project) - ? READ_PROJECT_API_TOKEN - : READ_API_TOKEN; - - const canCopy = () => { - if (isAdmin) { - return true; - } - if (token && token.projects && project && permission) { - const { projects } = token; - for (const tokenProject of projects) { - if (!hasAccess(permission, tokenProject)) { - return false; - } - } - return true; - } - }; - const copyToken = (value: string) => { if (copy(value)) { setToastData({ type: 'success', title: `Token copied to clipboard`, }); + + if (track && typeof track === 'function') { + track(); + } } }; @@ -57,7 +39,6 @@ export const CopyApiTokenButton = ({ tooltipProps={{ title: 'Copy token', arrow: true }} onClick={() => copyToken(token.secret)} size="large" - disabled={!canCopy()} > diff --git a/frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx similarity index 52% rename from frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx rename to frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx index 49ebe59ec5..429b77ed05 100644 --- a/frontend/src/component/admin/apiToken/CreateApiTokenButton/CreateApiTokenButton.tsx +++ b/frontend/src/component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton.tsx @@ -1,27 +1,24 @@ import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; -import { - CREATE_API_TOKEN, - CREATE_PROJECT_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'; -import { useOptionalPathParam } from 'hooks/useOptionalPathParam'; +interface ICreateApiTokenButton { + path: string; + permission: string; + project?: string; +} -export const CreateApiTokenButton = () => { +export const CreateApiTokenButton = ({ + path, + permission, + project, +}: ICreateApiTokenButton) => { const navigate = useNavigate(); - const project = useOptionalPathParam('projectId'); - - const to = Boolean(project) ? 'create' : '/admin/api/create-token'; - const permission = Boolean(project) - ? CREATE_PROJECT_API_TOKEN - : CREATE_API_TOKEN; - return ( navigate(to)} + onClick={() => navigate(path)} data-testid={CREATE_API_TOKEN_BUTTON} permission={permission} projectId={project} diff --git a/frontend/src/component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton.tsx b/frontend/src/component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton.tsx new file mode 100644 index 0000000000..c85e2daa32 --- /dev/null +++ b/frontend/src/component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton.tsx @@ -0,0 +1,77 @@ +import { Delete } from '@mui/icons-material'; +import { styled } from '@mui/material'; +import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; +import { useState } from 'react'; +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import useToast from 'hooks/useToast'; +import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; +import { formatUnknownError } from 'utils/formatUnknownError'; + +const StyledUl = styled('ul')({ + marginBottom: 0, +}); + +interface IRemoveApiTokenButtonProps { + token: IApiToken; + permission: string; + onRemove: () => void; + project?: string; +} + +export const RemoveApiTokenButton = ({ + token, + permission, + onRemove, + project, +}: IRemoveApiTokenButtonProps) => { + const [open, setOpen] = useState(false); + const { setToastData, setToastApiError } = useToast(); + + const onRemoveToken = async () => { + try { + await onRemove(); + setOpen(false); + + setToastData({ + type: 'success', + title: 'API token removed', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + + 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/component/common/ApiTokenTable/useApiTokenTable.tsx b/frontend/src/component/common/ApiTokenTable/useApiTokenTable.tsx new file mode 100644 index 0000000000..1c1f7f855f --- /dev/null +++ b/frontend/src/component/common/ApiTokenTable/useApiTokenTable.tsx @@ -0,0 +1,140 @@ +import { useMemo } from 'react'; +import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; +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 { sortTypes } from 'utils/sortTypes'; +import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; +import { Key } from '@mui/icons-material'; + +export const useApiTokenTable = ( + tokens: IApiToken[], + getActionCell: (props: any) => JSX.Element +) => { + const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []); + + const COLUMNS = useMemo(() => { + return [ + { + id: 'Icon', + width: '1%', + Cell: () => } />, + disableSortBy: true, + disableGlobalFilter: true, + }, + { + Header: 'Username', + accessor: 'username', + Cell: HighlightCell, + }, + { + Header: 'Type', + accessor: 'type', + Cell: ({ + value, + }: { + value: 'client' | 'admin' | 'frontend'; + }) => ( + + ), + minWidth: 280, + }, + { + 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: 'Last seen', + accessor: 'seenAt', + Cell: TimeAgoCell, + minWidth: 150, + disableGlobalFilter: true, + }, + { + Header: 'Actions', + id: 'Actions', + align: 'center', + width: '1%', + disableSortBy: true, + disableGlobalFilter: true, + Cell: getActionCell, + }, + ]; + }, []); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + setGlobalFilter, + setHiddenColumns, + } = useTable( + { + columns: COLUMNS as any, + data: tokens as any, + initialState, + sortTypes, + autoResetHiddenColumns: false, + disableSortRemove: true, + }, + useGlobalFilter, + useSortBy + ); + + return { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + setGlobalFilter, + setHiddenColumns, + columns: COLUMNS, + }; +}; + +const tokenDescriptions: { + [index: string]: { label: string; title: string }; +} = { + client: { + label: 'CLIENT', + title: 'Connect server-side SDK or Unleash Proxy', + }, + frontend: { + label: 'FRONTEND', + title: 'Connect web and mobile SDK', + }, + admin: { + label: 'ADMIN', + title: 'Full access for managing Unleash', + }, +}; diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx index 11e476761c..810b8aad9a 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess.tsx @@ -3,23 +3,84 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import { Alert } from '@mui/material'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import AccessContext from 'contexts/AccessContext'; -import { READ_PROJECT_API_TOKEN } from 'component/providers/AccessProvider/permissions'; +import { + CREATE_API_TOKEN, + READ_PROJECT_API_TOKEN, +} from 'component/providers/AccessProvider/permissions'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { usePageTitle } from 'hooks/usePageTitle'; import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; import { CreateProjectApiToken } from 'component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiToken'; import { Routes, Route } from 'react-router-dom'; -import { ApiTokenTable } from 'component/admin/apiToken/ApiTokenTable/ApiTokenTable'; +import { ApiTokenTable } from 'component/common/ApiTokenTable/ApiTokenTable'; import { useProjectApiTokens } from 'hooks/api/getters/useProjectApiTokens/useProjectApiTokens'; +import { CreateApiTokenButton } from 'component/common/ApiTokenTable/CreateApiTokenButton/CreateApiTokenButton'; +import { useApiTokenTable } from 'component/common/ApiTokenTable/useApiTokenTable'; +import { Search } from 'component/common/Search/Search'; +import { + CREATE_PROJECT_API_TOKEN, + DELETE_PROJECT_API_TOKEN, +} from '@server/types/permissions'; +import { CopyApiTokenButton } from 'component/common/ApiTokenTable/CopyApiTokenButton/CopyApiTokenButton'; +import { RemoveApiTokenButton } from 'component/common/ApiTokenTable/RemoveApiTokenButton/RemoveApiTokenButton'; +import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import useProjectApiTokensApi from 'hooks/api/actions/useProjectApiTokensApi/useProjectApiTokensApi'; export const ProjectApiAccess = () => { const projectId = useRequiredPathParam('projectId'); const projectName = useProjectNameOrId(projectId); const { hasAccess } = useContext(AccessContext); - const { tokens, loading } = useProjectApiTokens(projectId); + const { + tokens, + loading, + refetch: refetchProjectTokens, + } = useProjectApiTokens(projectId); + const { trackEvent } = usePlausibleTracker(); + const { deleteToken: deleteProjectToken } = useProjectApiTokensApi(); usePageTitle(`Project api access – ${projectName}`); + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { globalFilter }, + setGlobalFilter, + setHiddenColumns, + columns, + } = useApiTokenTable(tokens, props => ( + + + trackEvent('project_api_tokens', { + props: { eventType: 'api_key_copied' }, + }) + } + /> + { + await deleteProjectToken( + props.row.original.secret, + projectId + ); + trackEvent('project_api_tokens', { + props: { eventType: 'api_key_deleted' }, + }); + refetchProjectTokens(); + }} + /> + + )); + if (!hasAccess(READ_PROJECT_API_TOKEN, projectId)) { return ( }> @@ -33,12 +94,39 @@ export const ProjectApiAccess = () => { return (
    - + + + + + + } + /> + } + > + + } /> diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx index 7cef6a9f19..0bd2d1347a 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectSettings.tsx @@ -9,7 +9,7 @@ import { ITab, VerticalTabs } from 'component/common/VerticalTabs/VerticalTabs'; import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess'; import ProjectEnvironmentList from 'component/project/ProjectEnvironment/ProjectEnvironment'; import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeRequestConfiguration'; -import { ProjectApiAccess } from './ProjectApiAccess/ProjectApiAccess'; +import { ProjectApiAccess } from 'component/project/Project/ProjectSettings/ProjectApiAccess/ProjectApiAccess'; import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig'; export const ProjectSettings = () => {