Feat: API Playground (#10376)

/claim #10283

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
oliver
2025-03-07 09:03:57 -08:00
committed by GitHub
parent d1518764a8
commit fc287dac78
55 changed files with 2915 additions and 163 deletions

View File

@ -1,7 +1,7 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { SettingsDevelopers } from '~/pages/settings/developers/SettingsDevelopers';
import { SettingsApiKeys } from '~/pages/settings/developers/api-keys/SettingsApiKeys';
import {
PageDecorator,
PageDecoratorArgs,
@ -9,10 +9,10 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/SettingsDevelopers',
component: SettingsDevelopers,
title: 'Pages/Settings/ApiKeys',
component: SettingsApiKeys,
decorators: [PageDecorator],
args: { routePath: '/settings/developers' },
args: { routePath: '/settings/apis' },
parameters: {
msw: graphqlMocks,
},
@ -20,7 +20,7 @@ const meta: Meta<PageDecoratorArgs> = {
export default meta;
export type Story = StoryObj<typeof SettingsDevelopers>;
export type Story = StoryObj<typeof SettingsApiKeys>;
export const Default: Story = {
play: async ({ canvasElement }) => {

View File

@ -11,7 +11,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/utils/sleep';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeyDetail',
title: 'Pages/Settings/ApiKeys/SettingsDevelopersApiKeyDetail',
component: SettingsDevelopersApiKeyDetail,
decorators: [PageDecorator],
args: {

View File

@ -11,7 +11,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeysNew',
title: 'Pages/Settings/ApiKeys/SettingsDevelopersApiKeysNew',
component: SettingsDevelopersApiKeysNew,
decorators: [PageDecorator],
args: { routePath: getSettingsPath(SettingsPath.DevelopersNewApiKey) },

View File

@ -0,0 +1,34 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator, ComponentWithRouterDecorator } from 'twenty-ui';
import { SettingsGraphQLPlayground } from '~/pages/settings/developers/playground/SettingsGraphQLPlayground';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<any> = {
title: 'Pages/Settings/Playground/GraphQLPlayground',
component: SettingsGraphQLPlayground,
decorators: [
ComponentDecorator,
I18nFrontDecorator,
ComponentWithRouterDecorator,
],
parameters: {
docs: {
description: {
component:
'GraphQLPlayground provides an interactive environment to test GraphQL queries with authentication.',
},
},
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<any>;
export const Default: Story = {
args: {
onError: action('GraphQL Playground encountered unexpected error'),
},
};

View File

@ -0,0 +1,36 @@
import { action } from '@storybook/addon-actions';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator, ComponentWithRouterDecorator } from 'twenty-ui';
import { SettingsRestPlayground } from '~/pages/settings/developers/playground/SettingsRestPlayground';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<typeof SettingsRestPlayground> = {
title: 'Pages/Settings/Playground/RestPlayground',
component: SettingsRestPlayground,
decorators: [
ComponentDecorator,
I18nFrontDecorator,
ComponentWithRouterDecorator,
],
parameters: {
docs: {
description: {
component:
'RestPlayground provides an interactive environment to test Open API queries with authentication.',
},
},
msw: {
handlers: graphqlMocks,
},
},
};
export default meta;
type Story = StoryObj<typeof SettingsRestPlayground>;
export const Default: Story = {
args: {
onError: action('Rest Playground encountered unexpected error'),
},
};

View File

@ -10,7 +10,7 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Developers/Webhooks/SettingsDevelopersWebhooksDetail',
title: 'Pages/Settings/Webhooks/SettingsDevelopersWebhooksDetail',
component: SettingsDevelopersWebhooksDetail,
decorators: [PageDecorator],
args: {

View File

@ -0,0 +1,33 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { SettingsWebhooks } from '~/pages/settings/developers/webhooks/components/SettingsWebhooks';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/Webhooks',
component: SettingsWebhooks,
decorators: [PageDecorator],
args: { routePath: '/settings/webhooks' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsWebhooks>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Webhooks', undefined, {
timeout: 3000,
});
},
};

View File

@ -0,0 +1,74 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
import { PlaygroundSetupForm } from '@/settings/playground/components/PlaygroundSetupForm';
import { StyledSettingsApiPlaygroundCoverImage } from '@/settings/playground/components/SettingsPlaygroundCoverImage';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { Button, H2Title, IconPlus, MOBILE_VIEWPORT, Section } from 'twenty-ui';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-top: ${({ theme }) => theme.spacing(5)};
}
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsApiKeys = () => {
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`APIs`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>APIs</Trans> },
]}
>
<SettingsPageContainer>
<StyledContainer>
<Section>
<H2Title
title={t`Playground`}
description={t`Try our REST or GraphQL API playgrounds.`}
/>
<StyledSettingsApiPlaygroundCoverImage />
<PlaygroundSetupForm />
</Section>
</StyledContainer>
<StyledContainer>
<Section>
<H2Title
title={t`API keys`}
description={t`Active API keys created by you or your team.`}
/>
<SettingsApiKeysTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title={t`Create API key`}
size="small"
variant="secondary"
to={getSettingsPath(SettingsPath.DevelopersNewApiKey)}
/>
</StyledButtonContainer>
</Section>
</StyledContainer>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -90,7 +90,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
updateOneRecordInput: { revokedAt: DateTime.now().toString() },
});
if (redirect) {
navigate(SettingsPath.Developers);
navigate(SettingsPath.APIs);
}
} catch (err) {
enqueueSnackBar(t`Error deleting api key: ${err}`, {
@ -166,8 +166,8 @@ export const SettingsDevelopersApiKeyDetail = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Developers`,
href: getSettingsPath(SettingsPath.Developers),
children: t`APIs`,
href: getSettingsPath(SettingsPath.APIs),
},
{ children: t`${apiKeyName} API Key` },
]}

View File

@ -86,8 +86,8 @@ export const SettingsDevelopersApiKeysNew = () => {
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Developers`,
href: getSettingsPath(SettingsPath.Developers),
children: t`APIs`,
href: getSettingsPath(SettingsPath.APIs),
},
{ children: t`New Key` },
]}
@ -95,7 +95,7 @@ export const SettingsDevelopersApiKeysNew = () => {
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigateSettings(SettingsPath.Developers);
navigateSettings(SettingsPath.APIs);
}}
onSave={handleSave}
/>

