1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Fix/decouple api token list (#3171)

Decouples the API token list and adds tracking.
This commit is contained in:
Fredrik Strand Oseberg 2023-02-21 14:27:46 +01:00 committed by GitHub
parent f8535af687
commit f8c826450e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 550 additions and 438 deletions

View File

@ -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 = () => (
<Route path="users" element={<UsersAdmin />} />
<Route path="create-project-role" element={<CreateProjectRole />} />
<Route path="roles/:id/edit" element={<EditProjectRole />} />
<Route path="api" element={<ApiPage />} />
<Route path="api" element={<ApiTokenPage />} />
<Route path="api/create-token" element={<CreateApiToken />} />
<Route path="users/:id/edit" element={<EditUser />} />
<Route path="service-accounts" element={<ServiceAccounts />} />

View File

@ -1,11 +0,0 @@
import { ApiTokenPage } from 'component/admin/apiToken/ApiTokenPage/ApiTokenPage';
export const ApiPage = () => {
return (
<div>
<ApiTokenPage />
</div>
);
};
export default ApiPage;

View File

@ -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 => (
<ActionCell>
<CopyApiTokenButton
token={props.row.original}
permission={READ_API_TOKEN}
/>
<RemoveApiTokenButton
token={props.row.original}
permission={DELETE_API_TOKEN}
onRemove={async () => {
await deleteToken(props.row.original.secret);
refetch();
}}
/>
</ActionCell>
));
return (
<ConditionallyRender
condition={hasAccess(READ_API_TOKEN)}
show={() => <ApiTokenTable tokens={tokens} loading={loading} />}
show={() => (
<PageContent
header={
<PageHeader
title={`API access (${rows.length})`}
actions={
<>
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<CreateApiTokenButton
permission={CREATE_API_TOKEN}
path="/admin/api/create-token"
/>
</>
}
/>
}
>
<ApiTokenTable
loading={loading}
headerGroups={headerGroups}
setHiddenColumns={setHiddenColumns}
prepareRow={prepareRow}
getTableBodyProps={getTableBodyProps}
getTableProps={getTableProps}
rows={rows}
columns={columns}
globalFilter={globalFilter}
/>
</PageContent>
)}
elseShow={() => <AdminAlert />}
/>
);

View File

@ -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: () => <IconCell icon={<Key color="disabled" />} />,
disableSortBy: true,
disableGlobalFilter: true,
},
{
Header: 'Username',
accessor: 'username',
Cell: HighlightCell,
},
{
Header: 'Type',
accessor: 'type',
Cell: ({
value,
}: {
value: 'client' | 'admin' | 'frontend';
}) => (
<HighlightCell
value={tokenDescriptions[value.toLowerCase()].label}
subtitle={tokenDescriptions[value.toLowerCase()].title}
/>
),
minWidth: 280,
},
{
Header: 'Project',
accessor: 'project',
Cell: (props: any) => (
<ProjectsList
project={props.row.original.project}
projects={props.row.original.projects}
/>
),
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) => (
<ActionCell>
<CopyApiTokenButton
token={props.row.original}
project={filterForProject}
/>
<RemoveApiTokenButton
token={props.row.original}
project={filterForProject}
/>
</ActionCell>
),
},
];
}, [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 (
<PageContent
header={
<PageHeader
title={`API access (${rows.length})`}
actions={
<>
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<CreateApiTokenButton />
</>
}
/>
}
>
<ConditionallyRender
condition={rows.length > 0}
show={
<Box sx={{ mb: 4 }}>
<ApiTokenDocs />
</Box>
}
/>
<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>
</SearchHighlightProvider>
</Box>
<ConditionallyRender
condition={rows.length === 0 && !loading}
show={
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No tokens found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
<span>
{'No tokens available. Read '}
<a
href="https://docs.getunleash.io/how-to/api"
target="_blank"
rel="noreferrer"
>
API How-to guides
</a>{' '}
{' to learn more.'}
</span>
</TablePlaceholder>
}
/>
}
/>
</PageContent>
);
};
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',
},
};

View File

@ -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 (
<>
<PermissionIconButton
permission={permission}
projectId={project}
tooltipProps={{ title: 'Delete token', arrow: true }}
onClick={() => setOpen(true)}
size="large"
disabled={!canRemove()}
>
<Delete />
</PermissionIconButton>
<Dialogue
open={open}
onClick={onRemove}
onClose={() => setOpen(false)}
title="Confirm deletion"
>
<div>
Are you sure you want to delete the following API token?
<br />
<StyledUl>
<li>
<strong>username</strong>:{' '}
<code>{token.username}</code>
</li>
<li>
<strong>type</strong>: <code>{token.type}</code>
</li>
</StyledUl>
</div>
</Dialogue>
</>
);
};

