2062 view edit an api key (#2231)

* Add query to get api keys

* Add a link to apiKey detail page

* Reset generatedApiKey when leaving page

* Simplify stuff

* Regenerate key when clicking on button

* Simplify

* Fix test

* Refetch apiKeys when delete or create one

* Add test for utils

* Create utils function

* Enable null expiration dates

* Update formatExpiration

* Fix display

* Fix noteCard

* Fix errors

* Fix reset

* Fix display

* Fix renaming

* Fix tests

* Fix ci

* Fix mocked data

* Fix test

* Update coverage requiremeents

* Rename folder

* Code review returns

* Symplify sht code
This commit is contained in:
martmull
2023-10-26 11:32:44 +02:00
committed by GitHub
parent 2b1945a3e1
commit fc4075b372
34 changed files with 434 additions and 183 deletions

View File

@ -1,11 +1,18 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
import { IconSettings, IconTrash } from '@/ui/display/icon';
import { GET_API_KEYS } from '@/settings/developers/graphql/queries/getApiKeys';
import { useGeneratedApiKeys } from '@/settings/developers/hooks/useGeneratedApiKeys';
import { generatedApiKeyFamilyState } from '@/settings/developers/states/generatedApiKeyFamilyState';
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
import { IconRepeat, IconSettings, IconTrash } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
@ -15,62 +22,159 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import {
useDeleteOneApiKeyMutation,
useGetApiKeyQuery,
useInsertOneApiKeyMutation,
} from '~/generated/graphql';
const StyledInfo = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledInputContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export const SettingsDevelopersApiKeyDetail = () => {
const navigate = useNavigate();
const { apiKeyId = '' } = useParams();
const [generatedApiKey] = useRecoilState(generatedApiKeyState);
const apiKeyQuery = useGetApiKeyQuery({
const setGeneratedApi = useGeneratedApiKeys();
const [generatedApiKey] = useRecoilState(
generatedApiKeyFamilyState(apiKeyId),
);
const [deleteApiKey] = useDeleteOneApiKeyMutation();
const [insertOneApiKey] = useInsertOneApiKeyMutation();
const apiKeyData = useGetApiKeyQuery({
variables: {
apiKeyId,
},
});
const [deleteApiKey] = useDeleteOneApiKeyMutation();
const deleteIntegration = async () => {
await deleteApiKey({ variables: { apiKeyId } });
navigate('/settings/developers/api-keys');
}).data?.findManyApiKey[0];
const deleteIntegration = async (redirect = true) => {
await deleteApiKey({
variables: { apiKeyId },
refetchQueries: [getOperationName(GET_API_KEYS) ?? ''],
});
if (redirect) {
navigate('/settings/developers/api-keys');
}
};
const { expiresAt, name } = apiKeyQuery.data?.findManyApiKey[0] || {};
const regenerateApiKey = async () => {
if (apiKeyData?.name) {
const newExpiresAt = computeNewExpirationDate(
apiKeyData.expiresAt,
apiKeyData.createdAt,
);
const apiKey = await insertOneApiKey({
variables: {
data: {
name: apiKeyData.name,
expiresAt: newExpiresAt,
},
},
refetchQueries: [getOperationName(GET_API_KEYS) ?? ''],
});
await deleteIntegration(false);
if (apiKey.data?.createOneApiKey) {
setGeneratedApi(
apiKey.data.createOneApiKey.id,
apiKey.data.createOneApiKey.token,
);
navigate(
`/settings/developers/api-keys/${apiKey.data.createOneApiKey.id}`,
);
}
}
};
useEffect(() => {
if (apiKeyData) {
return () => {
setGeneratedApi(apiKeyId, null);
};
}
});
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'APIs', href: '/settings/developers/api-keys' },
{ children: name || '' },
]}
/>
</SettingsHeaderContainer>
<Section>
<H2Title
title="Api Key"
description="Copy this key as it will only be visible this one time"
/>
<ApiKeyInput expiresAt={expiresAt} apiKey={generatedApiKey || ''} />
</Section>
<Section>
<H2Title title="Name" description="Name of your API key" />
<TextInput
placeholder="E.g. backoffice integration"
value={name || ''}
disabled={true}
fullWidth
/>
</Section>
<Section>
<H2Title title="Danger zone" description="Delete this integration" />
<Button
accent="danger"
variant="secondary"
title="Disable"
Icon={IconTrash}
onClick={deleteIntegration}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
<>
{apiKeyData?.name && (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'APIs', href: '/settings/developers/api-keys' },
{ children: apiKeyData.name },
]}
/>
</SettingsHeaderContainer>
<Section>
{generatedApiKey ? (
<>
<H2Title
title="Api Key"
description="Copy this key as it will only be visible this one time"
/>
<ApiKeyInput apiKey={generatedApiKey} />
<StyledInfo>
{formatExpiration(apiKeyData?.expiresAt || '', true, false)}
</StyledInfo>
</>
) : (
<>
<H2Title
title="Api Key"
description="Regenerate an Api key"
/>
<StyledInputContainer>
<Button
title="Regenerate Key"
Icon={IconRepeat}
onClick={regenerateApiKey}
/>
<StyledInfo>
{formatExpiration(
apiKeyData?.expiresAt || '',
true,
false,
)}
</StyledInfo>
</StyledInputContainer>
</>
)}
</Section>
<Section>
<H2Title title="Name" description="Name of your API key" />
<TextInput
placeholder="E.g. backoffice integration"
value={apiKeyData.name}
disabled
fullWidth
/>
</Section>
<Section>
<H2Title
title="Danger zone"
description="Delete this integration"
/>
<Button
accent="danger"
variant="secondary"
title="Disable"
Icon={IconTrash}
onClick={() => deleteIntegration()}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
)}
</>
);
};

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
import { activeApiKeyItems } from '@/settings/developers/constants/mockObjects';
import { formatExpirations } from '@/settings/developers/utils/format-expiration';
import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title';
import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -12,6 +12,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useGetApiKeysQuery } from '~/generated/graphql';
const StyledContainer = styled.div`
height: fit-content;
@ -36,6 +37,8 @@ const StyledH1Title = styled(H1Title)`
export const SettingsDevelopersApiKeys = () => {
const navigate = useNavigate();
const apiKeysQuery = useGetApiKeysQuery();
const apiKeys = apiKeysQuery.data ? formatExpirations(apiKeysQuery.data) : [];
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
@ -63,10 +66,13 @@ export const SettingsDevelopersApiKeys = () => {
<TableHeader>Expiration</TableHeader>
<TableHeader></TableHeader>
</StyledTableRow>
{activeApiKeyItems.map((fieldItem) => (
{apiKeys.map((fieldItem) => (
<SettingsApiKeysFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem}
onClick={() => {
navigate(`/settings/developers/api-keys/${fieldItem.id}`);
}}
/>
))}
</Table>

View File

@ -1,13 +1,14 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getOperationName } from '@apollo/client/utilities';
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ExpirationDates } from '@/settings/developers/constants/expirationDates';
import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
import { GET_API_KEYS } from '@/settings/developers/graphql/queries/getApiKeys';
import { useGeneratedApiKeys } from '@/settings/developers/hooks/useGeneratedApiKeys';
import { IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Select } from '@/ui/input/components/Select';
@ -20,10 +21,10 @@ import { useInsertOneApiKeyMutation } from '~/generated/graphql';
export const SettingsDevelopersApiKeysNew = () => {
const [insertOneApiKey] = useInsertOneApiKeyMutation();
const navigate = useNavigate();
const [, setGeneratedApiKey] = useRecoilState(generatedApiKeyState);
const setGeneratedApi = useGeneratedApiKeys();
const [formValues, setFormValues] = useState<{
name: string;
expirationDate: number;
expirationDate: number | null;
}>({
expirationDate: ExpirationDates[0].value,
name: '',
@ -33,16 +34,24 @@ export const SettingsDevelopersApiKeysNew = () => {
variables: {
data: {
name: formValues.name,
expiresAt: DateTime.now()
.plus({ days: formValues.expirationDate })
.toISODate(),
expiresAt: formValues.expirationDate
? DateTime.now()
.plus({ days: formValues.expirationDate })
.toISODate()
: null,
},
},
refetchQueries: [getOperationName(GET_API_KEYS) ?? ''],
});
setGeneratedApiKey(apiKey.data?.createOneApiKey?.token);
navigate(
`/settings/developers/api-keys/${apiKey.data?.createOneApiKey?.id}`,
);
if (apiKey.data?.createOneApiKey) {
setGeneratedApi(
apiKey.data.createOneApiKey.id,
apiKey.data.createOneApiKey.token,
);
navigate(
`/settings/developers/api-keys/${apiKey.data.createOneApiKey.id}`,
);
}
};
const canSave = !!formValues.name;
return (

View File

@ -6,7 +6,6 @@ import {
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedApiKeyToken } from '~/testing/mock-data/api-keys';
import { sleep } from '~/testing/sleep';
const meta: Meta<PageDecoratorArgs> = {
@ -15,7 +14,6 @@ const meta: Meta<PageDecoratorArgs> = {
decorators: [PageDecorator],
args: {
routePath: '/settings/apis/f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
state: mockedApiKeyToken,
},
parameters: {
msw: graphqlMocks,