Api keys and webhook migration to core (#13011)
TODO: check Zapier trigger records work as expected --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -1,6 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fireEvent, userEvent, within } from '@storybook/test';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
|
||||
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
|
||||
import {
|
||||
@ -21,26 +20,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
...graphqlMocks.handlers,
|
||||
graphql.query('FindOneApiKey', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apiKey: {
|
||||
__typename: 'ApiKey',
|
||||
id: 'f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
|
||||
revokedAt: null,
|
||||
expiresAt: '2024-03-10T09:23:10.511Z',
|
||||
name: 'sfsfdsf',
|
||||
updatedAt: '2024-02-24T10:23:10.673Z',
|
||||
createdAt: '2024-02-24T10:23:10.673Z',
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
},
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
@ -50,14 +30,14 @@ export type Story = StoryObj<typeof SettingsDevelopersApiKeyDetail>;
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('sfsfdsf', undefined, { timeout: 3000 });
|
||||
await canvas.findByText('Zapier Integration', undefined, { timeout: 3000 });
|
||||
},
|
||||
};
|
||||
|
||||
export const RegenerateApiKey: Story = {
|
||||
play: async ({ step }) => {
|
||||
const canvas = within(document.body);
|
||||
await canvas.findByText('sfsfdsf', undefined, { timeout: 3000 });
|
||||
await canvas.findByText('Zapier Integration', undefined, { timeout: 3000 });
|
||||
|
||||
await userEvent.click(await canvas.findByText('Regenerate Key'));
|
||||
|
||||
@ -85,7 +65,7 @@ export const RegenerateApiKey: Story = {
|
||||
export const DeleteApiKey: Story = {
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('sfsfdsf', undefined, { timeout: 3000 });
|
||||
await canvas.findByText('Zapier Integration', undefined, { timeout: 3000 });
|
||||
|
||||
await userEvent.click(await canvas.findByText('Delete'));
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export type Story = StoryObj<typeof SettingsDevelopersWebhookNew>;
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText('New Webhook', undefined, { timeout: 10000 });
|
||||
await canvas.findByText('New Webhook', undefined, { timeout: 3000 });
|
||||
await canvas.findByText(
|
||||
'We will send POST requests to this endpoint for every new event',
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { expect, within } from '@storybook/test';
|
||||
|
||||
import {
|
||||
PageDecorator,
|
||||
@ -28,11 +28,20 @@ export type Story = StoryObj<typeof SettingsDevelopersWebhookDetail>;
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText(
|
||||
'We will send POST requests to this endpoint for every new event',
|
||||
await canvas.findByDisplayValue(
|
||||
'https://api.slackbot.io/webhooks/twenty',
|
||||
undefined,
|
||||
{ timeout: 10000 },
|
||||
{
|
||||
timeout: 3000,
|
||||
},
|
||||
);
|
||||
await canvas.findByDisplayValue('Slack notifications for lead updates');
|
||||
|
||||
const allObjectsLabels = await canvas.findAllByText('All Objects');
|
||||
expect(allObjectsLabels).toHaveLength(2);
|
||||
await canvas.findByText('Created');
|
||||
await canvas.findByText('Updated');
|
||||
|
||||
await canvas.findByText('Delete this webhook');
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||
import { ApiKeyNameInput } from '@/settings/developers/components/ApiKeyNameInput';
|
||||
import { apiKeyTokenFamilyState } from '@/settings/developers/states/apiKeyTokenFamilyState';
|
||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||
import { computeNewExpirationDate } from '@/settings/developers/utils/computeNewExpirationDate';
|
||||
import { formatExpiration } from '@/settings/developers/utils/formatExpiration';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
@ -23,10 +17,16 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title, IconRepeat, IconTrash } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useGenerateApiKeyTokenMutation } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
useCreateApiKeyMutation,
|
||||
useGenerateApiKeyTokenMutation,
|
||||
useGetApiKeyQuery,
|
||||
useRevokeApiKeyMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
@ -67,30 +67,34 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
);
|
||||
|
||||
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
|
||||
const { createOneRecord: createOneApiKey } = useCreateOneRecord<ApiKey>({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
});
|
||||
const { updateOneRecord: updateApiKey } = useUpdateOneRecord<ApiKey>({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
});
|
||||
|
||||
const [apiKeyName, setApiKeyName] = useState('');
|
||||
|
||||
const { record: apiKeyData, loading } = useFindOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
objectRecordId: apiKeyId,
|
||||
onCompleted: (record) => {
|
||||
setApiKeyName(record.name);
|
||||
const [createApiKey] = useCreateApiKeyMutation();
|
||||
const [revokeApiKey] = useRevokeApiKeyMutation();
|
||||
const { data: apiKeyData } = useGetApiKeyQuery({
|
||||
variables: {
|
||||
input: {
|
||||
id: apiKeyId,
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (isDefined(data?.apiKey)) {
|
||||
setApiKeyName(data.apiKey.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const apiKey = apiKeyData?.apiKey;
|
||||
const [apiKeyName, setApiKeyName] = useState('');
|
||||
|
||||
const deleteIntegration = async (redirect = true) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await updateApiKey?.({
|
||||
idToUpdate: apiKeyId,
|
||||
updateOneRecordInput: { revokedAt: DateTime.now().toString() },
|
||||
await revokeApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
id: apiKeyId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (redirect) {
|
||||
navigate(SettingsPath.APIs);
|
||||
@ -106,11 +110,17 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
name: string,
|
||||
newExpiresAt: string | null,
|
||||
) => {
|
||||
const newApiKey = await createOneApiKey?.({
|
||||
name: name,
|
||||
expiresAt: newExpiresAt ?? '',
|
||||
const { data: newApiKeyData } = await createApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
name: name,
|
||||
expiresAt: newExpiresAt ?? '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newApiKey = newApiKeyData?.createApiKey;
|
||||
|
||||
if (!newApiKey) {
|
||||
return;
|
||||
}
|
||||
@ -130,18 +140,18 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
const regenerateApiKey = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isNonEmptyString(apiKeyData?.name)) {
|
||||
if (isNonEmptyString(apiKey?.name)) {
|
||||
const newExpiresAt = computeNewExpirationDate(
|
||||
apiKeyData?.expiresAt,
|
||||
apiKeyData?.createdAt,
|
||||
apiKey?.expiresAt,
|
||||
apiKey?.createdAt,
|
||||
);
|
||||
const apiKey = await createIntegration(apiKeyData?.name, newExpiresAt);
|
||||
const newApiKey = await createIntegration(apiKey?.name, newExpiresAt);
|
||||
await deleteIntegration(false);
|
||||
|
||||
if (isNonEmptyString(apiKey?.token)) {
|
||||
setApiKeyTokenCallback(apiKey.id, apiKey.token);
|
||||
if (isNonEmptyString(newApiKey?.token)) {
|
||||
setApiKeyTokenCallback(newApiKey.id, newApiKey.token);
|
||||
navigate(SettingsPath.ApiKeyDetail, {
|
||||
apiKeyId: apiKey.id,
|
||||
apiKeyId: newApiKey.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -158,9 +168,9 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{apiKeyData?.name && (
|
||||
{apiKey?.name && (
|
||||
<SubMenuTopBarContainer
|
||||
title={apiKeyData?.name}
|
||||
title={apiKey?.name}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@ -196,11 +206,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
onClick={() => openModal(REGENERATE_API_KEY_MODAL_ID)}
|
||||
/>
|
||||
<StyledInfo>
|
||||
{formatExpiration(
|
||||
apiKeyData?.expiresAt || '',
|
||||
true,
|
||||
false,
|
||||
)}
|
||||
{formatExpiration(apiKey?.expiresAt || '', true, false)}
|
||||
</StyledInfo>
|
||||
</StyledInputContainer>
|
||||
</>
|
||||
@ -210,8 +216,8 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
<H2Title title={t`Name`} description={t`Name of your API key`} />
|
||||
<ApiKeyNameInput
|
||||
apiKeyName={apiKeyName}
|
||||
apiKeyId={apiKeyData?.id}
|
||||
disabled={loading}
|
||||
apiKeyId={apiKey?.id}
|
||||
disabled={isLoading}
|
||||
onNameUpdate={setApiKeyName}
|
||||
/>
|
||||
</Section>
|
||||
@ -221,13 +227,9 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
||||
description={t`When the key will be disabled`}
|
||||
/>
|
||||
<TextInput
|
||||
instanceId={`api-key-expiration-${apiKeyData?.id}`}
|
||||
instanceId={`api-key-expiration-${apiKey?.id}`}
|
||||
placeholder={t`E.g. backoffice integration`}
|
||||
value={formatExpiration(
|
||||
apiKeyData?.expiresAt || '',
|
||||
true,
|
||||
false,
|
||||
)}
|
||||
value={formatExpiration(apiKey?.expiresAt || '', true, false)}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { EXPIRATION_DATES } from '@/settings/developers/constants/ExpirationDates';
|
||||
import { apiKeyTokenFamilyState } from '@/settings/developers/states/apiKeyTokenFamilyState';
|
||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
@ -18,7 +15,10 @@ import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useGenerateApiKeyTokenMutation } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
useCreateApiKeyMutation,
|
||||
useGenerateApiKeyTokenMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
@ -34,9 +34,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
name: '',
|
||||
});
|
||||
|
||||
const { createOneRecord: createOneApiKey } = useCreateOneRecord<ApiKey>({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
});
|
||||
const [createApiKey] = useCreateApiKeyMutation();
|
||||
|
||||
const setApiKeyTokenCallback = useRecoilCallback(
|
||||
({ set }) =>
|
||||
@ -51,11 +49,17 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
.plus({ days: formValues.expirationDate ?? 30 })
|
||||
.toString();
|
||||
|
||||
const newApiKey = await createOneApiKey?.({
|
||||
name: formValues.name,
|
||||
expiresAt,
|
||||
const { data: newApiKeyData } = await createApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
name: formValues.name,
|
||||
expiresAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newApiKey = newApiKeyData?.createApiKey;
|
||||
|
||||
if (!newApiKey) {
|
||||
return;
|
||||
}
|
||||
@ -77,7 +81,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
const canSave = !!formValues.name && createOneApiKey;
|
||||
const canSave = !!formValues.name && createApiKey;
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`New key`}
|
||||
|
||||
Reference in New Issue
Block a user