mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02:00
Fix/decouple api token list (#3171)
Decouples the API token list and adds tracking.
This commit is contained in:
parent
f8535af687
commit
f8c826450e
@ -1,5 +1,5 @@
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { ApiPage } from './api';
|
import { ApiTokenPage } from './apiToken/ApiTokenPage/ApiTokenPage';
|
||||||
import { CreateApiToken } from './apiToken/CreateApiToken/CreateApiToken';
|
import { CreateApiToken } from './apiToken/CreateApiToken/CreateApiToken';
|
||||||
import { AuthSettings } from './auth/AuthSettings';
|
import { AuthSettings } from './auth/AuthSettings';
|
||||||
import { Billing } from './billing/Billing';
|
import { Billing } from './billing/Billing';
|
||||||
@ -29,7 +29,7 @@ export const Admin = () => (
|
|||||||
<Route path="users" element={<UsersAdmin />} />
|
<Route path="users" element={<UsersAdmin />} />
|
||||||
<Route path="create-project-role" element={<CreateProjectRole />} />
|
<Route path="create-project-role" element={<CreateProjectRole />} />
|
||||||
<Route path="roles/:id/edit" element={<EditProjectRole />} />
|
<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="api/create-token" element={<CreateApiToken />} />
|
||||||
<Route path="users/:id/edit" element={<EditUser />} />
|
<Route path="users/:id/edit" element={<EditUser />} />
|
||||||
<Route path="service-accounts" element={<ServiceAccounts />} />
|
<Route path="service-accounts" element={<ServiceAccounts />} />
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { ApiTokenPage } from 'component/admin/apiToken/ApiTokenPage/ApiTokenPage';
|
|
||||||
|
|
||||||
export const ApiPage = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ApiTokenPage />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiPage;
|
|
@ -1,19 +1,93 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 { 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 { 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 = () => {
|
export const ApiTokenPage = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
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 (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(READ_API_TOKEN)}
|
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 />}
|
elseShow={() => <AdminAlert />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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 “
|
|
||||||
{globalFilter}
|
|
||||||
”
|
|
||||||
</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',
|
|
||||||
},
|
|
||||||
};
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
136
frontend/src/component/common/ApiTokenTable/ApiTokenTable.tsx
Normal file
136
frontend/src/component/common/ApiTokenTable/ApiTokenTable.tsx
Normal 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 “
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,51 +2,33 @@ import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens';
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { FileCopy } from '@mui/icons-material';
|
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 PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { useContext } from 'react';
|
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
|
|
||||||
interface ICopyApiTokenButtonProps {
|
interface ICopyApiTokenButtonProps {
|
||||||
token: IApiToken;
|
token: IApiToken;
|
||||||
|
permission: string;
|
||||||
project?: string;
|
project?: string;
|
||||||
|
track?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyApiTokenButton = ({
|
export const CopyApiTokenButton = ({
|
||||||
token,
|
token,
|
||||||
project,
|
project,
|
||||||
|
permission,
|
||||||
|
track,
|
||||||
}: ICopyApiTokenButtonProps) => {
|
}: ICopyApiTokenButtonProps) => {
|
||||||
const { hasAccess, isAdmin } = useContext(AccessContext);
|
|
||||||
const { setToastData } = useToast();
|
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) => {
|
const copyToken = (value: string) => {
|
||||||
if (copy(value)) {
|
if (copy(value)) {
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `Token copied to clipboard`,
|
title: `Token copied to clipboard`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (track && typeof track === 'function') {
|
||||||
|
track();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,7 +39,6 @@ export const CopyApiTokenButton = ({
|
|||||||
tooltipProps={{ title: 'Copy token', arrow: true }}
|
tooltipProps={{ title: 'Copy token', arrow: true }}
|
||||||
onClick={() => copyToken(token.secret)}
|
onClick={() => copyToken(token.secret)}
|
||||||
size="large"
|
size="large"
|
||||||
disabled={!canCopy()}
|
|
||||||
>
|
>
|
||||||
<FileCopy />
|
<FileCopy />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
@ -1,27 +1,24 @@
|
|||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
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 { CREATE_API_TOKEN_BUTTON } from 'utils/testIds';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Add } from '@mui/icons-material';
|
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 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 (
|
return (
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
Icon={Add}
|
Icon={Add}
|
||||||
onClick={() => navigate(to)}
|
onClick={() => navigate(path)}
|
||||||
data-testid={CREATE_API_TOKEN_BUTTON}
|
data-testid={CREATE_API_TOKEN_BUTTON}
|
||||||
permission={permission}
|
permission={permission}
|
||||||
projectId={project}
|
projectId={project}
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
140
frontend/src/component/common/ApiTokenTable/useApiTokenTable.tsx
Normal file
140
frontend/src/component/common/ApiTokenTable/useApiTokenTable.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
@ -3,23 +3,84 @@ import { PageContent } from 'component/common/PageContent/PageContent';
|
|||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
||||||
import { CreateProjectApiToken } from 'component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiToken';
|
import { CreateProjectApiToken } from 'component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiToken';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
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 { 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 = () => {
|
export const ProjectApiAccess = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const projectName = useProjectNameOrId(projectId);
|
const projectName = useProjectNameOrId(projectId);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
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}`);
|
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)) {
|
if (!hasAccess(READ_PROJECT_API_TOKEN, projectId)) {
|
||||||
return (
|
return (
|
||||||
<PageContent header={<PageHeader title="Api access" />}>
|
<PageContent header={<PageHeader title="Api access" />}>
|
||||||
@ -33,12 +94,39 @@ export const ProjectApiAccess = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', overflow: 'hidden' }}>
|
<div style={{ width: '100%', overflow: 'hidden' }}>
|
||||||
<ApiTokenTable
|
<PageContent
|
||||||
tokens={tokens}
|
header={
|
||||||
loading={loading}
|
<PageHeader
|
||||||
compact
|
title={`API access (${rows.length})`}
|
||||||
filterForProject={projectId}
|
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>
|
<Routes>
|
||||||
<Route path="create" element={<CreateProjectApiToken />} />
|
<Route path="create" element={<CreateProjectApiToken />} />
|
||||||
|
@ -9,7 +9,7 @@ import { ITab, VerticalTabs } from 'component/common/VerticalTabs/VerticalTabs';
|
|||||||
import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess';
|
import { ProjectAccess } from 'component/project/ProjectAccess/ProjectAccess';
|
||||||
import ProjectEnvironmentList from 'component/project/ProjectEnvironment/ProjectEnvironment';
|
import ProjectEnvironmentList from 'component/project/ProjectEnvironment/ProjectEnvironment';
|
||||||
import { ChangeRequestConfiguration } from './ChangeRequestConfiguration/ChangeRequestConfiguration';
|
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';
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
export const ProjectSettings = () => {
|
export const ProjectSettings = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user