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:
martmull
2024-07-29 13:03:09 +02:00
committed by GitHub
parent 936279f895
commit 00fea17920
100 changed files with 2283 additions and 121 deletions

View File

@ -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>
);
};

View File

@ -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"
/>
</>
);
};

View File

@ -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>
);
};

View File

@ -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 <></>;
};