6653 serverless functions store and use environment variables in serverless function scripts (#7390)

![image](https://github.com/user-attachments/assets/a15bd4c1-3db4-4466-b748-06bdf3874354)

![image](https://github.com/user-attachments/assets/71242dfb-956b-43ed-9704-87cb0dfbc98d)
This commit is contained in:
martmull
2024-10-03 13:56:17 +02:00
committed by GitHub
parent 3cd24d542b
commit 62fe1d0e88
39 changed files with 815 additions and 513 deletions

View File

@ -1,9 +1,8 @@
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { Button } from '@/ui/input/button/components/Button';
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
import { CodeEditor, File } from '@/ui/input/code-editor/components/CodeEditor';
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
import { Section } from '@/ui/layout/section/components/Section';
import { TabList } from '@/ui/layout/tab/components/TabList';
@ -13,13 +12,16 @@ import { useNavigate } from 'react-router-dom';
import { Key } from 'ts-key-enum';
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useRecoilValue } from 'recoil';
const StyledTabList = styled(TabList)`
border-bottom: none;
`;
export const SettingsServerlessFunctionCodeEditorTab = ({
formValues,
files,
handleExecute,
handlePublish,
handleReset,
@ -28,15 +30,19 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
onChange,
setIsCodeValid,
}: {
formValues: ServerlessFunctionFormValues;
files: File[];
handleExecute: () => void;
handlePublish: () => void;
handleReset: () => void;
resetDisabled: boolean;
publishDisabled: boolean;
onChange: (key: string) => (value: string) => void;
onChange: (filePath: string, value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void;
}) => {
const { activeTabIdState } = useTabList(
SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID,
);
const activeTabId = useRecoilValue(activeTabIdState);
const TestButton = (
<Button
title="Test"
@ -68,21 +74,15 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
/>
);
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
const HeaderTabList = (
<StyledTabList
tabListId={TAB_LIST_COMPONENT_ID}
tabs={[{ id: 'index.ts', title: 'index.ts' }]}
tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
tabs={files.map((file) => {
return { id: file.path, title: file.path.split('/').at(-1) || '' };
})}
/>
);
const Header = (
<CoreEditorHeader
leftNodes={[HeaderTabList]}
rightNodes={[ResetButton, PublishButton, TestButton]}
/>
);
const navigate = useNavigate();
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
@ -95,18 +95,25 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
);
return (
<Section>
<H2Title
title="Code your function"
description="Write your function (in typescript) below"
/>
<CodeEditor
value={formValues.code}
onChange={onChange('code')}
setIsCodeValid={setIsCodeValid}
header={Header}
<CoreEditorHeader
leftNodes={[HeaderTabList]}
rightNodes={[ResetButton, PublishButton, TestButton]}
/>
{activeTabId && (
<CodeEditor
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
setIsCodeValid={setIsCodeValid}
/>
)}
</Section>
);
};

View File

@ -44,28 +44,6 @@ export const SettingsServerlessFunctionTestTab = ({
settingsServerlessFunctionOutput.error ||
'';
const InputHeader = (
<CoreEditorHeader
title={'Input'}
rightNodes={[
<Button
title="Run Function"
variant="primary"
accent="blue"
size="small"
Icon={IconPlayerPlay}
onClick={handleExecute}
/>,
]}
/>
);
const OutputHeader = (
<CoreEditorHeader
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
rightNodes={[<LightCopyIconButton copyText={result} />]}
/>
);
const navigate = useNavigate();
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab,
@ -86,20 +64,52 @@ export const SettingsServerlessFunctionTestTab = ({
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={result}
height={settingsServerlessFunctionCodeEditorOutputParams.height}
language={settingsServerlessFunctionCodeEditorOutputParams.language}
options={{ readOnly: true, domReadOnly: true }}
header={OutputHeader}
/>
<div>
<CoreEditorHeader
title={'Input'}
rightNodes={[
<Button
title="Run Function"
variant="primary"
accent="blue"
size="small"
Icon={IconPlayerPlay}
onClick={handleExecute}
/>,
]}
/>
<CodeEditor
files={[
{
content: settingsServerlessFunctionInput,
language: 'json',
path: 'input.json',
},
]}
currentFilePath={'input.json'}
height={200}
onChange={setSettingsServerlessFunctionInput}
/>
</div>
<div>
<CoreEditorHeader
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
rightNodes={[<LightCopyIconButton copyText={result} />]}
/>
<CodeEditor
files={[
{
content: result,
language:
settingsServerlessFunctionCodeEditorOutputParams.language,
path: 'result.any',
},
]}
currentFilePath={'result.any'}
height={settingsServerlessFunctionCodeEditorOutputParams.height}
options={{ readOnly: true, domReadOnly: true }}
/>
</div>
</StyledInputsContainer>
</Section>
);

View File

@ -0,0 +1,2 @@
export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
'settings-serverless-function-editor-tab-list';

View File

@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
id
name
description
sourceCodeHash
runtime
syncStatus
latestVersion

View File

@ -9,7 +9,7 @@ export type ServerlessFunctionNewFormValues = {
};
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
code: string;
code: { [filePath: string]: string } | undefined;
};
type SetServerlessFunctionFormValues = Dispatch<
@ -26,7 +26,7 @@ export const useServerlessFunctionUpdateFormState = (
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
name: '',
description: '',
code: '',
code: undefined,
});
const { serverlessFunction } =
@ -37,7 +37,7 @@ export const useServerlessFunctionUpdateFormState = (
version: 'draft',
onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => {
const newState = {
code: data?.getServerlessFunctionSourceCode || '',
code: data?.getServerlessFunctionSourceCode || undefined,
name: serverlessFunction?.name || '',
description: serverlessFunction?.description || '',
};

View File

@ -1,22 +1,13 @@
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
import dotenv from 'dotenv';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { editor, MarkerSeverity } from 'monaco-editor';
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useEffect } from 'react';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { isDefined } from '~/utils/isDefined';
export const DEFAULT_CODE = `export const handler = async (
event: object,
context: object
): Promise<object> => {
// Your code here
return {};
}
`;
const StyledEditor = styled(Editor)`
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top: none;
@ -24,25 +15,34 @@ const StyledEditor = styled(Editor)`
${({ theme }) => theme.border.radius.sm};
`;
export type File = {
language: string;
content: string;
path: string;
};
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
header: React.ReactNode;
currentFilePath: string;
files: File[];
onChange?: (value: string) => void;
setIsCodeValid?: (isCodeValid: boolean) => void;
};
export const CodeEditor = ({
value = DEFAULT_CODE,
currentFilePath,
files,
onChange,
setIsCodeValid,
language = 'typescript',
height = 450,
options = undefined,
header,
}: CodeEditorProps) => {
const theme = useTheme();
const { availablePackages } = useGetAvailablePackages();
const currentFile = files.find((file) => file.path === currentFilePath);
const environmentVariablesFile = files.find((file) => file.path === '.env');
const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
@ -50,7 +50,57 @@ export const CodeEditor = ({
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
monaco.editor.setTheme('codeEditorTheme');
if (language === 'typescript') {
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,
});
if (isDefined(environmentVariablesFile)) {
const environmentVariables = dotenv.parse(
environmentVariablesFile.content,
);
const environmentDefinition = `
declare namespace NodeJS {
interface ProcessEnv {
${Object.keys(environmentVariables)
.map((key) => `${key}: string;`)
.join('\n')}
}
}
declare const process: {
env: NodeJS.ProcessEnv;
};
`;
monaco.languages.typescript.typescriptDefaults.addExtraLib(
environmentDefinition,
'ts:process-env.d.ts',
);
}
await AutoTypings.create(editor, {
monaco,
preloadPackages: true,
@ -71,43 +121,28 @@ export const CodeEditor = ({
setIsCodeValid?.(true);
};
useEffect(() => {
const style = document.createElement('style');
style.innerHTML = `
.monaco-editor .margin .line-numbers {
font-weight: bold;
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
return (
isDefined(currentFile) &&
isDefined(availablePackages) && (
<>
{header}
<StyledEditor
height={height}
language={language}
value={value}
onMount={handleEditorDidMount}
onChange={(value?: string) => value && onChange?.(value)}
onValidate={handleEditorValidation}
options={{
...options,
overviewRulerLanes: 0,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
minimap: {
enabled: false,
},
}}
/>
</>
<StyledEditor
height={height}
value={currentFile.content}
language={currentFile.language}
onMount={handleEditorDidMount}
onChange={(value?: string) => value && onChange?.(value)}
onValidate={handleEditorValidation}
options={{
...options,
overviewRulerLanes: 0,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
},
minimap: {
enabled: false,
},
}}
/>
)
);
};