Serverless function UI (#6388)
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=36235-120877 Did not do the file manager part. A Function is defined using one unique file at the moment Feature protected by featureFlag `IS_FUNCTION_SETTINGS_ENABLED` ## Demo https://github.com/user-attachments/assets/0acb8291-47b4-4521-a6fa-a88b9198609b
This commit is contained in:
@ -0,0 +1,41 @@
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import styled from '@emotion/styled';
|
||||
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionNewForm = ({
|
||||
formValues,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionNewFormValues;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title="About" description="Name and set your function" />
|
||||
<StyledInputsContainer>
|
||||
<TextInput
|
||||
placeholder="Name"
|
||||
fullWidth
|
||||
focused
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder="Description"
|
||||
minRows={4}
|
||||
value={formValues.description}
|
||||
onChange={onChange('description')}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { IconChevronRight } from 'twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||
grid-template-columns: 312px 132px 68px;
|
||||
`;
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIconTableCell = styled(TableCell)`
|
||||
justify-content: center;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsFieldItemTableRow = ({
|
||||
serverlessFunction,
|
||||
to,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunction;
|
||||
to: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledApisFieldTableRow to={to}>
|
||||
<StyledNameTableCell>{serverlessFunction.name}</StyledNameTableCell>
|
||||
<StyledNameTableCell>{serverlessFunction.runtime}</StyledNameTableCell>
|
||||
<StyledIconTableCell>
|
||||
<StyledIconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconTableCell>
|
||||
</StyledApisFieldTableRow>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import styled from '@emotion/styled';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow';
|
||||
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
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 SettingsServerlessFunctionsTable = () => {
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
return (
|
||||
<>
|
||||
{serverlessFunctions.length ? (
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Runtime</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBody>
|
||||
{serverlessFunctions.map(
|
||||
(serverlessFunction: ServerlessFunction) => (
|
||||
<SettingsServerlessFunctionsFieldItemTableRow
|
||||
key={serverlessFunction.id}
|
||||
serverlessFunction={serverlessFunction}
|
||||
to={getSettingsPagePath(SettingsPath.ServerlessFunctions, {
|
||||
id: serverlessFunction.id,
|
||||
})}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<SettingsServerlessFunctionsTableEmpty />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import {
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledEmptyFunctionsContainer = styled.div`
|
||||
height: 60vh;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsTableEmpty = () => {
|
||||
return (
|
||||
<StyledEmptyFunctionsContainer>
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
>
|
||||
<AnimatedPlaceholder type="emptyFunctions" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
Add your first Function
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
Add your first Function to get started
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New function"
|
||||
to={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
||||
/>
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
</StyledEmptyFunctionsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||
import styled from '@emotion/styled';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
formValues,
|
||||
handleExecute,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
handleExecute: () => void;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
const HeaderButton = (
|
||||
<Button
|
||||
title="Test"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
tabListId={TAB_LIST_COMPONENT_ID}
|
||||
tabs={[{ id: 'index.ts', title: 'index.ts' }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const Header = (
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[HeaderButton]} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Code your function"
|
||||
description="Write your function (in typescript) below"
|
||||
/>
|
||||
<CodeEditor
|
||||
value={formValues.code}
|
||||
onChange={onChange('code')}
|
||||
header={Header}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { useState } from 'react';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const SettingsServerlessFunctionSettingsTab = ({
|
||||
formValues,
|
||||
serverlessFunctionId,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
serverlessFunctionId: string;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] =
|
||||
useState(false);
|
||||
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
|
||||
|
||||
const deleteFunction = async () => {
|
||||
await deleteOneServerlessFunction({ id: serverlessFunctionId });
|
||||
navigate('/settings/functions');
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SettingsServerlessFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Delete this function" />
|
||||
<Button
|
||||
accent="danger"
|
||||
onClick={() => setIsDeleteFunctionModalOpen(true)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
title="Delete function"
|
||||
/>
|
||||
</Section>
|
||||
<ConfirmationModal
|
||||
confirmationValue={formValues.name}
|
||||
confirmationPlaceholder={formValues.name}
|
||||
isOpen={isDeleteFunctionModalOpen}
|
||||
setIsOpen={setIsDeleteFunctionModalOpen}
|
||||
title="Function Deletion"
|
||||
subtitle={
|
||||
<>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
function. <br /> Please type in the function name to confirm.
|
||||
</>
|
||||
}
|
||||
onConfirmClick={deleteFunction}
|
||||
deleteButtonText="Delete function"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
|
||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import styled from '@emotion/styled';
|
||||
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionTestTab = ({
|
||||
handleExecute,
|
||||
}: {
|
||||
handleExecute: () => void;
|
||||
}) => {
|
||||
const settingsServerlessFunctionCodeEditorOutputParams = useRecoilValue(
|
||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
);
|
||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||
settingsServerlessFunctionOutputState,
|
||||
);
|
||||
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
||||
useRecoilState(settingsServerlessFunctionInputState);
|
||||
|
||||
const InputHeaderButton = (
|
||||
<Button
|
||||
title="Run Function"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
|
||||
const InputHeader = (
|
||||
<CoreEditorHeader title={'Input'} rightNodes={[InputHeaderButton]} />
|
||||
);
|
||||
|
||||
const OutputHeaderButton = (
|
||||
<LightCopyIconButton copyText={settingsServerlessFunctionOutput} />
|
||||
);
|
||||
|
||||
const OutputHeader = (
|
||||
<CoreEditorHeader title={'Output'} rightNodes={[OutputHeaderButton]} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Test your function"
|
||||
description='Insert a JSON input, then press "Run" to test your function.'
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<CodeEditor
|
||||
value={settingsServerlessFunctionInput}
|
||||
height={200}
|
||||
onChange={setSettingsServerlessFunctionInput}
|
||||
language={'json'}
|
||||
header={InputHeader}
|
||||
/>
|
||||
<CodeEditor
|
||||
value={settingsServerlessFunctionOutput}
|
||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
header={OutputHeader}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||
|
||||
export const SettingsServerlessFunctionTestTabEffect = () => {
|
||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||
settingsServerlessFunctionOutputState,
|
||||
);
|
||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
);
|
||||
try {
|
||||
JSON.parse(settingsServerlessFunctionOutput);
|
||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||
language: 'json',
|
||||
height: 300,
|
||||
});
|
||||
} catch {
|
||||
return <></>;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
fragment ServerlessFunctionFields on ServerlessFunction {
|
||||
id
|
||||
name
|
||||
description
|
||||
sourceCodeHash
|
||||
sourceCodeFullPath
|
||||
runtime
|
||||
syncStatus
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const CREATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation CreateOneServerlessFunctionItem(
|
||||
$input: CreateServerlessFunctionInput!
|
||||
) {
|
||||
createOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const DELETE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {
|
||||
deleteOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
||||
executeOneServerlessFunction(id: $id, payload: $payload) {
|
||||
result
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const UPDATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {
|
||||
updateOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const FIND_MANY_SERVERLESS_FUNCTIONS = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
query GetManyServerlessFunctions {
|
||||
serverlessFunctions(paging: { first: 100 }) {
|
||||
edges {
|
||||
node {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const FIND_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
query GetOneServerlessFunction($id: UUID!) {
|
||||
serverlessFunction(id: $id) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,34 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
jest.mock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||
() => ({
|
||||
useGetOneServerlessFunction: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('useServerlessFunctionUpdateFormState', () => {
|
||||
test('should return a form', () => {
|
||||
const serverlessFunctionId = 'serverlessFunctionId';
|
||||
const useGetOneServerlessFunctionMock = jest.requireMock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||
);
|
||||
useGetOneServerlessFunctionMock.useGetOneServerlessFunction.mockReturnValue(
|
||||
{
|
||||
serverlessFunction: { sourceCodeFullPath: undefined },
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
const [formValues] = result.current;
|
||||
|
||||
expect(formValues).toEqual({ name: '', description: '', code: '' });
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import {
|
||||
CreateServerlessFunctionInput,
|
||||
CreateOneServerlessFunctionItemMutation,
|
||||
CreateOneServerlessFunctionItemMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction';
|
||||
|
||||
export const useCreateOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
CreateOneServerlessFunctionItemMutation,
|
||||
CreateOneServerlessFunctionItemMutationVariables
|
||||
>(CREATE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const createOneServerlessFunction = async (
|
||||
input: CreateServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { createOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
||||
import {
|
||||
DeleteServerlessFunctionInput,
|
||||
DeleteOneServerlessFunctionMutation,
|
||||
DeleteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDeleteOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
DeleteOneServerlessFunctionMutation,
|
||||
DeleteOneServerlessFunctionMutationVariables
|
||||
>(DELETE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const deleteOneServerlessFunction = async (
|
||||
input: DeleteServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { deleteOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
||||
import {
|
||||
ExecuteOneServerlessFunctionMutation,
|
||||
ExecuteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useExecuteOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
ExecuteOneServerlessFunctionMutation,
|
||||
ExecuteOneServerlessFunctionMutationVariables
|
||||
>(EXECUTE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const executeOneServerlessFunction = async (
|
||||
id: string,
|
||||
payload: object = {},
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
id,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
};
|
||||
return { executeOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import {
|
||||
GetManyServerlessFunctionsQuery,
|
||||
GetManyServerlessFunctionsQueryVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetManyServerlessFunctions = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const { data } = useQuery<
|
||||
GetManyServerlessFunctionsQuery,
|
||||
GetManyServerlessFunctionsQueryVariables
|
||||
>(FIND_MANY_SERVERLESS_FUNCTIONS, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
});
|
||||
return {
|
||||
serverlessFunctions:
|
||||
data?.serverlessFunctions?.edges.map(({ node }) => node) || [],
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction';
|
||||
import {
|
||||
GetOneServerlessFunctionQuery,
|
||||
GetOneServerlessFunctionQueryVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetOneServerlessFunction = (id: string) => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const { data } = useQuery<
|
||||
GetOneServerlessFunctionQuery,
|
||||
GetOneServerlessFunctionQueryVariables
|
||||
>(FIND_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
serverlessFunction: data?.serverlessFunction || null,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||
|
||||
export type ServerlessFunctionNewFormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
|
||||
code: string;
|
||||
};
|
||||
|
||||
type SetServerlessFunctionFormValues = Dispatch<
|
||||
SetStateAction<ServerlessFunctionFormValues>
|
||||
>;
|
||||
|
||||
export const useServerlessFunctionUpdateFormState = (
|
||||
serverlessFunctionId: string,
|
||||
): [ServerlessFunctionFormValues, SetServerlessFunctionFormValues] => {
|
||||
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
|
||||
name: '',
|
||||
description: '',
|
||||
code: '',
|
||||
});
|
||||
|
||||
const { serverlessFunction } =
|
||||
useGetOneServerlessFunction(serverlessFunctionId);
|
||||
|
||||
useEffect(() => {
|
||||
const getFileContent = async () => {
|
||||
const resp = await fetch(
|
||||
getFileAbsoluteURI(serverlessFunction?.sourceCodeFullPath),
|
||||
);
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
} else {
|
||||
const result = await resp.text();
|
||||
const newState = {
|
||||
code: result,
|
||||
name: serverlessFunction?.name || '',
|
||||
description: serverlessFunction?.description || '',
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
...newState,
|
||||
}));
|
||||
}
|
||||
};
|
||||
if (isDefined(serverlessFunction?.sourceCodeFullPath)) {
|
||||
getFileContent();
|
||||
}
|
||||
}, [serverlessFunction, setFormValues]);
|
||||
|
||||
return [formValues, setFormValues];
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction';
|
||||
import {
|
||||
UpdateServerlessFunctionInput,
|
||||
UpdateOneServerlessFunctionMutation,
|
||||
UpdateOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
|
||||
export const useUpdateOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
UpdateOneServerlessFunctionMutation,
|
||||
UpdateOneServerlessFunctionMutationVariables
|
||||
>(UPDATE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const updateOneServerlessFunction = async (
|
||||
input: UpdateServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { updateOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionCodeEditorOutputParamsState =
|
||||
createState<{ language: string; height: number }>({
|
||||
key: 'settingsServerlessFunctionCodeEditorOutputParamsState',
|
||||
defaultValue: { language: 'plaintext', height: 64 },
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionInputState = createState<string>({
|
||||
key: 'settingsServerlessFunctionInputState',
|
||||
defaultValue: '{}',
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionOutputState = createState<string>({
|
||||
key: 'settingsServerlessFunctionOutputState',
|
||||
defaultValue: 'Enter an input above then press "run Function"',
|
||||
});
|
||||
Reference in New Issue
Block a user