2060 create a new api key (#2206)

* Add folder for api settings

* Init create api key page

* Update create api key page

* Implement api call to create apiKey

* Add create api key mutation

* Get id when creating apiKey

* Display created Api Key

* Add delete api key button

* Remove button from InputText

* Update stuff

* Add test for ApiDetail

* Fix type

* Use recoil instead of router state

* Remane route paths

* Remove online return

* Move and test date util

* Remove useless Component

* Rename ApiKeys paths

* Rename ApiKeys files

* Add input text info testing

* Rename hooks to webhooks

* Remove console error

* Add tests to reach minimum coverage
This commit is contained in:
martmull
2023-10-24 16:14:54 +02:00
committed by GitHub
parent b6e8fabbb1
commit d61511262e
55 changed files with 919 additions and 133 deletions

View File

@ -136,7 +136,7 @@ export const CreateProfile = () => {
title="Name"
description="Your name as it will be displayed on the app"
/>
{/* TODO: When react-hook-form is added to edit page we should create a dedicated component with context */}
{/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
<StyledComboInputContainer>
<Controller
name="firstName"

View File

@ -1,25 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsApis } from '../SettingsApis';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsApi',
component: SettingsApis,
decorators: [PageDecorator],
args: { routePath: '/settings/apis' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsApis>;
export const Default: Story = {};

View File

@ -0,0 +1,76 @@
import { useNavigate, useParams } from 'react-router-dom';
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 { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import {
useDeleteOneApiKeyMutation,
useGetApiKeyQuery,
} from '~/generated/graphql';
export const SettingsDevelopersApiKeyDetail = () => {
const navigate = useNavigate();
const { apiKeyId = '' } = useParams();
const [generatedApiKey] = useRecoilState(generatedApiKeyState);
const apiKeyQuery = useGetApiKeyQuery({
variables: {
apiKeyId,
},
});
const [deleteApiKey] = useDeleteOneApiKeyMutation();
const deleteIntegration = async () => {
await deleteApiKey({ variables: { apiKeyId } });
navigate('/settings/developers/api-keys');
};
const { expiresAt, name } = apiKeyQuery.data?.findManyApiKey[0] || {};
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>
);
};

View File

@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
import { SettingsApisFieldItemTableRow } from '@/settings/developers/components/SettingsApisFieldItemTableRow';
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
import { activeApiKeyItems } from '@/settings/developers/constants/mockObjects';
import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title';
@ -34,7 +34,7 @@ const StyledH1Title = styled(H1Title)`
margin-bottom: 0;
`;
export const SettingsApis = () => {
export const SettingsDevelopersApiKeys = () => {
const navigate = useNavigate();
return (
@ -48,7 +48,7 @@ export const SettingsApis = () => {
accent="blue"
size="small"
onClick={() => {
navigate('/');
navigate('/settings/developers/api-keys/new');
}}
/>
</StyledHeader>
@ -64,7 +64,7 @@ export const SettingsApis = () => {
<TableHeader></TableHeader>
</StyledTableRow>
{activeApiKeyItems.map((fieldItem) => (
<SettingsApisFieldItemTableRow
<SettingsApiKeysFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem}
/>

View File

@ -0,0 +1,100 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
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 { IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useInsertOneApiKeyMutation } from '~/generated/graphql';
export const SettingsDevelopersApiKeysNew = () => {
const [insertOneApiKey] = useInsertOneApiKeyMutation();
const navigate = useNavigate();
const [, setGeneratedApiKey] = useRecoilState(generatedApiKeyState);
const [formValues, setFormValues] = useState<{
name: string;
expirationDate: number;
}>({
expirationDate: ExpirationDates[0].value,
name: '',
});
const onSave = async () => {
const apiKey = await insertOneApiKey({
variables: {
data: {
name: formValues.name,
expiresAt: DateTime.now()
.plus({ days: formValues.expirationDate })
.toISODate(),
},
},
});
setGeneratedApiKey(apiKey.data?.createOneApiKey?.token);
navigate(
`/settings/developers/api-keys/${apiKey.data?.createOneApiKey?.id}`,
);
};
const canSave = !!formValues.name;
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'APIs', href: '/settings/developers/api-keys' },
{ children: 'New' },
]}
/>
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigate('/settings/developers/api-keys');
}}
onSave={onSave}
/>
</SettingsHeaderContainer>
<Section>
<H2Title title="Name" description="Name of your API key" />
<TextInput
placeholder="E.g. backoffice integration"
value={formValues.name}
onChange={(value) => {
setFormValues((prevState) => ({
...prevState,
name: value,
}));
}}
fullWidth
/>
</Section>
<Section>
<H2Title
title="Expiration Date"
description="When the API key will expire."
/>
<Select
dropdownScopeId="object-field-type-select"
options={ExpirationDates}
value={formValues.expirationDate}
onChange={(value) => {
setFormValues((prevState) => ({
...prevState,
expirationDate: value,
}));
}}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys',
component: SettingsDevelopersApiKeys,
decorators: [PageDecorator],
args: { routePath: '/settings/developers/api-keys' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsDevelopersApiKeys>;
export const Default: Story = {
play: async ({}) => {
await sleep(100);
},
};

View File

@ -0,0 +1,32 @@
import { Meta, StoryObj } from '@storybook/react';
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
import {
PageDecorator,
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> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeyDetail',
component: SettingsDevelopersApiKeyDetail,
decorators: [PageDecorator],
args: {
routePath: '/settings/apis/f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
state: mockedApiKeyToken,
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsDevelopersApiKeyDetail>;
export const Default: Story = {
play: async ({}) => {
await sleep(100);
},
};

View File

@ -0,0 +1,29 @@
import { Meta, StoryObj } from '@storybook/react';
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeysNew',
component: SettingsDevelopersApiKeysNew,
decorators: [PageDecorator],
args: { routePath: '/settings/developers/api-keys/new' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsDevelopersApiKeysNew>;
export const Default: Story = {
play: async ({}) => {
await sleep(100);
},
};