diff --git a/packages/twenty-docs/docs/contributor/server/others/zapier.mdx b/packages/twenty-docs/docs/contributor/server/others/zapier.mdx index ad21df25f..c57adb625 100644 --- a/packages/twenty-docs/docs/contributor/server/others/zapier.mdx +++ b/packages/twenty-docs/docs/contributor/server/others/zapier.mdx @@ -37,7 +37,7 @@ From the `packages/twenty-zapier` folder, run: ```bash cp .env.example .env ``` -Run the application locally, go to [http://localhost:3000/settings/developers/api-keys](http://localhost:3000/settings/developers/api-keys), and generate an API key. +Run the application locally, go to [http://localhost:3000/settings/developers](http://localhost:3000/settings/developers/api-keys), and generate an API key. Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated. diff --git a/packages/twenty-docs/src/components/token-form.tsx b/packages/twenty-docs/src/components/token-form.tsx index b39f5603c..3e499a2df 100644 --- a/packages/twenty-docs/src/components/token-form.tsx +++ b/packages/twenty-docs/src/components/token-form.tsx @@ -1,68 +1,68 @@ import React, { useEffect, useState } from 'react'; -import { parseJson } from 'nx/src/utils/json'; -import tokenForm from '!css-loader!./token-form.css'; import { TbLoader2 } from 'react-icons/tb'; +import { parseJson } from 'nx/src/utils/json'; + +import tokenForm from '!css-loader!./token-form.css'; export type TokenFormProps = { - setOpenApiJson?: (json: object) => void, - setToken?: (token: string) => void, - isTokenValid: boolean, - setIsTokenValid: (boolean) => void, -} + setOpenApiJson?: (json: object) => void; + setToken?: (token: string) => void; + isTokenValid: boolean; + setIsTokenValid: (boolean) => void; +}; -const TokenForm = ( - { - setOpenApiJson, - setToken, - isTokenValid, - setIsTokenValid, - }: TokenFormProps -) => { - const [isLoading, setIsLoading] = useState(false) - const token = parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? '' +const TokenForm = ({ + setOpenApiJson, + setToken, + isTokenValid, + setIsTokenValid, +}: TokenFormProps) => { + const [isLoading, setIsLoading] = useState(false); + const token = + parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? + ''; const updateToken = async (event: React.ChangeEvent) => { localStorage.setItem( 'TryIt_securitySchemeValues', - JSON.stringify({bearerAuth: event.target.value}), - ) - await submitToken(event.target.value) - } + JSON.stringify({ bearerAuth: event.target.value }), + ); + await submitToken(event.target.value); + }; - const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags) + const validateToken = (openApiJson) => setIsTokenValid(!!openApiJson.tags); - const getJson = async (token: string ) => { - setIsLoading(true) + const getJson = async (token: string) => { + setIsLoading(true); - return await fetch( - 'https://api.twenty.com/open-api', - {headers: {Authorization: `Bearer ${token}`}} - ) - .then((res)=> res.json()) - .then((result)=> { - validateToken(result) - setIsLoading(false) + return await fetch('https://api.twenty.com/open-api', { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((res) => res.json()) + .then((result) => { + validateToken(result); + setIsLoading(false); - return result + return result; }) - .catch(() => setIsLoading(false)) - } + .catch(() => setIsLoading(false)); + }; const submitToken = async (token) => { - if (isLoading) return + if (isLoading) return; - const json = await getJson(token) + const json = await getJson(token); - setToken && setToken(token) + setToken && setToken(token); - setOpenApiJson && setOpenApiJson(json) - } + setOpenApiJson && setOpenApiJson(json); + }; - useEffect(()=> { - (async ()=> { - await submitToken(token) - })() - },[]) + useEffect(() => { + (async () => { + await submitToken(token); + })(); + }, []); // We load playground style using useEffect as it breaks remaining docs style useEffect(() => { @@ -73,31 +73,48 @@ const TokenForm = ( return () => styleElement.remove(); }, []); - return !isTokenValid && ( -
-
-
- -

- - Token invalid -

- -
-

-
+ return ( + !isTokenValid && ( +
+
+
+ +

+ + + Token invalid + +

+ +
+

+
+
-
- ) -} + ) + ); +}; export default TokenForm; diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 8f17069e2..33e6f2676 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -30,9 +30,9 @@ import { SettingsObjectFieldEdit } from '~/pages/settings/data-model/SettingsObj import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1'; import { SettingsObjectNewFieldStep2 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2'; import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects'; -import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail'; -import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys'; -import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew'; +import { SettingsDevelopers } from '~/pages/settings/developers/SettingsDevelopers'; +import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/SettingsDevelopersApiKeyDetail'; +import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/SettingsDevelopersApiKeysNew'; import { SettingsAppearance } from '~/pages/settings/SettingsAppearance'; import { SettingsProfile } from '~/pages/settings/SettingsProfile'; import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace'; @@ -135,7 +135,7 @@ export const App = () => { } + element={} /> { /> onClick()}> {fieldItem.name} - Internal{' '} theme.spacing(1)}; +`; + +const StyledUrlTableCell = styled(TableCell)` + color: ${({ theme }) => theme.font.color.primary}; + overflow-x: scroll; + white-space: nowrap; +`; + +const StyledIconChevronRight = styled(IconChevronRight)` + color: ${({ theme }) => theme.font.color.tertiary}; +`; + +export const SettingsDevelopersWebhookTableRow = ({ + fieldItem, + onClick, +}: { + fieldItem: WebhookFieldItem; + onClick: () => void; +}) => { + const theme = useTheme(); + + const soon = true; // Temporarily disabled while awaiting the development of the feature. + const onClickAction = !soon ? () => onClick() : undefined; + + return ( + + {fieldItem.targetUrl} + + + {soon && } + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/developers/types/WebhookFieldItem.ts b/packages/twenty-front/src/modules/settings/developers/types/WebhookFieldItem.ts new file mode 100644 index 000000000..593c356ff --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/types/WebhookFieldItem.ts @@ -0,0 +1,5 @@ +export type WebhookFieldItem = { + id: string; + targetUrl: string; + operation: string; +}; diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index 88a08a4aa..a73126399 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -14,7 +14,7 @@ export enum SettingsPath { NewObject = 'objects/new', WorkspaceMembersPage = 'workspace-members', Workspace = 'workspace', - Developers = 'api-keys', + Developers = '', DevelopersNewApiKey = 'api-keys/new', DevelopersApiKeyDetail = 'api-keys/:apiKeyId', } diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx new file mode 100644 index 000000000..0e304cb7b --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx @@ -0,0 +1,18 @@ +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { IconSettings } from '@/ui/display/icon'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/SettingsDevelopersApiKeys'; +import { SettingsDevelopersWebhooks } from '~/pages/settings/developers/SettingsDevelopersWebhooks'; + +export const SettingsDevelopers = () => { + return ( + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeyDetail.tsx similarity index 99% rename from packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx rename to packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeyDetail.tsx index d647821c9..5e1057669 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeyDetail.tsx @@ -70,7 +70,7 @@ export const SettingsDevelopersApiKeyDetail = () => { }); performOptimisticEvict('ApiKey', 'id', apiKeyId); if (redirect) { - navigate('/settings/developers/api-keys'); + navigate('/settings/developers'); } }; @@ -131,7 +131,7 @@ export const SettingsDevelopersApiKeyDetail = () => { diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeys.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeys.tsx new file mode 100644 index 000000000..4946e5254 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeys.tsx @@ -0,0 +1,81 @@ +import { useNavigate } from 'react-router-dom'; +import styled from '@emotion/styled'; + +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/ApiFieldItem'; +import { ApiKey } from '@/settings/developers/types/ApiKey'; +import { formatExpirations } from '@/settings/developers/utils/format-expiration'; +import { IconPlus } from '@/ui/display/icon'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; +import { Button } from '@/ui/input/button/components/Button'; +import { Section } from '@/ui/layout/section/components/Section'; +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'; + +const StyledDiv = styled.div` + display: flex; + justify-content: flex-end; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTableRow = styled(TableRow)` + grid-template-columns: 312px 132px 68px; +`; + +const StyledTableBody = styled(TableBody)` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + +export const SettingsDevelopersApiKeys = () => { + const navigate = useNavigate(); + + const { records: apiKeys } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.ApiKey, + filter: { revokedAt: { is: 'NULL' } }, + orderBy: {}, + }); + + return ( +
+ + + + Name + Expiration + + + {!!apiKeys.length && ( + + {formatExpirations(apiKeys as ApiKey[]).map((fieldItem) => ( + { + navigate(`/settings/developers/api-keys/${fieldItem.id}`); + }} + /> + ))} + + )} +
+ +
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeysNew.tsx similarity index 96% rename from packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx rename to packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeysNew.tsx index b3efb82d6..b672ebab2 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersApiKeysNew.tsx @@ -66,14 +66,14 @@ export const SettingsDevelopersApiKeysNew = () => { { - navigate('/settings/developers/api-keys'); + navigate('/settings/developers'); }} onSave={onSave} /> diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersWebhooks.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersWebhooks.tsx new file mode 100644 index 000000000..efd1a666b --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopersWebhooks.tsx @@ -0,0 +1,78 @@ +import { useNavigate } from 'react-router-dom'; +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 { WebhookFieldItem } from '@/settings/developers/types/WebhookFieldItem'; +import { IconPlus } from '@/ui/display/icon'; +import { H2Title } from '@/ui/display/typography/components/H2Title'; +import { Button } from '@/ui/input/button/components/Button'; +import { Section } from '@/ui/layout/section/components/Section'; +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'; + +const StyledDiv = styled.div` + display: flex; + justify-content: flex-end; + padding-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTableRow = styled(TableRow)` + grid-template-columns: 444px 68px; +`; + +const StyledTableBody = styled(TableBody)` + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; +`; + +export const SettingsDevelopersWebhooks = () => { + const navigate = useNavigate(); + + const { records: webhooks } = useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.Webhook, + orderBy: {}, + }); + + return ( +
+ + + + Url + + + {!!webhooks.length && ( + + {webhooks.map((fieldItem) => ( + { + navigate(`/settings/developers/webhooks/${fieldItem.id}`); + }} + /> + ))} + + )} +
+ +
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopers.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopers.stories.tsx new file mode 100644 index 000000000..80c140b43 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopers.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { SettingsDevelopers } from '~/pages/settings/developers/SettingsDevelopers'; +import { + PageDecorator, + PageDecoratorArgs, +} from '~/testing/decorators/PageDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { sleep } from '~/testing/sleep'; + +const meta: Meta = { + title: 'Pages/Settings/Developers/SettingsDevelopers', + component: SettingsDevelopers, + decorators: [PageDecorator], + args: { routePath: '/settings/developers' }, + parameters: { + msw: graphqlMocks, + }, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + play: async () => { + await sleep(100); + }, +}; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeys.stories.tsx similarity index 87% rename from packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx rename to packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeys.stories.tsx index 3e4d8cf34..bad6956b1 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeys.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys'; +import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/SettingsDevelopersApiKeys'; import { PageDecorator, PageDecoratorArgs, @@ -12,7 +12,7 @@ const meta: Meta = { title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys', component: SettingsDevelopersApiKeys, decorators: [PageDecorator], - args: { routePath: '/settings/developers/api-keys' }, + args: { routePath: '/settings/developers' }, parameters: { msw: graphqlMocks, }, diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx similarity index 93% rename from packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx rename to packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx index f4470b344..60175a3b5 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail'; +import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/SettingsDevelopersApiKeyDetail'; import { PageDecorator, PageDecoratorArgs, diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysNew.stories.tsx similarity index 93% rename from packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx rename to packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysNew.stories.tsx index 11846c79a..fcc0e6f0a 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersApiKeysNew.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew'; +import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/SettingsDevelopersApiKeysNew'; import { PageDecorator, PageDecoratorArgs, diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersWebhooks.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersWebhooks.stories.tsx new file mode 100644 index 000000000..8ea6c81d8 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/SettingsDevelopersWebhooks.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { SettingsDevelopersWebhooks } from '~/pages/settings/developers/SettingsDevelopersWebhooks'; +import { + PageDecorator, + PageDecoratorArgs, +} from '~/testing/decorators/PageDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { sleep } from '~/testing/sleep'; + +const meta: Meta = { + title: 'Pages/Settings/Developers/SettingsDevelopersWebhooks', + component: SettingsDevelopersWebhooks, + decorators: [PageDecorator], + args: { routePath: '/settings/developers' }, + parameters: { + msw: graphqlMocks, + }, +}; + +export default meta; + +export type Story = StoryObj; + +export const Default: Story = { + play: async () => { + await sleep(100); + }, +}; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx deleted file mode 100644 index 881b0b4f6..000000000 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import styled from '@emotion/styled'; - -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow'; -import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem'; -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'; -import { Button } from '@/ui/input/button/components/Button'; -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'; - -const StyledContainer = styled.div` - height: fit-content; -`; - -const StyledTableRow = styled(TableRow)` - grid-template-columns: 180px 98.7px 98.7px 98.7px 36px; -`; - -const StyledHeader = styled.div` - align-items: center; - display: flex; - justify-content: space-between; - margin-bottom: ${({ theme }) => theme.spacing(8)}; -`; - -const StyledH1Title = styled(H1Title)` - margin-bottom: 0; -`; - -export const SettingsDevelopersApiKeys = () => { - const navigate = useNavigate(); - - const [apiKeys, setApiKeys] = useState>([]); - - useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.ApiKey, - filter: { revokedAt: { is: 'NULL' } }, - orderBy: {}, - onCompleted: (data) => { - setApiKeys( - formatExpirations( - data.edges.map((apiKey) => ({ - id: apiKey.node.id, - name: apiKey.node.name, - expiresAt: apiKey.node.expiresAt, - })), - ), - ); - }, - }); - - return ( - - - - - -