6654 serverless functions add a deploy button disable deploy when autosave (#6715)
- improvements on serverless function behavior (autosave performances, deploy on execution only) - add versioning to serverless functions - add a publish endpoint to create a new version of a serverless function - add deploy and reset to lastVersion button in the settings section: <img width="736" alt="image" src="https://github.com/user-attachments/assets/2001f8d2-07a4-4f79-84dd-ec74b6f301d3">
This commit is contained in:
@ -33,13 +33,15 @@ const 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": 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 fromRelationMetadata {\n id\n relationType\n toObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n toFieldMetadataId\n }\n toRelationMetadata {\n id\n relationType\n fromObjectMetadata {\n id\n dataSourceId\n nameSingular\n namePlural\n isSystem\n isRemote\n }\n fromFieldMetadataId\n }\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 sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||
"\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 \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($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||
"\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument,
|
||||
"\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument,
|
||||
"\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument,
|
||||
"\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument,
|
||||
"\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n": types.FindOneServerlessFunctionSourceCodeDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -139,7 +141,7 @@ export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilt
|
||||
/**
|
||||
* 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 sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\n sourceCodeFullPath\n runtime\n syncStatus\n createdAt\n updatedAt\n }\n"];
|
||||
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"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -151,7 +153,11 @@ export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($i
|
||||
/**
|
||||
* 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 ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {\n executeOneServerlessFunction(id: $id, payload: $payload) {\n data\n duration\n status\n error\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n"): (typeof documents)["\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\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 \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -164,6 +170,10 @@ export function graphql(source: "\n \n query GetManyServerlessFunctions {\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 \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\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 query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n"): (typeof documents)["\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -58,7 +58,9 @@ export const useRestoreManyRecords = ({
|
||||
|
||||
// TODO: fix optimistic effect
|
||||
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||
const findManyQueryName = `FindMany${capitalize(objectMetadataItem.namePlural)}`;
|
||||
const findManyQueryName = `FindMany${capitalize(
|
||||
objectMetadataItem.namePlural,
|
||||
)}`;
|
||||
|
||||
const restoredRecordsResponse = await apolloClient.mutate({
|
||||
mutation: restoreManyRecordsMutation,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||
import { H2Title, IconPlayerPlay, IconGitCommit, IconRestore } 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';
|
||||
@ -14,13 +14,23 @@ const StyledTabList = styled(TabList)`
|
||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
formValues,
|
||||
handleExecute,
|
||||
handlePublish,
|
||||
handleReset,
|
||||
resetDisabled,
|
||||
publishDisabled,
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
handleExecute: () => void;
|
||||
handlePublish: () => void;
|
||||
handleReset: () => void;
|
||||
resetDisabled: boolean;
|
||||
publishDisabled: boolean;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||
}) => {
|
||||
const HeaderButton = (
|
||||
const TestButton = (
|
||||
<Button
|
||||
title="Test"
|
||||
variant="primary"
|
||||
@ -30,6 +40,26 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
const PublishButton = (
|
||||
<Button
|
||||
title="Publish"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconGitCommit}
|
||||
onClick={handlePublish}
|
||||
disabled={publishDisabled}
|
||||
/>
|
||||
);
|
||||
const ResetButton = (
|
||||
<Button
|
||||
title="Reset"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconRestore}
|
||||
onClick={handleReset}
|
||||
disabled={resetDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||
|
||||
@ -41,7 +71,10 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
);
|
||||
|
||||
const Header = (
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[HeaderButton]} />
|
||||
<CoreEditorHeader
|
||||
leftNodes={[HeaderTabList]}
|
||||
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
@ -53,6 +86,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
<CodeEditor
|
||||
value={formValues.code}
|
||||
onChange={onChange('code')}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
header={Header}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
DEFAULT_OUTPUT_VALUE,
|
||||
@ -13,11 +13,16 @@ export const SettingsServerlessFunctionTestTabEffect = () => {
|
||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
);
|
||||
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||
language: 'json',
|
||||
height: 300,
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (settingsServerlessFunctionOutput.data !== DEFAULT_OUTPUT_VALUE) {
|
||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||
language: 'json',
|
||||
height: 300,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
settingsServerlessFunctionOutput.data,
|
||||
setSettingsServerlessFunctionCodeEditorOutputParams,
|
||||
]);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -6,9 +6,9 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
name
|
||||
description
|
||||
sourceCodeHash
|
||||
sourceCodeFullPath
|
||||
runtime
|
||||
syncStatus
|
||||
latestVersion
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
||||
executeOneServerlessFunction(id: $id, payload: $payload) {
|
||||
mutation ExecuteOneServerlessFunction(
|
||||
$input: ExecuteServerlessFunctionInput!
|
||||
) {
|
||||
executeOneServerlessFunction(input: $input) {
|
||||
data
|
||||
duration
|
||||
status
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const PUBLISH_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation PublishOneServerlessFunction(
|
||||
$input: PublishServerlessFunctionInput!
|
||||
) {
|
||||
publishServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE = gql`
|
||||
query FindOneServerlessFunctionSourceCode(
|
||||
$input: GetServerlessFunctionSourceCodeInput!
|
||||
) {
|
||||
getServerlessFunctionSourceCode(input: $input)
|
||||
}
|
||||
`;
|
||||
@ -9,6 +9,13 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode',
|
||||
() => ({
|
||||
useGetOneServerlessFunctionSourceCode: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('useServerlessFunctionUpdateFormState', () => {
|
||||
test('should return a form', () => {
|
||||
const serverlessFunctionId = 'serverlessFunctionId';
|
||||
@ -20,6 +27,14 @@ describe('useServerlessFunctionUpdateFormState', () => {
|
||||
serverlessFunction: { sourceCodeFullPath: undefined },
|
||||
},
|
||||
);
|
||||
const useGetOneServerlessFunctionSourceCodeMock = jest.requireMock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode',
|
||||
);
|
||||
useGetOneServerlessFunctionSourceCodeMock.useGetOneServerlessFunctionSourceCode.mockReturnValue(
|
||||
{
|
||||
code: 'export const handler = () => {}',
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
||||
{
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
||||
import {
|
||||
DeleteServerlessFunctionInput,
|
||||
DeleteOneServerlessFunctionMutation,
|
||||
DeleteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||
|
||||
export const useDeleteOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
@ -26,7 +26,9 @@ export const useDeleteOneServerlessFunction = () => {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
refetchQueries: [
|
||||
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
||||
import {
|
||||
ExecuteServerlessFunctionInput,
|
||||
ExecuteOneServerlessFunctionMutation,
|
||||
ExecuteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@ -16,13 +17,11 @@ export const useExecuteOneServerlessFunction = () => {
|
||||
});
|
||||
|
||||
const executeOneServerlessFunction = async (
|
||||
id: string,
|
||||
payload: object = {},
|
||||
input: ExecuteServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
id,
|
||||
payload,
|
||||
input,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||
import {
|
||||
FindOneServerlessFunctionSourceCodeQuery,
|
||||
FindOneServerlessFunctionSourceCodeQueryVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetOneServerlessFunctionSourceCode = ({
|
||||
id,
|
||||
version,
|
||||
onCompleted,
|
||||
}: {
|
||||
id: string;
|
||||
version: string;
|
||||
onCompleted?: (data: FindOneServerlessFunctionSourceCodeQuery) => void;
|
||||
}) => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const { data, loading } = useQuery<
|
||||
FindOneServerlessFunctionSourceCodeQuery,
|
||||
FindOneServerlessFunctionSourceCodeQueryVariables
|
||||
>(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
variables: {
|
||||
input: { id, version },
|
||||
},
|
||||
onCompleted,
|
||||
});
|
||||
return { code: data?.getServerlessFunctionSourceCode, loading };
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { PUBLISH_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/publishOneServerlessFunction';
|
||||
import {
|
||||
PublishServerlessFunctionInput,
|
||||
PublishOneServerlessFunctionMutation,
|
||||
PublishOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||
|
||||
export const usePublishOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
PublishOneServerlessFunctionMutation,
|
||||
PublishOneServerlessFunctionMutationVariables
|
||||
>(PUBLISH_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const publishOneServerlessFunction = async (
|
||||
input: PublishServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [
|
||||
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
return { publishOneServerlessFunction };
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||
import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ServerlessFunctionNewFormValues = {
|
||||
name: string;
|
||||
@ -28,30 +28,21 @@ export const useServerlessFunctionUpdateFormState = (
|
||||
const { serverlessFunction } =
|
||||
useGetOneServerlessFunction(serverlessFunctionId);
|
||||
|
||||
useEffect(() => {
|
||||
const getFileContent = async () => {
|
||||
const resp = await fetch(
|
||||
getFileAbsoluteURI(serverlessFunction?.sourceCodeFullPath),
|
||||
);
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
} else {
|
||||
const result = await resp.text();
|
||||
const newState = {
|
||||
code: result,
|
||||
name: serverlessFunction?.name || '',
|
||||
description: serverlessFunction?.description || '',
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
...newState,
|
||||
}));
|
||||
}
|
||||
};
|
||||
if (isDefined(serverlessFunction?.sourceCodeFullPath)) {
|
||||
getFileContent();
|
||||
}
|
||||
}, [serverlessFunction, setFormValues]);
|
||||
useGetOneServerlessFunctionSourceCode({
|
||||
id: serverlessFunctionId,
|
||||
version: 'draft',
|
||||
onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => {
|
||||
const newState = {
|
||||
code: data?.getServerlessFunctionSourceCode || '',
|
||||
name: serverlessFunction?.name || '',
|
||||
description: serverlessFunction?.description || '',
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
...newState,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return [formValues, setFormValues];
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
||||
import { editor } from 'monaco-editor';
|
||||
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';
|
||||
@ -24,17 +24,20 @@ const StyledEditor = styled(Editor)`
|
||||
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||
header: React.ReactNode;
|
||||
onChange?: (value: string) => void;
|
||||
setIsCodeValid?: (isCodeValid: boolean) => void;
|
||||
};
|
||||
|
||||
export const CodeEditor = ({
|
||||
value = DEFAULT_CODE,
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
language = 'typescript',
|
||||
height = 500,
|
||||
height = 450,
|
||||
options = undefined,
|
||||
header,
|
||||
}: CodeEditorProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const handleEditorDidMount = (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
@ -42,6 +45,17 @@ export const CodeEditor = ({
|
||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||
monaco.editor.setTheme('codeEditorTheme');
|
||||
};
|
||||
|
||||
const handleEditorValidation = (markers: editor.IMarker[]) => {
|
||||
for (const marker of markers) {
|
||||
if (marker.severity === MarkerSeverity.Error) {
|
||||
setIsCodeValid?.(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setIsCodeValid?.(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
@ -63,6 +77,7 @@ export const CodeEditor = ({
|
||||
value={value}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(value?: string) => value && onChange?.(value)}
|
||||
onValidate={handleEditorValidation}
|
||||
options={{
|
||||
...options,
|
||||
overviewRulerLanes: 0,
|
||||
|
||||
@ -2,6 +2,7 @@ import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/setting
|
||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { useResetRecoilState } from 'recoil';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const ResetServerlessFunctionStatesEffect = () => {
|
||||
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
||||
@ -13,8 +14,14 @@ export const ResetServerlessFunctionStatesEffect = () => {
|
||||
const resetSettingsServerlessFunctionCodeEditorOutputParamsState =
|
||||
useResetRecoilState(settingsServerlessFunctionCodeEditorOutputParamsState);
|
||||
|
||||
resetSettingsServerlessFunctionInput();
|
||||
resetSettingsServerlessFunctionOutput();
|
||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
||||
useEffect(() => {
|
||||
resetSettingsServerlessFunctionInput();
|
||||
resetSettingsServerlessFunctionOutput();
|
||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
||||
}, [
|
||||
resetSettingsServerlessFunctionInput,
|
||||
resetSettingsServerlessFunctionOutput,
|
||||
resetSettingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
]);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-f
|
||||
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||
import { usePublishOneServerlessFunction } from '@/settings/serverless-functions/hooks/usePublishOneServerlessFunction';
|
||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
@ -18,7 +19,10 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
||||
import { useState } from 'react';
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||
|
||||
@ -29,10 +33,16 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
const [isCodeValid, setIsCodeValid] = useState(true);
|
||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||
const { publishOneServerlessFunction } = usePublishOneServerlessFunction();
|
||||
const [formValues, setFormValues] =
|
||||
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||
const { code: latestVersionCode } = useGetOneServerlessFunctionSourceCode({
|
||||
id: serverlessFunctionId,
|
||||
version: 'latest',
|
||||
});
|
||||
const setSettingsServerlessFunctionOutput = useSetRecoilState(
|
||||
settingsServerlessFunctionOutputState,
|
||||
);
|
||||
@ -70,13 +80,56 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const handleExecute = async () => {
|
||||
await handleSave();
|
||||
const resetDisabled =
|
||||
!isDefined(latestVersionCode) || latestVersionCode === formValues.code;
|
||||
const publishDisabled = !isCodeValid || latestVersionCode === formValues.code;
|
||||
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
const result = await executeOneServerlessFunction(
|
||||
serverlessFunctionId,
|
||||
JSON.parse(settingsServerlessFunctionInput),
|
||||
const newState = {
|
||||
code: latestVersionCode || '',
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
...newState,
|
||||
}));
|
||||
await handleSave();
|
||||
} catch (err) {
|
||||
enqueueSnackBar(
|
||||
(err as Error)?.message || 'An error occurred while reset function',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
try {
|
||||
await publishOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
enqueueSnackBar(`New function version has been published`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} catch (err) {
|
||||
enqueueSnackBar(
|
||||
(err as Error)?.message ||
|
||||
'An error occurred while publishing new version',
|
||||
{
|
||||
variant: SnackBarVariant.Error,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async () => {
|
||||
try {
|
||||
const result = await executeOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
payload: JSON.parse(settingsServerlessFunctionInput),
|
||||
version: 'draft',
|
||||
});
|
||||
setSettingsServerlessFunctionOutput({
|
||||
data: result?.data?.executeOneServerlessFunction?.data
|
||||
? JSON.stringify(
|
||||
@ -119,7 +172,12 @@ export const SettingsServerlessFunctionDetail = () => {
|
||||
<SettingsServerlessFunctionCodeEditorTab
|
||||
formValues={formValues}
|
||||
handleExecute={handleExecute}
|
||||
handlePublish={handlePublish}
|
||||
handleReset={handleReset}
|
||||
resetDisabled={resetDisabled}
|
||||
publishDisabled={publishDisabled}
|
||||
onChange={onChange}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
/>
|
||||
);
|
||||
case 'test':
|
||||
|
||||
Reference in New Issue
Block a user