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 <></>;
|
||||
};
|
||||
Reference in New Issue
Block a user