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