mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
feat: manage SA tokens through UI (#2840)
https://linear.app/unleash/issue/2-542/manage-service-account-tokens-through-the-ui 
This commit is contained in:
parent
2b8f1ee0d7
commit
b10d9c435e
@ -31,6 +31,7 @@ import {
|
|||||||
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
|
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
|
||||||
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||||
|
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -86,9 +87,7 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({
|
|||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
[theme.breakpoints.down('sm')]: {
|
paddingTop: theme.spacing(4),
|
||||||
marginTop: theme.spacing(4),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||||
@ -225,16 +224,18 @@ export const ServiceAccountModal = ({
|
|||||||
!serviceAccounts?.some(
|
!serviceAccounts?.some(
|
||||||
(serviceAccount: IUser) => serviceAccount.username === value
|
(serviceAccount: IUser) => serviceAccount.username === value
|
||||||
);
|
);
|
||||||
|
const isPATValid =
|
||||||
|
tokenGeneration === TokenGeneration.LATER ||
|
||||||
|
(isNotEmpty(patDescription) && patExpiresAt > new Date());
|
||||||
const isValid =
|
const isValid =
|
||||||
isNotEmpty(name) &&
|
isNotEmpty(name) &&
|
||||||
isNotEmpty(username) &&
|
isNotEmpty(username) &&
|
||||||
(editing || isUnique(username)) &&
|
(editing || isUnique(username)) &&
|
||||||
(tokenGeneration === TokenGeneration.LATER ||
|
isPATValid;
|
||||||
isNotEmpty(patDescription));
|
|
||||||
|
|
||||||
const suggestUsername = () => {
|
const suggestUsername = () => {
|
||||||
if (isNotEmpty(name) && !isNotEmpty(username)) {
|
if (isNotEmpty(name) && !isNotEmpty(username)) {
|
||||||
const normalizedFromName = `service:${name
|
const normalizedFromName = `service-${name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ /g, '-')
|
.replace(/ /g, '-')
|
||||||
.replace(/[^\w_-]/g, '')}`;
|
.replace(/[^\w_-]/g, '')}`;
|
||||||
@ -411,6 +412,16 @@ export const ServiceAccountModal = ({
|
|||||||
</StyledInlineContainer>
|
</StyledInlineContainer>
|
||||||
</StyledSecondaryContainer>
|
</StyledSecondaryContainer>
|
||||||
}
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<StyledInputDescription>
|
||||||
|
Service account tokens
|
||||||
|
</StyledInputDescription>
|
||||||
|
<ServiceAccountTokens
|
||||||
|
serviceAccount={serviceAccount!}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import {
|
||||||
|
calculateExpirationDate,
|
||||||
|
ExpirationOption,
|
||||||
|
IPersonalAPITokenFormErrors,
|
||||||
|
PersonalAPITokenForm,
|
||||||
|
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
|
||||||
|
import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
|
import { IUser } from 'interfaces/user';
|
||||||
|
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
|
||||||
|
|
||||||
|
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
|
||||||
|
|
||||||
|
interface IServiceAccountCreateTokenDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
serviceAccount: IUser;
|
||||||
|
onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServiceAccountCreateTokenDialog = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
serviceAccount,
|
||||||
|
onCreateClick,
|
||||||
|
}: IServiceAccountCreateTokenDialogProps) => {
|
||||||
|
const { tokens = [] } = usePersonalAPITokens(serviceAccount.id);
|
||||||
|
|
||||||
|
const [patDescription, setPatDescription] = useState('');
|
||||||
|
const [patExpiration, setPatExpiration] =
|
||||||
|
useState<ExpirationOption>(DEFAULT_EXPIRATION);
|
||||||
|
const [patExpiresAt, setPatExpiresAt] = useState(
|
||||||
|
calculateExpirationDate(DEFAULT_EXPIRATION)
|
||||||
|
);
|
||||||
|
const [patErrors, setPatErrors] = useState<IPersonalAPITokenFormErrors>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPatDescription('');
|
||||||
|
setPatExpiration(DEFAULT_EXPIRATION);
|
||||||
|
setPatExpiresAt(calculateExpirationDate(DEFAULT_EXPIRATION));
|
||||||
|
setPatErrors({});
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const isDescriptionUnique = (description: string) =>
|
||||||
|
!tokens?.some(token => token.description === description);
|
||||||
|
|
||||||
|
const isPATValid =
|
||||||
|
patDescription.length &&
|
||||||
|
isDescriptionUnique(patDescription) &&
|
||||||
|
patExpiresAt > new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialogue
|
||||||
|
open={open}
|
||||||
|
primaryButtonText="Create token"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={() =>
|
||||||
|
onCreateClick({
|
||||||
|
description: patDescription,
|
||||||
|
expiresAt: patExpiresAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabledPrimaryButton={!isPATValid}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
title="New token"
|
||||||
|
>
|
||||||
|
<PersonalAPITokenForm
|
||||||
|
description={patDescription}
|
||||||
|
setDescription={setPatDescription}
|
||||||
|
isDescriptionUnique={isDescriptionUnique}
|
||||||
|
expiration={patExpiration}
|
||||||
|
setExpiration={setPatExpiration}
|
||||||
|
expiresAt={patExpiresAt}
|
||||||
|
setExpiresAt={setPatExpiresAt}
|
||||||
|
errors={patErrors}
|
||||||
|
setErrors={setPatErrors}
|
||||||
|
/>
|
||||||
|
</Dialogue>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,364 @@
|
|||||||
|
import { Delete } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
styled,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||||
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { PAT_LIMIT } from '@server/util/constants';
|
||||||
|
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens';
|
||||||
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import {
|
||||||
|
INewPersonalAPIToken,
|
||||||
|
IPersonalAPIToken,
|
||||||
|
} from 'interfaces/personalAPIToken';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog';
|
||||||
|
import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
||||||
|
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { IUser } from 'interfaces/user';
|
||||||
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
import {
|
||||||
|
ICreatePersonalApiTokenPayload,
|
||||||
|
usePersonalAPITokensApi,
|
||||||
|
} from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
'& > div': {
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTablePlaceholder = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPlaceholderTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPlaceholderSubtitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const tokensPlaceholder: IPersonalAPIToken[] = Array(15).fill({
|
||||||
|
description: 'Short description of the feature',
|
||||||
|
type: '-',
|
||||||
|
createdAt: new Date(2022, 1, 1),
|
||||||
|
project: 'projectID',
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PageQueryType = Partial<
|
||||||
|
Record<'sort' | 'order' | 'search', string>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const defaultSort: SortingRule<string> = { id: 'createdAt' };
|
||||||
|
|
||||||
|
interface IServiceAccountTokensProps {
|
||||||
|
serviceAccount: IUser;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServiceAccountTokens = ({
|
||||||
|
serviceAccount,
|
||||||
|
readOnly,
|
||||||
|
}: IServiceAccountTokensProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const {
|
||||||
|
tokens = [],
|
||||||
|
refetchTokens,
|
||||||
|
loading,
|
||||||
|
} = usePersonalAPITokens(serviceAccount.id);
|
||||||
|
const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } =
|
||||||
|
usePersonalAPITokensApi();
|
||||||
|
|
||||||
|
const [initialState] = useState(() => ({
|
||||||
|
sortBy: [defaultSort],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [tokenOpen, setTokenOpen] = useState(false);
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [newToken, setNewToken] = useState<INewPersonalAPIToken>();
|
||||||
|
const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>();
|
||||||
|
|
||||||
|
const onCreateClick = async (newToken: ICreatePersonalApiTokenPayload) => {
|
||||||
|
try {
|
||||||
|
const token = await createUserPersonalAPIToken(
|
||||||
|
serviceAccount.id,
|
||||||
|
newToken
|
||||||
|
);
|
||||||
|
refetchTokens();
|
||||||
|
setCreateOpen(false);
|
||||||
|
setNewToken(token);
|
||||||
|
setTokenOpen(true);
|
||||||
|
setToastData({
|
||||||
|
title: 'Token created successfully',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteClick = async () => {
|
||||||
|
if (selectedToken) {
|
||||||
|
try {
|
||||||
|
await deleteUserPersonalAPIToken(
|
||||||
|
serviceAccount.id,
|
||||||
|
selectedToken?.id
|
||||||
|
);
|
||||||
|
refetchTokens();
|
||||||
|
setDeleteOpen(false);
|
||||||
|
setToastData({
|
||||||
|
title: 'Token deleted successfully',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Description',
|
||||||
|
accessor: 'description',
|
||||||
|
Cell: HighlightCell,
|
||||||
|
minWidth: 100,
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Expires',
|
||||||
|
accessor: 'expiresAt',
|
||||||
|
Cell: ({ value }: { value: string }) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (date.getFullYear() > new Date().getFullYear() + 100) {
|
||||||
|
return <TextCell>Never</TextCell>;
|
||||||
|
}
|
||||||
|
return <DateCell value={value} />;
|
||||||
|
},
|
||||||
|
sortType: 'date',
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Created',
|
||||||
|
accessor: 'createdAt',
|
||||||
|
Cell: DateCell,
|
||||||
|
sortType: 'date',
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Last seen',
|
||||||
|
accessor: 'seenAt',
|
||||||
|
Cell: TimeAgoCell,
|
||||||
|
sortType: 'date',
|
||||||
|
maxWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Actions',
|
||||||
|
id: 'Actions',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original: rowToken } }: any) => (
|
||||||
|
<ActionCell>
|
||||||
|
<Tooltip title="Delete token" arrow describeChild>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedToken(rowToken);
|
||||||
|
setDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionCell>
|
||||||
|
),
|
||||||
|
maxWidth: 100,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[setSelectedToken, setDeleteOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedData,
|
||||||
|
getSearchText,
|
||||||
|
getSearchContext,
|
||||||
|
} = useSearch(columns, searchValue, tokens);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
searchedData?.length === 0 && loading
|
||||||
|
? tokensPlaceholder
|
||||||
|
: searchedData,
|
||||||
|
[searchedData, loading]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { sortBy },
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
disableMultiSort: true,
|
||||||
|
},
|
||||||
|
useSortBy,
|
||||||
|
useFlexLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
useConditionallyHiddenColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: isExtraSmallScreen,
|
||||||
|
columns: ['expiresAt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: isSmallScreen,
|
||||||
|
columns: ['createdAt'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: Boolean(readOnly),
|
||||||
|
columns: ['Actions', 'expiresAt', 'createdAt'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setHiddenColumns,
|
||||||
|
columns
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!readOnly}
|
||||||
|
show={
|
||||||
|
<StyledHeader>
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={tokens.length >= PAT_LIMIT}
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
>
|
||||||
|
New token
|
||||||
|
</Button>
|
||||||
|
</StyledHeader>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
|
<VirtualizedTable
|
||||||
|
rows={rows}
|
||||||
|
headerGroups={headerGroups}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
/>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={searchValue?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No tokens found matching “
|
||||||
|
{searchValue}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<StyledTablePlaceholder>
|
||||||
|
<StyledPlaceholderTitle>
|
||||||
|
You have no tokens for this service account
|
||||||
|
yet.
|
||||||
|
</StyledPlaceholderTitle>
|
||||||
|
<StyledPlaceholderSubtitle>
|
||||||
|
Create a service account token for access to
|
||||||
|
the Unleash API.
|
||||||
|
</StyledPlaceholderSubtitle>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
>
|
||||||
|
Create new service account token
|
||||||
|
</Button>
|
||||||
|
</StyledTablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ServiceAccountCreateTokenDialog
|
||||||
|
open={createOpen}
|
||||||
|
setOpen={setCreateOpen}
|
||||||
|
serviceAccount={serviceAccount}
|
||||||
|
onCreateClick={onCreateClick}
|
||||||
|
/>
|
||||||
|
<ServiceAccountTokenDialog
|
||||||
|
open={tokenOpen}
|
||||||
|
setOpen={setTokenOpen}
|
||||||
|
token={newToken}
|
||||||
|
/>
|
||||||
|
<Dialogue
|
||||||
|
open={deleteOpen}
|
||||||
|
primaryButtonText="Delete token"
|
||||||
|
secondaryButtonText="Cancel"
|
||||||
|
onClick={onDeleteClick}
|
||||||
|
onClose={() => {
|
||||||
|
setDeleteOpen(false);
|
||||||
|
}}
|
||||||
|
title="Delete token?"
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
Any applications or scripts using this token "
|
||||||
|
<strong>{selectedToken?.description}</strong>" will no
|
||||||
|
longer be able to access the Unleash API. You cannot undo
|
||||||
|
this action.
|
||||||
|
</Typography>
|
||||||
|
</Dialogue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -13,7 +13,6 @@ import { useFlexLayout, useSortBy, useTable } from 'react-table';
|
|||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
@ -29,7 +28,6 @@ import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
|||||||
import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
import { ServiceAccountTokenDialog } from './ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
||||||
|
|
||||||
export const ServiceAccountsTable = () => {
|
export const ServiceAccountsTable = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
const { serviceAccounts, roles, refetch, loading } = useServiceAccounts();
|
const { serviceAccounts, roles, refetch, loading } = useServiceAccounts();
|
||||||
@ -128,7 +126,7 @@ export const ServiceAccountsTable = () => {
|
|||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[roles, navigate]
|
[roles]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [initialState] = useState({
|
const [initialState] = useState({
|
||||||
|
@ -155,7 +155,7 @@ export const PersonalAPITokenForm = ({
|
|||||||
if (isDescriptionUnique && !isDescriptionUnique(description)) {
|
if (isDescriptionUnique && !isDescriptionUnique(description)) {
|
||||||
setError(
|
setError(
|
||||||
ErrorField.DESCRIPTION,
|
ErrorField.DESCRIPTION,
|
||||||
'A personal API token with that description already exists.'
|
'A token with that description already exists.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setDescription(description);
|
setDescription(description);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
interface ICreatePersonalApiTokenPayload {
|
export interface ICreatePersonalApiTokenPayload {
|
||||||
description: string;
|
description: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
}
|
}
|
||||||
@ -53,10 +53,22 @@ export const usePersonalAPITokensApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUserPersonalAPIToken = async (userId: number, id: string) => {
|
||||||
|
const req = createRequest(`api/admin/user-admin/${userId}/pat/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createPersonalAPIToken,
|
createPersonalAPIToken,
|
||||||
deletePersonalAPIToken,
|
deletePersonalAPIToken,
|
||||||
createUserPersonalAPIToken,
|
createUserPersonalAPIToken,
|
||||||
|
deleteUserPersonalAPIToken,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
@ -10,9 +10,15 @@ export interface IUsePersonalAPITokensOutput {
|
|||||||
error?: Error;
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePersonalAPITokens = (): IUsePersonalAPITokensOutput => {
|
export const usePersonalAPITokens = (
|
||||||
|
userId?: number
|
||||||
|
): IUsePersonalAPITokensOutput => {
|
||||||
const { data, error, mutate } = useSWR(
|
const { data, error, mutate } = useSWR(
|
||||||
formatApiPath('api/admin/user/tokens'),
|
formatApiPath(
|
||||||
|
userId
|
||||||
|
? `api/admin/user-admin/${userId}/pat`
|
||||||
|
: 'api/admin/user/tokens'
|
||||||
|
),
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user