View File

@ -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<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'));
useConditionallyHiddenColumns(
[
{
condition: isSmallScreen,
columns: hiddenColumnsSmall,
},
{
condition: compact,
columns: hiddenColumnsCompact,
},
],
setHiddenColumns,
columns
);
return (
<>
<ConditionallyRender
condition={rows.length > 0}
show={
<Box sx={{ mb: 4 }}>
<ApiTokenDocs />
</Box>
}
/>
<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>
</SearchHighlightProvider>
</Box>
<ConditionallyRender
condition={rows.length === 0 && !loading}
show={
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No tokens found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
<span>
{'No tokens available. Read '}
<a
href="https://docs.getunleash.io/how-to/api"
target="_blank"
rel="noreferrer"
>
API How-to guides
</a>{' '}
{' to learn more.'}
</span>
</TablePlaceholder>
}
/>
}
/>
</>
);
};

View File

@ -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()}
>
<FileCopy />
</PermissionIconButton>

View File

@ -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 (
<ResponsiveButton
Icon={Add}
onClick={() => navigate(to)}
onClick={() => navigate(path)}
data-testid={CREATE_API_TOKEN_BUTTON}
permission={permission}
projectId={project}

View File

@ -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 (
<>
<PermissionIconButton
permission={permission}
projectId={project}
tooltipProps={{ title: 'Delete token', arrow: true }}
onClick={() => setOpen(true)}
size="large"
>
<Delete />
</PermissionIconButton>
<Dialogue
open={open}
onClick={onRemoveToken}
onClose={() => setOpen(false)}
title="Confirm deletion"
>
<div>
Are you sure you want to delete the following API token?
<br />
<StyledUl>
<li>
<strong>username</strong>:{' '}
<code>{token.username}</code>
</li>
<li>
<strong>type</strong>: <code>{token.type}</code>
</li>
</StyledUl>
</div>
</Dialogue>
</>
);
};

View File

@ -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: () => <IconCell icon={<Key color="disabled" />} />,
disableSortBy: true,
disableGlobalFilter: true,
},
{
Header: 'Username',
accessor: 'username',
Cell: HighlightCell,
},
{
Header: 'Type',
accessor: 'type',
Cell: ({
value,
}: {
value: 'client' | 'admin' | 'frontend';
}) => (
<HighlightCell
value={tokenDescriptions[value.toLowerCase()].label}
subtitle={tokenDescriptions[value.toLowerCase()].title}
/>
),
minWidth: 280,
},
{
Header: 'Project',
accessor: 'project',
Cell: (props: any) => (
<ProjectsList
project={props.row.original.project}
projects={props.row.original.projects}
/>
),
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',
},
};

View File

@ -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 => (
<ActionCell>
<CopyApiTokenButton
token={props.row.original}
permission={READ_PROJECT_API_TOKEN}
project={projectId}
track={() =>
trackEvent('project_api_tokens', {
props: { eventType: 'api_key_copied' },
})
}
/>
<RemoveApiTokenButton
token={props.row.original}
permission={DELETE_PROJECT_API_TOKEN}
project={projectId}
onRemove={async () => {
await deleteProjectToken(
props.row.original.secret,
projectId
);
trackEvent('project_api_tokens', {
props: { eventType: 'api_key_deleted' },
});
refetchProjectTokens();
}}
/>
</ActionCell>
));
if (!hasAccess(READ_PROJECT_API_TOKEN, projectId)) {
return (
<PageContent header={<PageHeader title="Api access" />}>
@ -33,12 +94,39 @@ export const ProjectApiAccess = () => {
return (
<div style={{ width: '100%', overflow: 'hidden' }}>
<ApiTokenTable
tokens={tokens}
loading={loading}
compact
filterForProject={projectId}
<PageContent
header={
<PageHeader
title={`API access (${rows.length})`}
actions={
<>
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<CreateApiTokenButton
permission={CREATE_PROJECT_API_TOKEN}
path="create"
/>
</>
}
/>
}
>
<ApiTokenTable
compact
loading={loading}
headerGroups={headerGroups}
setHiddenColumns={setHiddenColumns}
prepareRow={prepareRow}
getTableBodyProps={getTableBodyProps}
getTableProps={getTableProps}
rows={rows}
columns={columns}
globalFilter={globalFilter}
/>
</PageContent>
<Routes>
<Route path="create" element={<CreateProjectApiToken />} />

View File

@ -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 = () => {