feat: added webhook list section and updated api key section (#3567)
* feat: added webhook list section and updated api key ui * Fix style * Fix webhook style * Update setting path * Add soon pill on not developped features * Code review returns --------- Co-authored-by: Lakshay saini <lakshay.saini@finmo.net> Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
@ -37,7 +37,7 @@ From the `packages/twenty-zapier` folder, run:
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
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.
|
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +1,68 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
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 { TbLoader2 } from 'react-icons/tb';
|
||||||
|
import { parseJson } from 'nx/src/utils/json';
|
||||||
|
|
||||||
|
import tokenForm from '!css-loader!./token-form.css';
|
||||||
|
|
||||||
export type TokenFormProps = {
|
export type TokenFormProps = {
|
||||||
setOpenApiJson?: (json: object) => void,
|
setOpenApiJson?: (json: object) => void;
|
||||||
setToken?: (token: string) => void,
|
setToken?: (token: string) => void;
|
||||||
isTokenValid: boolean,
|
isTokenValid: boolean;
|
||||||
setIsTokenValid: (boolean) => void,
|
setIsTokenValid: (boolean) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const TokenForm = (
|
const TokenForm = ({
|
||||||
{
|
setOpenApiJson,
|
||||||
setOpenApiJson,
|
setToken,
|
||||||
setToken,
|
isTokenValid,
|
||||||
isTokenValid,
|
setIsTokenValid,
|
||||||
setIsTokenValid,
|
}: TokenFormProps) => {
|
||||||
}: TokenFormProps
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
) => {
|
const token =
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ??
|
||||||
const token = parseJson(localStorage.getItem('TryIt_securitySchemeValues'))?.bearerAuth ?? ''
|
'';
|
||||||
|
|
||||||
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'TryIt_securitySchemeValues',
|
'TryIt_securitySchemeValues',
|
||||||
JSON.stringify({bearerAuth: event.target.value}),
|
JSON.stringify({ bearerAuth: event.target.value }),
|
||||||
)
|
);
|
||||||
await submitToken(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 ) => {
|
const getJson = async (token: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
|
|
||||||
return await fetch(
|
return await fetch('https://api.twenty.com/open-api', {
|
||||||
'https://api.twenty.com/open-api',
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
{headers: {Authorization: `Bearer ${token}`}}
|
})
|
||||||
)
|
.then((res) => res.json())
|
||||||
.then((res)=> res.json())
|
.then((result) => {
|
||||||
.then((result)=> {
|
validateToken(result);
|
||||||
validateToken(result)
|
setIsLoading(false);
|
||||||
setIsLoading(false)
|
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
})
|
})
|
||||||
.catch(() => setIsLoading(false))
|
.catch(() => setIsLoading(false));
|
||||||
}
|
};
|
||||||
|
|
||||||
const submitToken = async (token) => {
|
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(()=> {
|
useEffect(() => {
|
||||||
(async ()=> {
|
(async () => {
|
||||||
await submitToken(token)
|
await submitToken(token);
|
||||||
})()
|
})();
|
||||||
},[])
|
}, []);
|
||||||
|
|
||||||
// We load playground style using useEffect as it breaks remaining docs style
|
// We load playground style using useEffect as it breaks remaining docs style
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,31 +73,48 @@ const TokenForm = (
|
|||||||
return () => styleElement.remove();
|
return () => styleElement.remove();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return !isTokenValid && (
|
return (
|
||||||
<div>
|
!isTokenValid && (
|
||||||
<div className='container'>
|
<div>
|
||||||
<form className="form">
|
<div className="container">
|
||||||
<label>
|
<form className="form">
|
||||||
To load your playground schema, <a className='link' href='https://app.twenty.com/settings/developers/api-keys'>generate an API key</a> and paste it here:
|
<label>
|
||||||
</label>
|
To load your playground schema,{' '}
|
||||||
<p>
|
<a
|
||||||
<input
|
className="link"
|
||||||
className={(token && !isLoading) ? 'input invalid' : 'input'}
|
href="https://app.twenty.com/settings/developers"
|
||||||
type='text'
|
>
|
||||||
readOnly={isLoading}
|
generate an API key
|
||||||
placeholder='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD...'
|
</a>{' '}
|
||||||
defaultValue={token}
|
and paste it here:
|
||||||
onChange={updateToken}
|
</label>
|
||||||
/>
|
<p>
|
||||||
<span className={`token-invalid ${(!token || isLoading )&& 'not-visible'}`}>Token invalid</span>
|
<input
|
||||||
<div className='loader-container'>
|
className={token && !isLoading ? 'input invalid' : 'input'}
|
||||||
<TbLoader2 className={`loader ${!isLoading && 'not-visible'}`} />
|
type="text"
|
||||||
</div>
|
readOnly={isLoading}
|
||||||
</p>
|
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD..."
|
||||||
</form>
|
defaultValue={token}
|
||||||
|
onChange={updateToken}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`token-invalid ${
|
||||||
|
(!token || isLoading) && 'not-visible'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Token invalid
|
||||||
|
</span>
|
||||||
|
<div className="loader-container">
|
||||||
|
<TbLoader2
|
||||||
|
className={`loader ${!isLoading && 'not-visible'}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default TokenForm;
|
export default TokenForm;
|
||||||
|
|||||||
@ -30,9 +30,9 @@ import { SettingsObjectFieldEdit } from '~/pages/settings/data-model/SettingsObj
|
|||||||
import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
||||||
import { SettingsObjectNewFieldStep2 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
|
import { SettingsObjectNewFieldStep2 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
|
||||||
import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects';
|
import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects';
|
||||||
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
|
import { SettingsDevelopers } from '~/pages/settings/developers/SettingsDevelopers';
|
||||||
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
|
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/SettingsDevelopersApiKeyDetail';
|
||||||
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
|
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/SettingsDevelopersApiKeysNew';
|
||||||
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
|
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
|
||||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||||
@ -135,7 +135,7 @@ export const App = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.Developers}
|
path={SettingsPath.Developers}
|
||||||
element={<SettingsDevelopersApiKeys />}
|
element={<SettingsDevelopers />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.DevelopersNewApiKey}
|
path={SettingsPath.DevelopersNewApiKey}
|
||||||
|
|||||||
@ -128,11 +128,11 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
/>
|
/>
|
||||||
<NavigationDrawerItem
|
<NavigationDrawerItem
|
||||||
label="Developers"
|
label="Developers"
|
||||||
to="/settings/developers/api-keys"
|
to="/settings/developers"
|
||||||
Icon={IconRobot}
|
Icon={IconRobot}
|
||||||
active={
|
active={
|
||||||
!!useMatch({
|
!!useMatch({
|
||||||
path: useResolvedPath('/settings/developers/api-keys').pathname,
|
path: useResolvedPath('/settings/developers').pathname,
|
||||||
end: true,
|
end: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
|
|||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
|
||||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||||
grid-template-columns: 180px 148px 148px 36px;
|
grid-template-columns: 312px 132px 68px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledNameTableCell = styled(TableCell)`
|
const StyledNameTableCell = styled(TableCell)`
|
||||||
@ -36,7 +36,6 @@ export const SettingsApiKeysFieldItemTableRow = ({
|
|||||||
return (
|
return (
|
||||||
<StyledApisFieldTableRow onClick={() => onClick()}>
|
<StyledApisFieldTableRow onClick={() => onClick()}>
|
||||||
<StyledNameTableCell>{fieldItem.name}</StyledNameTableCell>
|
<StyledNameTableCell>{fieldItem.name}</StyledNameTableCell>
|
||||||
<TableCell color={theme.font.color.tertiary}>Internal</TableCell>{' '}
|
|
||||||
<TableCell
|
<TableCell
|
||||||
color={
|
color={
|
||||||
fieldItem.expiration === 'Expired'
|
fieldItem.expiration === 'Expired'
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { WebhookFieldItem } from '@/settings/developers/types/WebhookFieldItem';
|
||||||
|
import { IconChevronRight } from '@/ui/display/icon';
|
||||||
|
import { SoonPill } from '@/ui/display/pill/components/SoonPill';
|
||||||
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
|
||||||
|
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||||
|
grid-template-columns: 444px 68px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconTableCell = styled(TableCell)`
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: ${({ theme }) => 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 (
|
||||||
|
<StyledApisFieldTableRow onClick={onClickAction}>
|
||||||
|
<StyledUrlTableCell>{fieldItem.targetUrl}</StyledUrlTableCell>
|
||||||
|
<StyledIconTableCell>
|
||||||
|
<StyledIconChevronRight
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
{soon && <SoonPill />}
|
||||||
|
</StyledIconTableCell>
|
||||||
|
</StyledApisFieldTableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export type WebhookFieldItem = {
|
||||||
|
id: string;
|
||||||
|
targetUrl: string;
|
||||||
|
operation: string;
|
||||||
|
};
|
||||||
@ -14,7 +14,7 @@ export enum SettingsPath {
|
|||||||
NewObject = 'objects/new',
|
NewObject = 'objects/new',
|
||||||
WorkspaceMembersPage = 'workspace-members',
|
WorkspaceMembersPage = 'workspace-members',
|
||||||
Workspace = 'workspace',
|
Workspace = 'workspace',
|
||||||
Developers = 'api-keys',
|
Developers = '',
|
||||||
DevelopersNewApiKey = 'api-keys/new',
|
DevelopersNewApiKey = 'api-keys/new',
|
||||||
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<Breadcrumb links={[{ children: 'Developers' }]} />
|
||||||
|
<SettingsDevelopersApiKeys />
|
||||||
|
<SettingsDevelopersWebhooks />
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -70,7 +70,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
|||||||
});
|
});
|
||||||
performOptimisticEvict('ApiKey', 'id', apiKeyId);
|
performOptimisticEvict('ApiKey', 'id', apiKeyId);
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
navigate('/settings/developers/api-keys');
|
navigate('/settings/developers');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
|||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'APIs', href: '/settings/developers/api-keys' },
|
{ children: 'APIs', href: '/settings/developers' },
|
||||||
{ children: apiKeyData.name },
|
{ children: apiKeyData.name },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -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 (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="API keys"
|
||||||
|
description="Active APIs keys created by you or your team."
|
||||||
|
/>
|
||||||
|
<Table>
|
||||||
|
<StyledTableRow>
|
||||||
|
<TableHeader>Name</TableHeader>
|
||||||
|
<TableHeader>Expiration</TableHeader>
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</StyledTableRow>
|
||||||
|
{!!apiKeys.length && (
|
||||||
|
<StyledTableBody>
|
||||||
|
{formatExpirations(apiKeys as ApiKey[]).map((fieldItem) => (
|
||||||
|
<SettingsApiKeysFieldItemTableRow
|
||||||
|
key={fieldItem.id}
|
||||||
|
fieldItem={fieldItem as ApiFieldItem}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/settings/developers/api-keys/${fieldItem.id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledTableBody>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
<StyledDiv>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Create API key"
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/settings/developers/api-keys/new');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledDiv>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -66,14 +66,14 @@ export const SettingsDevelopersApiKeysNew = () => {
|
|||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'APIs', href: '/settings/developers/api-keys' },
|
{ children: 'APIs', href: '/settings/developers' },
|
||||||
{ children: 'New' },
|
{ children: 'New' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
navigate('/settings/developers/api-keys');
|
navigate('/settings/developers');
|
||||||
}}
|
}}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
/>
|
/>
|
||||||
@ -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 (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Webhooks"
|
||||||
|
description="Establish Webhook endpoints for notifications on asynchronous events."
|
||||||
|
/>
|
||||||
|
<Table>
|
||||||
|
<StyledTableRow>
|
||||||
|
<TableHeader>Url</TableHeader>
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</StyledTableRow>
|
||||||
|
{!!webhooks.length && (
|
||||||
|
<StyledTableBody>
|
||||||
|
{webhooks.map((fieldItem) => (
|
||||||
|
<SettingsDevelopersWebhookTableRow
|
||||||
|
key={fieldItem.id}
|
||||||
|
fieldItem={fieldItem as WebhookFieldItem}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/settings/developers/webhooks/${fieldItem.id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledTableBody>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
<StyledDiv>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Create Webhook"
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
soon={true}
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/settings/developers/webhooks/new');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledDiv>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Settings/Developers/SettingsDevelopers',
|
||||||
|
component: SettingsDevelopers,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { routePath: '/settings/developers' },
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof SettingsDevelopers>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async () => {
|
||||||
|
await sleep(100);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
|
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/SettingsDevelopersApiKeys';
|
||||||
import {
|
import {
|
||||||
PageDecorator,
|
PageDecorator,
|
||||||
PageDecoratorArgs,
|
PageDecoratorArgs,
|
||||||
@ -12,7 +12,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
|||||||
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys',
|
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys',
|
||||||
component: SettingsDevelopersApiKeys,
|
component: SettingsDevelopersApiKeys,
|
||||||
decorators: [PageDecorator],
|
decorators: [PageDecorator],
|
||||||
args: { routePath: '/settings/developers/api-keys' },
|
args: { routePath: '/settings/developers' },
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
},
|
},
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
|
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/SettingsDevelopersApiKeyDetail';
|
||||||
import {
|
import {
|
||||||
PageDecorator,
|
PageDecorator,
|
||||||
PageDecoratorArgs,
|
PageDecoratorArgs,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
|
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/SettingsDevelopersApiKeysNew';
|
||||||
import {
|
import {
|
||||||
PageDecorator,
|
PageDecorator,
|
||||||
PageDecoratorArgs,
|
PageDecoratorArgs,
|
||||||
@ -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<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Settings/Developers/SettingsDevelopersWebhooks',
|
||||||
|
component: SettingsDevelopersWebhooks,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { routePath: '/settings/developers' },
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof SettingsDevelopersWebhooks>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async () => {
|
||||||
|
await sleep(100);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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<Array<ApiFieldItem>>([]);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
|
||||||
<SettingsPageContainer>
|
|
||||||
<StyledContainer>
|
|
||||||
<StyledHeader>
|
|
||||||
<StyledH1Title title="APIs" />
|
|
||||||
<Button
|
|
||||||
Icon={IconPlus}
|
|
||||||
title="Create Key"
|
|
||||||
accent="blue"
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
navigate('/settings/developers/api-keys/new');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledHeader>
|
|
||||||
<H2Title
|
|
||||||
title="Active Keys"
|
|
||||||
description="Active APIs keys created by you or your team"
|
|
||||||
/>
|
|
||||||
<Table>
|
|
||||||
<StyledTableRow>
|
|
||||||
<TableHeader>Name</TableHeader>
|
|
||||||
<TableHeader>Type</TableHeader>
|
|
||||||
<TableHeader>Expiration</TableHeader>
|
|
||||||
<TableHeader></TableHeader>
|
|
||||||
</StyledTableRow>
|
|
||||||
{apiKeys.map((fieldItem) => (
|
|
||||||
<SettingsApiKeysFieldItemTableRow
|
|
||||||
key={fieldItem.id}
|
|
||||||
fieldItem={fieldItem}
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/settings/developers/api-keys/${fieldItem.id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Table>
|
|
||||||
</StyledContainer>
|
|
||||||
</SettingsPageContainer>
|
|
||||||
</SubMenuTopBarContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -47,7 +47,7 @@ export class ApiRestQueryBuilderFactory {
|
|||||||
|
|
||||||
if (!objectMetadataItems.length) {
|
if (!objectMetadataItems.length) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.environmentService.getFrontBaseUrl()}/settings/developers/api-keys`,
|
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.environmentService.getFrontBaseUrl()}/settings/developers`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -158,7 +158,7 @@ const TokenForm = ({
|
|||||||
<Form>
|
<Form>
|
||||||
<label>
|
<label>
|
||||||
To load your playground schema,{' '}
|
To load your playground schema,{' '}
|
||||||
<StyledLink href="https://app.twenty.com/settings/developers/api-keys">
|
<StyledLink href="https://app.twenty.com/settings/developers">
|
||||||
generate an API key
|
generate an API key
|
||||||
</StyledLink>{' '}
|
</StyledLink>{' '}
|
||||||
and paste it here:
|
and paste it here:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Bundle, ZObject } from 'zapier-platform-core';
|
import { Bundle, ZObject } from 'zapier-platform-core';
|
||||||
|
|
||||||
import requestDb from './utils/requestDb';
|
import requestDb from './utils/requestDb';
|
||||||
|
|
||||||
const testAuthentication = async (z: ZObject, bundle: Bundle) => {
|
const testAuthentication = async (z: ZObject, bundle: Bundle) => {
|
||||||
@ -20,7 +21,7 @@ export default {
|
|||||||
label: 'Api Key',
|
label: 'Api Key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpText:
|
helpText:
|
||||||
'Create the api-keys key in [your twenty workspace](https://app.twenty.com/settings/developers/api-keys)',
|
'Create an API key in [your twenty workspace](https://app.twenty.com/settings/developers)',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
connectionLabel: '{{data.currentWorkspace.displayName}}',
|
connectionLabel: '{{data.currentWorkspace.displayName}}',
|
||||||
|
|||||||
Reference in New Issue
Block a user