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:
@ -2,11 +2,9 @@ import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useUpdateApiKeyMutation } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledComboInputContainer = styled.div`
|
||||
display: flex;
|
||||
@ -29,9 +27,7 @@ export const ApiKeyNameInput = ({
|
||||
disabled,
|
||||
onNameUpdate,
|
||||
}: ApiKeyNameInputProps) => {
|
||||
const { updateOneRecord: updateApiKey } = useUpdateOneRecord<ApiKey>({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
});
|
||||
const [updateApiKey] = useUpdateApiKeyMutation();
|
||||
|
||||
// TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -43,10 +39,18 @@ export const ApiKeyNameInput = ({
|
||||
if (!apiKeyName) {
|
||||
return;
|
||||
}
|
||||
await updateApiKey({
|
||||
idToUpdate: apiKeyId,
|
||||
updateOneRecordInput: { name },
|
||||
const { data: updatedApiKeyData } = await updateApiKey({
|
||||
variables: {
|
||||
input: {
|
||||
id: apiKeyId,
|
||||
name,
|
||||
},
|
||||
},
|
||||
});
|
||||
const updatedApiKey = updatedApiKeyData?.updateApiKey;
|
||||
if (isDefined(updatedApiKey)) {
|
||||
onNameUpdate?.(updatedApiKey.name);
|
||||
}
|
||||
}, 500),
|
||||
[updateApiKey, onNameUpdate],
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ApiFieldItem } from '@/settings/developers/types/api-key/ApiFieldItem';
|
||||
import { formatExpiration } from '@/settings/developers/utils/formatExpiration';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import {
|
||||
@ -9,6 +9,7 @@ import {
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
import { ApiKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||
grid-template-columns: 312px auto 28px;
|
||||
@ -34,27 +35,28 @@ const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
`;
|
||||
|
||||
export const SettingsApiKeysFieldItemTableRow = ({
|
||||
fieldItem,
|
||||
apiKey,
|
||||
to,
|
||||
}: {
|
||||
fieldItem: ApiFieldItem;
|
||||
apiKey: Pick<ApiKey, 'id' | 'name' | 'expiresAt' | 'revokedAt'>;
|
||||
to: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const formattedExpiration = formatExpiration(apiKey.expiresAt || null);
|
||||
|
||||
return (
|
||||
<StyledApisFieldTableRow to={to}>
|
||||
<StyledNameTableCell>
|
||||
<OverflowingTextWithTooltip text={fieldItem.name} />
|
||||
<OverflowingTextWithTooltip text={apiKey.name} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell
|
||||
color={
|
||||
fieldItem.expiration === 'Expired'
|
||||
formattedExpiration === 'Expired'
|
||||
? theme.font.color.danger
|
||||
: theme.font.color.tertiary
|
||||
}
|
||||
>
|
||||
{fieldItem.expiration}
|
||||
{formattedExpiration}
|
||||
</TableCell>
|
||||
<StyledIconTableCell>
|
||||
<StyledIconChevronRight
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
|
||||
import { ApiFieldItem } from '@/settings/developers/types/api-key/ApiFieldItem';
|
||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||
import { formatExpirations } from '@/settings/developers/utils/formatExpiration';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
@ -12,6 +7,7 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
import { useGetApiKeysQuery } from '~/generated-metadata/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
@ -33,10 +29,9 @@ const StyledTableRow = styled(TableRow)`
|
||||
`;
|
||||
|
||||
export const SettingsApiKeysTable = () => {
|
||||
const { records: apiKeys } = useFindManyRecords<ApiKey>({
|
||||
objectNameSingular: CoreObjectNameSingular.ApiKey,
|
||||
filter: { revokedAt: { is: 'NULL' } },
|
||||
});
|
||||
const { data: apiKeysData } = useGetApiKeysQuery();
|
||||
|
||||
const apiKeys = apiKeysData?.apiKeys;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
@ -49,14 +44,14 @@ export const SettingsApiKeysTable = () => {
|
||||
</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
{!!apiKeys.length && (
|
||||
{!!apiKeys?.length && (
|
||||
<StyledTableBody>
|
||||
{formatExpirations(apiKeys).map((fieldItem) => (
|
||||
{apiKeys.map((apiKey) => (
|
||||
<SettingsApiKeysFieldItemTableRow
|
||||
key={fieldItem.id}
|
||||
fieldItem={fieldItem as ApiFieldItem}
|
||||
key={apiKey.id}
|
||||
apiKey={apiKey}
|
||||
to={getSettingsPath(SettingsPath.ApiKeyDetail, {
|
||||
apiKeyId: fieldItem.id,
|
||||
apiKeyId: apiKey.id,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -79,7 +79,7 @@ export const SettingsDevelopersWebhookForm = ({
|
||||
handleSave,
|
||||
updateOperation,
|
||||
removeOperation,
|
||||
deleteWebhook,
|
||||
handleDelete,
|
||||
isCreationMode,
|
||||
error,
|
||||
} = useWebhookForm({ webhookId, mode });
|
||||
@ -285,7 +285,7 @@ export const SettingsDevelopersWebhookForm = ({
|
||||
Please type "yes" to confirm you want to delete this webhook.
|
||||
</Trans>
|
||||
}
|
||||
onConfirmClick={deleteWebhook}
|
||||
onConfirmClick={handleDelete}
|
||||
confirmButtonText={t`Delete`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { getUrlHostnameOrThrow, isValidUrl } from 'twenty-shared/utils';
|
||||
import { IconChevronRight } from 'twenty-ui/display';
|
||||
import { Webhook } from '~/generated-metadata/graphql';
|
||||
|
||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||
grid-template-columns: 1fr 28px;
|
||||
@ -28,10 +28,13 @@ const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
`;
|
||||
|
||||
export const SettingsDevelopersWebhookTableRow = ({
|
||||
fieldItem,
|
||||
webhook,
|
||||
to,
|
||||
}: {
|
||||
fieldItem: Webhook;
|
||||
webhook: Pick<
|
||||
Webhook,
|
||||
'id' | 'targetUrl' | 'operations' | 'description' | 'secret'
|
||||
>;
|
||||
to: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@ -39,9 +42,9 @@ export const SettingsDevelopersWebhookTableRow = ({
|
||||
return (
|
||||
<StyledApisFieldTableRow to={to}>
|
||||
<StyledUrlTableCell>
|
||||
{isValidUrl(fieldItem.targetUrl)
|
||||
? getUrlHostnameOrThrow(fieldItem.targetUrl)
|
||||
: fieldItem.targetUrl}
|
||||
{isValidUrl(webhook.targetUrl)
|
||||
? getUrlHostnameOrThrow(webhook.targetUrl)
|
||||
: webhook.targetUrl}
|
||||
</StyledUrlTableCell>
|
||||
<StyledIconTableCell>
|
||||
<StyledIconChevronRight
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsDevelopersWebhookTableRow } from '@/settings/developers/components/SettingsDevelopersWebhookTableRow';
|
||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useGetWebhooksQuery } from '~/generated-metadata/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
@ -22,9 +20,9 @@ const StyledTableRow = styled(TableRow)`
|
||||
`;
|
||||
|
||||
export const SettingsWebhooksTable = () => {
|
||||
const { records: webhooks } = useFindManyRecords<Webhook>({
|
||||
objectNameSingular: CoreObjectNameSingular.Webhook,
|
||||
});
|
||||
const { data: webhooksData } = useGetWebhooksQuery();
|
||||
|
||||
const webhooks = webhooksData?.webhooks;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
@ -32,12 +30,12 @@ export const SettingsWebhooksTable = () => {
|
||||
<TableHeader>URL</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
{!!webhooks.length && (
|
||||
{!!webhooks?.length && (
|
||||
<StyledTableBody>
|
||||
{webhooks.map((webhookFieldItem) => (
|
||||
<SettingsDevelopersWebhookTableRow
|
||||
key={webhookFieldItem.id}
|
||||
fieldItem={webhookFieldItem}
|
||||
webhook={webhookFieldItem}
|
||||
to={getSettingsPath(SettingsPath.WebhookDetail, {
|
||||
webhookId: webhookFieldItem.id,
|
||||
})}
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
|
||||
|
||||
const meta: Meta<typeof SettingsApiKeysFieldItemTableRow> = {
|
||||
title: 'Modules/Settings/Developers/ApiKeys/SettingsApiKeysFieldItemTableRow',
|
||||
component: SettingsApiKeysFieldItemTableRow,
|
||||
decorators: [ComponentDecorator],
|
||||
decorators: [ComponentDecorator, RouterDecorator],
|
||||
args: {
|
||||
fieldItem: {
|
||||
apiKey: {
|
||||
id: '3f4a42e8-b81f-4f8c-9c20-1602e6b34791',
|
||||
name: 'Zapier Api Key',
|
||||
type: 'internal',
|
||||
expiration: 'In 3 days',
|
||||
expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days from now
|
||||
revokedAt: null,
|
||||
},
|
||||
to: '/settings/developers/api-keys/3f4a42e8-b81f-4f8c-9c20-1602e6b34791',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ export const CreateMode: 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.findByPlaceholderText('https://example.com/webhook');
|
||||
await canvas.findByPlaceholderText('Write a description');
|
||||
|
||||
@ -48,15 +48,21 @@ export const EditMode: Story = {
|
||||
mode: WebhookFormMode.Edit,
|
||||
webhookId: '1234',
|
||||
},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByDisplayValue('https://example.com/webhook', undefined, {
|
||||
timeout: 10000,
|
||||
});
|
||||
await canvas.findByDisplayValue('A Sample Description');
|
||||
await canvas.findByDisplayValue(
|
||||
'https://api.slackbot.io/webhooks/twenty',
|
||||
undefined,
|
||||
{
|
||||
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('Danger zone');
|
||||
await canvas.findByText('Delete this webhook');
|
||||
|
||||
Reference in New Issue
Block a user