View File

@ -0,0 +1,47 @@
import { GraphQLPlayground } from '@/settings/playground/components/GraphQLPlayground';
import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas';
import { SettingsPath } from '@/types/SettingsPath';
import { FullScreenContainer } from '@/ui/layout/fullscreen/components/FullScreenContainer';
import { Trans } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsGraphQLPlayground = () => {
const navigateSettings = useNavigateSettings();
const { schema: urlSchema = 'core' } = useParams<{ schema: string }>();
// Convert lowercase URL parameter to PlaygroundSchemas enum
const schema =
urlSchema === 'metadata'
? PlaygroundSchemas.METADATA
: PlaygroundSchemas.CORE;
const handleExitFullScreen = () => {
navigateSettings(SettingsPath.APIs);
};
const handleOnError = () => {
handleExitFullScreen();
};
return (
<FullScreenContainer
exitFullScreen={handleExitFullScreen}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>APIs</Trans>,
href: getSettingsPath(SettingsPath.APIs),
},
{ children: <Trans>GraphQL API Playground</Trans> },
]}
>
<GraphQLPlayground onError={handleOnError} schema={schema} />
</FullScreenContainer>
);
};

View File

@ -0,0 +1,41 @@
import { RestPlayground } from '@/settings/playground/components/RestPlayground';
import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas';
import { SettingsPath } from '@/types/SettingsPath';
import { FullScreenContainer } from '@/ui/layout/fullscreen/components/FullScreenContainer';
import { Trans } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const SettingsRestPlayground = () => {
const navigateSettings = useNavigateSettings();
const { schema = PlaygroundSchemas.CORE } = useParams<{
schema: PlaygroundSchemas;
}>();
const handleExitFullScreen = () => {
navigateSettings(SettingsPath.APIs);
};
return (
<FullScreenContainer
exitFullScreen={handleExitFullScreen}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>APIs</Trans>,
href: getSettingsPath(SettingsPath.APIs),
},
{ children: <Trans>REST</Trans> },
]}
>
<RestPlayground
schema={schema}
onError={() => navigateSettings(SettingsPath.APIs)}
/>
</FullScreenContainer>
);
};

View File

@ -128,10 +128,6 @@ export const SettingsDevelopersWebhooksDetail = () => {
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: t`Developers`,
href: getSettingsPath(SettingsPath.Developers),
},
{ children: t`Webhook` },
]}
>

View File

@ -1,7 +1,4 @@
import { v4 } from 'uuid';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
import { SettingsWebhooksTable } from '@/settings/developers/components/SettingsWebhooksTable';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
@ -9,6 +6,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { Button, H2Title, IconPlus, MOBILE_VIEWPORT, Section } from 'twenty-ui';
import { v4 } from 'uuid';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledButtonContainer = styled.div`
@ -27,40 +25,23 @@ const StyledContainer = styled.div<{ isMobile: boolean }>`
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsDevelopers = () => {
export const SettingsWebhooks = () => {
const isMobile = useIsMobile();
const { t } = useLingui();
return (
<SubMenuTopBarContainer
title={t`Developers`}
actionButton={<SettingsReadDocumentationButton />}
title={t`Webhooks`}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Developers</Trans> },
{ children: <Trans>Webhooks</Trans> },
]}
>
<SettingsPageContainer>
<StyledContainer isMobile={isMobile}>
<Section>
<H2Title
title={t`API keys`}
description={t`Active API keys created by you or your team.`}
/>
<SettingsApiKeysTable />
<StyledButtonContainer>
<Button
Icon={IconPlus}
title={t`Create API key`}
size="small"
variant="secondary"
to={getSettingsPath(SettingsPath.DevelopersNewApiKey)}
/>
</StyledButtonContainer>
</Section>
<Section>
<H2Title
title={t`Webhooks`}

View File

@ -3,15 +3,14 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { H2Title, IconLock, Section, Tag } from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledContainer = styled.div`
width: 100%;
@ -38,7 +37,6 @@ export const SettingsSecurity = () => {
return (
<SubMenuTopBarContainer
title={t`Security`}
actionButton={<SettingsReadDocumentationButton />}
links={[
{
children: <Trans>Workspace</Trans>,