6653 serverless functions store and use environment variables in serverless function scripts (#7390)
 
This commit is contained in:
@ -25,15 +25,15 @@ const documents = {
|
||||
"\n \n query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {\n findDistantTablesWithStatus(input: $input) {\n ...RemoteTableFields\n }\n }\n": types.GetManyRemoteTablesDocument,
|
||||
"\n \n query GetOneDatabaseConnection($input: RemoteServerIdInput!) {\n findOneRemoteServerById(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.GetOneDatabaseConnectionDocument,
|
||||
"\n mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {\n createOneObject(input: $input) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.CreateOneObjectMetadataItemDocument,
|
||||
"\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument,
|
||||
"\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument,
|
||||
"\n mutation CreateOneRelationMetadata($input: CreateOneRelationInput!) {\n createOneRelation(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n": types.CreateOneRelationMetadataDocument,
|
||||
"\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.UpdateOneFieldMetadataItemDocument,
|
||||
"\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.UpdateOneFieldMetadataItemDocument,
|
||||
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
|
||||
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
|
||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
||||
"\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||
"\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||
"\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||
@ -110,7 +110,7 @@ export function graphql(source: "\n mutation CreateOneObjectMetadataItem($input
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -118,7 +118,7 @@ export function graphql(source: "\n mutation CreateOneRelationMetadata($input:
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -130,7 +130,7 @@ export function graphql(source: "\n mutation DeleteOneObjectMetadataItem($idToD
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"): (typeof documents)["\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -138,11 +138,11 @@ export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idT
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"];
|
||||
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID =
|
||||
'settings-serverless-function-editor-tab-list';
|
||||
@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
id
|
||||
name
|
||||
description
|
||||
sourceCodeHash
|
||||
runtime
|
||||
syncStatus
|
||||
latestVersion
|
||||
|
||||
@ -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 || '',
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@ -24,6 +24,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||
|
||||
@ -81,14 +82,24 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const onCodeChange = async (filePath: string, value: string) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
code: { ...prevState.code, [filePath]: value },
|
||||
}));
|
||||
await handleSave();
|
||||
};
|
||||
|
||||
const resetDisabled =
|
||||
!isDefined(latestVersionCode) || latestVersionCode === formValues.code;
|
||||
const publishDisabled = !isCodeValid || latestVersionCode === formValues.code;
|
||||
!isDefined(latestVersionCode) ||
|
||||
isDeeplyEqual(latestVersionCode, formValues.code);
|
||||
const publishDisabled =
|
||||
!isCodeValid || isDeeplyEqual(latestVersionCode, formValues.code);
|
||||
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
const newState = {
|
||||
code: latestVersionCode || '',
|
||||
code: latestVersionCode || {},
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
@ -166,18 +177,30 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
{ id: 'settings', title: 'Settings', Icon: IconSettings },
|
||||
];
|
||||
|
||||
const files = formValues.code
|
||||
? Object.keys(formValues.code)
|
||||
.map((key) => {
|
||||
return {
|
||||
path: key,
|
||||
language: key === '.env' ? 'ini' : 'typescript',
|
||||
content: formValues.code?.[key] || '',
|
||||
};
|
||||
})
|
||||
.reverse()
|
||||
: [];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'editor':
|
||||
return (
|
||||
<SettingsServerlessFunctionCodeEditorTab
|
||||
formValues={formValues}
|
||||
files={files}
|
||||
handleExecute={handleExecute}
|
||||
handlePublish={handlePublish}
|
||||
handleReset={handleReset}
|
||||
resetDisabled={resetDisabled}
|
||||
publishDisabled={publishDisabled}
|
||||
onChange={onChange}
|
||||
onChange={onCodeChange}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -9,7 +9,6 @@ import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions
|
||||
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
@ -31,7 +30,6 @@ export const SettingsServerlessFunctionsNew = () => {
|
||||
const newServerlessFunction = await createOneServerlessFunction({
|
||||
name: formValues.name,
|
||||
description: formValues.description,
|
||||
code: DEFAULT_CODE,
|
||||
});
|
||||
|
||||
if (!isDefined(newServerlessFunction?.data)) {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { graphql, http, HttpResponse } from 'msw';
|
||||
@ -38,7 +37,6 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
description: '',
|
||||
syncStatus: 'READY',
|
||||
runtime: 'nodejs18.x',
|
||||
sourceCodeHash: '42d2734b3dc8a7b45a16803ed7f417bc',
|
||||
updatedAt: '2024-02-24T10:23:10.673Z',
|
||||
createdAt: '2024-02-24T10:23:10.673Z',
|
||||
},
|
||||
@ -46,7 +44,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
});
|
||||
}),
|
||||
http.get(getImageAbsoluteURI(SOURCE_CODE_FULL_PATH) || '', () => {
|
||||
return HttpResponse.text(DEFAULT_CODE);
|
||||
return HttpResponse.text('export const handler = () => {}');
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user