Rename serverlessFunction to logicFunction (#17494)
## Summary Rename "Serverless Function" to "Logic Function" across the codebase for clearer naming. ### Environment Variable Changes | Old | New | |-----|-----| | `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` | | `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` | | `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` | | `SERVERLESS_LAMBDA_SUBHOSTING_URL` | `LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` | | `SERVERLESS_LAMBDA_ACCESS_KEY_ID` | `LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` | | `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` | `LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` | ### Breaking Changes - Environment variables must be updated in production deployments - Database migration renames `serverlessFunction` → `logicFunction` tables
This commit is contained in:
+117
@@ -0,0 +1,117 @@
|
||||
import { useGetAvailablePackages } from '@/settings/logic-functions/hooks/useGetAvailablePackages';
|
||||
import { type EditorProps, type Monaco } from '@monaco-editor/react';
|
||||
import { type editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
|
||||
export type File = {
|
||||
language: string;
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
type SettingsLogicFunctionCodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||
currentFilePath: string;
|
||||
files: File[];
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionCodeEditor = ({
|
||||
currentFilePath,
|
||||
files,
|
||||
onChange,
|
||||
height = 450,
|
||||
options = undefined,
|
||||
}: SettingsLogicFunctionCodeEditorProps) => {
|
||||
const { logicFunctionId = '' } = useParams();
|
||||
const { availablePackages } = useGetAvailablePackages({
|
||||
id: logicFunctionId,
|
||||
});
|
||||
|
||||
const currentFile = files.find((file) => file.path === currentFilePath);
|
||||
|
||||
const handleEditorDidMount = async (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => {
|
||||
if (files.length > 1) {
|
||||
files.forEach((file) => {
|
||||
const model = monaco.editor.getModel(monaco.Uri.file(file.path));
|
||||
if (!isDefined(model)) {
|
||||
monaco.editor.createModel(
|
||||
file.content,
|
||||
file.language,
|
||||
monaco.Uri.file(file.path),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
baseUrl: 'file:///src',
|
||||
paths: {
|
||||
'src/*': ['file:///src/*'],
|
||||
},
|
||||
allowSyntheticDefaultImports: true,
|
||||
esModuleInterop: true,
|
||||
noEmit: true,
|
||||
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||
});
|
||||
|
||||
// TODO load that with proper env variables
|
||||
const environmentVariables = {};
|
||||
|
||||
if (isDefined(environmentVariables)) {
|
||||
const envTypeDefinitions = Object.keys(environmentVariables)
|
||||
// eslint-disable-next-line lingui/no-unlocalized-strings
|
||||
.map((key) => `${key}: string;`)
|
||||
.join('\n');
|
||||
const environmentDefinition = `
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${envTypeDefinitions}
|
||||
}
|
||||
}
|
||||
|
||||
declare const process: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
`;
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
|
||||
{
|
||||
content: environmentDefinition,
|
||||
filePath: 'ts:process-env.d.ts',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
await AutoTypings.create(editor, {
|
||||
monaco,
|
||||
preloadPackages: true,
|
||||
onlySpecifiedPackages: true,
|
||||
versions: availablePackages,
|
||||
debounceDuration: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
isDefined(currentFile) &&
|
||||
isDefined(availablePackages) && (
|
||||
<CodeEditor
|
||||
height={height}
|
||||
value={currentFile.content}
|
||||
language={currentFile.language}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
variant="with-header"
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
width: fit-content;
|
||||
max-width: 420px;
|
||||
& > input:disabled {
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
type SettingsLogicFunctionLabelContainerProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionLabelContainer = ({
|
||||
value,
|
||||
onChange,
|
||||
}: SettingsLogicFunctionLabelContainerProps) => {
|
||||
return (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="logic-function-name-input"
|
||||
sizeVariant="md"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t`Function name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
);
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { type LogicFunctionNewFormValues } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionNewForm = ({
|
||||
formValues,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
formValues: LogicFunctionNewFormValues;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
const descriptionTextAreaId = `${formValues.name}-description`;
|
||||
const nameTextInputId = `${formValues.name}-name`;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`About`}
|
||||
description={t`Name and describe your function`}
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<SettingsTextInput
|
||||
instanceId={nameTextInputId}
|
||||
placeholder={t`Name`}
|
||||
fullWidth
|
||||
autoFocusOnMount
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<TextArea
|
||||
textAreaId={descriptionTextAreaId}
|
||||
placeholder={t`Description`}
|
||||
minRows={4}
|
||||
value={formValues.description}
|
||||
onChange={onChange('description')}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { LinkChip } from 'twenty-ui/components';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
|
||||
const { applicationId = '' } = useParams<{ applicationId: string }>();
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Environment Variables`}
|
||||
description={t`Accessible in your function via process.env.KEY`}
|
||||
/>
|
||||
<Trans>
|
||||
Environment variables are defined at application level for all
|
||||
functions. Please check{' '}
|
||||
<LinkChip
|
||||
label={t`application detail page`}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'settings',
|
||||
)}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconChevronRight } from 'twenty-ui/display';
|
||||
import { StyledTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRuntimeTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
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 SettingsLogicFunctionsFieldItemTableRow = ({
|
||||
logicFunction,
|
||||
to,
|
||||
}: {
|
||||
logicFunction: LogicFunction;
|
||||
to: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledTableRow to={to}>
|
||||
<StyledNameTableCell>{logicFunction.name}</StyledNameTableCell>
|
||||
<StyledNameTableCell></StyledNameTableCell>
|
||||
<StyledRuntimeTableCell>{logicFunction.runtime}</StyledRuntimeTableCell>
|
||||
<StyledIconTableCell>
|
||||
<StyledIconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconTableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { SettingsLogicFunctionsFieldItemTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsFieldItemTableRow';
|
||||
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';
|
||||
import styled from '@emotion/styled';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 164px 1fr 96px 32px;
|
||||
`;
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionsTable = ({
|
||||
logicFunctions,
|
||||
}: {
|
||||
logicFunctions: LogicFunction[];
|
||||
}) => {
|
||||
const { applicationId = '' } = useParams();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
if (logicFunctions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
<TableHeader>{t`Runtime`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBody>
|
||||
{logicFunctions.map((logicFunction: LogicFunction) => (
|
||||
<SettingsLogicFunctionsFieldItemTableRow
|
||||
key={logicFunction.id}
|
||||
logicFunction={logicFunction}
|
||||
to={getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
|
||||
applicationId,
|
||||
logicFunctionId: logicFunction.id,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
type File,
|
||||
SettingsLogicFunctionCodeEditor,
|
||||
} from '@/settings/logic-functions/components/SettingsLogicFunctionCodeEditor';
|
||||
import { SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/logic-functions/constants/SettingsLogicFunctionTabListComponentId';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { Button, CoreEditorHeader } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionCodeEditorTab = ({
|
||||
files,
|
||||
handleExecute,
|
||||
onChange,
|
||||
isTesting = false,
|
||||
isManaged = false,
|
||||
}: {
|
||||
files: File[];
|
||||
handleExecute: () => void;
|
||||
onChange: (filePath: string, value: string) => void;
|
||||
isTesting?: boolean;
|
||||
isManaged?: boolean;
|
||||
}) => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const TestButton = (
|
||||
<Button
|
||||
title={t`Test`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
disabled={isTesting}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
tabs={files.map((file) => {
|
||||
return { id: file.path, title: file.path.split('/').at(-1) || '' };
|
||||
})}
|
||||
componentInstanceId={SETTINGS_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Code your function`}
|
||||
description={t`Write your function (in typescript) below`}
|
||||
/>
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[TestButton]} />
|
||||
{activeTabId && (
|
||||
<SettingsLogicFunctionCodeEditor
|
||||
files={files}
|
||||
currentFilePath={activeTabId}
|
||||
onChange={(newCodeValue: string) =>
|
||||
onChange(activeTabId, newCodeValue)
|
||||
}
|
||||
options={
|
||||
isManaged
|
||||
? {
|
||||
readOnly: true,
|
||||
readOnlyMessage: {
|
||||
value: t`Managed logic functions are not editable`,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
|
||||
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
|
||||
import { type LogicFunctionFormValues } from '@/settings/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
|
||||
export const SettingsLogicFunctionSettingsTab = ({
|
||||
formValues,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: LogicFunctionFormValues;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsLogicFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
IconCheck,
|
||||
IconDotsVertical,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
IconX,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import type { ApplicationVariable } from '~/generated/graphql';
|
||||
|
||||
const StyledEditModeTableRow = styled(TableRow)`
|
||||
grid-template-columns: 180px auto 56px;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 180px 300px 32px;
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTabEnvironmentVariableTableRow = ({
|
||||
envVariable,
|
||||
onChange,
|
||||
onDelete,
|
||||
initialEditMode = false,
|
||||
}: {
|
||||
envVariable: ApplicationVariable;
|
||||
onChange: (newEnvVariable: ApplicationVariable) => void;
|
||||
onDelete: () => void;
|
||||
initialEditMode?: boolean;
|
||||
}) => {
|
||||
const [editedEnvVariable, setEditedEnvVariable] = useState(envVariable);
|
||||
const [editMode, setEditMode] = useState(initialEditMode);
|
||||
const dropDownId = `settings-environment-variable-dropdown-${envVariable.id}`;
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
return editMode ? (
|
||||
<StyledEditModeTableRow>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={editedEnvVariable.key}
|
||||
onChange={(newKey) =>
|
||||
setEditedEnvVariable({ ...editedEnvVariable, key: newKey })
|
||||
}
|
||||
placeholder={t`Name`}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
value={editedEnvVariable.value}
|
||||
onChange={(newValue) =>
|
||||
setEditedEnvVariable({ ...editedEnvVariable, value: newValue })
|
||||
}
|
||||
placeholder={t`Value`}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconX}
|
||||
onClick={() => {
|
||||
if (envVariable.key === '' && envVariable.value === '') {
|
||||
onDelete();
|
||||
}
|
||||
setEditedEnvVariable(envVariable);
|
||||
setEditMode(false);
|
||||
}}
|
||||
/>
|
||||
<LightIconButton
|
||||
accent="tertiary"
|
||||
Icon={IconCheck}
|
||||
disabled={
|
||||
editedEnvVariable.key === '' || editedEnvVariable.value === ''
|
||||
}
|
||||
onClick={() => {
|
||||
onChange(editedEnvVariable);
|
||||
setEditMode(false);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledEditModeTableRow>
|
||||
) : (
|
||||
<StyledTableRow onClick={() => setEditMode(true)}>
|
||||
<TableCell>
|
||||
<OverflowingTextWithTooltip text={envVariable.key} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<OverflowingTextWithTooltip text={envVariable.value} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Dropdown
|
||||
dropdownId={dropDownId}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
aria-label={t`Env Variable Options`}
|
||||
Icon={IconDotsVertical}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text={t`Edit`}
|
||||
LeftIcon={IconPencil}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
closeDropdown(dropDownId);
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={t`Delete`}
|
||||
LeftIcon={IconTrash}
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeDropdown(dropDownId);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { LogicFunctionExecutionResult } from '@/logic-functions/components/LogicFunctionExecutionResult';
|
||||
import {
|
||||
type LogicFunctionTestData,
|
||||
logicFunctionTestDataFamilyState,
|
||||
} from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { Button, CodeEditor, CoreEditorHeader } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledCodeEditorContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTestTab = ({
|
||||
handleExecute,
|
||||
logicFunctionId,
|
||||
isTesting = false,
|
||||
}: {
|
||||
handleExecute: () => void;
|
||||
logicFunctionId: string;
|
||||
isTesting?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const [logicFunctionTestData, setLogicFunctionTestData] =
|
||||
useRecoilState<LogicFunctionTestData>(
|
||||
logicFunctionTestDataFamilyState(logicFunctionId),
|
||||
);
|
||||
|
||||
const onChange = (newInput: string) => {
|
||||
setLogicFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
input: JSON.parse(newInput),
|
||||
}));
|
||||
};
|
||||
|
||||
const testLogsTextAreaId = `${logicFunctionId}-test-logs`;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Test your function`}
|
||||
description={t`Insert a JSON input, then press "Run" to test your function.`}
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<StyledCodeEditorContainer>
|
||||
<CoreEditorHeader
|
||||
title={t`Input`}
|
||||
rightNodes={[
|
||||
<Button
|
||||
title={t`Run Function`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
disabled={isTesting}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<CodeEditor
|
||||
value={JSON.stringify(logicFunctionTestData.input, null, 4)}
|
||||
language="json"
|
||||
height={100}
|
||||
onChange={onChange}
|
||||
variant="with-header"
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
<LogicFunctionExecutionResult
|
||||
logicFunctionTestData={logicFunctionTestData}
|
||||
maxHeight={
|
||||
logicFunctionTestData.output.logs.length > 0 ? 200 : undefined
|
||||
}
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
{logicFunctionTestData.output.logs.length > 0 && (
|
||||
<StyledCodeEditorContainer>
|
||||
<InputLabel>{t`Logs`}</InputLabel>
|
||||
<TextArea
|
||||
textAreaId={testLogsTextAreaId}
|
||||
value={isTesting ? '' : logicFunctionTestData.output.logs}
|
||||
maxRows={20}
|
||||
disabled
|
||||
/>
|
||||
</StyledCodeEditorContainer>
|
||||
)}
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type LogicFunction } from '~/generated/graphql';
|
||||
|
||||
export const StyledRouteTriggerTableRow = styled(TableRow)`
|
||||
grid-template-columns: 1fr 120px 120px;
|
||||
`;
|
||||
|
||||
// TODO: @Charles put back with new sources
|
||||
// const StyledTableCell = styled(TableCell)`
|
||||
// color: ${({ theme }) => theme.font.color.tertiary};
|
||||
// gap: ${({ theme }) => theme.spacing(2)};
|
||||
// min-width: 0;
|
||||
// overflow: hidden;
|
||||
// `;
|
||||
|
||||
const StyledRouteTriggerTableHeaderRow = styled(StyledRouteTriggerTableRow)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
height: 160px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionTriggersTab = ({
|
||||
logicFunction: _logicFunction,
|
||||
}: {
|
||||
logicFunction: LogicFunction;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const cronTriggers: [] = [];
|
||||
|
||||
const routeTriggers: [] = [];
|
||||
|
||||
const databaseEvents: [] = [];
|
||||
|
||||
const hasNoTriggers =
|
||||
databaseEvents.length === 0 &&
|
||||
cronTriggers.length === 0 &&
|
||||
routeTriggers.length === 0;
|
||||
|
||||
if (hasNoTriggers) {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Triggers`}
|
||||
description={t`Configure when this function should be executed`}
|
||||
/>
|
||||
<StyledEmptyState>
|
||||
{t`No triggers configured for this function.`}
|
||||
</StyledEmptyState>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{databaseEvents.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Database event`}
|
||||
description={t`Select the events that should trigger the function`}
|
||||
/>
|
||||
<SettingsDatabaseEventsForm events={databaseEvents} disabled />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{cronTriggers.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Cron`}
|
||||
description={t`Triggers the function at regular intervals`}
|
||||
/>
|
||||
{cronTriggers.map((cronTrigger, index) => (
|
||||
<FormTextFieldInput
|
||||
key={index}
|
||||
label={t`Expression`}
|
||||
placeholder="0 */1 * * *"
|
||||
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
|
||||
onChange={() => {}}
|
||||
readonly
|
||||
defaultValue={cronTrigger}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{routeTriggers.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Http`}
|
||||
description={t`Triggers the function with Http request`}
|
||||
/>
|
||||
<Table>
|
||||
<StyledRouteTriggerTableHeaderRow>
|
||||
<TableHeader>{t`Path`}</TableHeader>
|
||||
<TableHeader>{t`Method`}</TableHeader>
|
||||
<TableHeader>{t`Auth Required`}</TableHeader>
|
||||
</StyledRouteTriggerTableHeaderRow>
|
||||
{routeTriggers.map((_, _index) => (
|
||||
<></>
|
||||
// <StyledRouteTriggerTableRow key={index}>
|
||||
// <StyledTableCell>
|
||||
// <OverflowingTextWithTooltip
|
||||
// text={`${REACT_APP_SERVER_BASE_URL}/s${routeTrigger.path}`}
|
||||
// />
|
||||
// </StyledTableCell>
|
||||
// <StyledTableCell>{routeTrigger.httpMethod}</StyledTableCell>
|
||||
// <StyledTableCell>
|
||||
// <Tag
|
||||
// text={routeTrigger.isAuthRequired ? t`True` : t`False`}
|
||||
// color={routeTrigger.isAuthRequired ? 'green' : 'orange'}
|
||||
// weight="medium"
|
||||
// />
|
||||
// </StyledTableCell>
|
||||
// </StyledRouteTriggerTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user