Serverless function follow up (#9924)
- remove asynchronous serverless function build - build serverless function synchronously instead on activate workflow or execute - add a loader on workflow code step test tab test button - add a new `ServerlessFunctionSyncStatus` `BUILDING` - add a new route to build a serverless function draft version - delay artificially execution to avoid UI flashing https://github.com/user-attachments/assets/8d958d9a-ef41-4261-999e-6ea374191e33
This commit is contained in:
@ -1,15 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||
import {
|
||||
DEFAULT_OUTPUT_VALUE,
|
||||
ServerlessFunctionTestData,
|
||||
} from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||
import { ServerlessFunctionTestData } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import {
|
||||
CodeEditor,
|
||||
CoreEditorHeader,
|
||||
IconSquareRoundedCheck,
|
||||
IconSquareRoundedX,
|
||||
IconLoader,
|
||||
IconSettings,
|
||||
AnimatedCircleLoading,
|
||||
} from 'twenty-ui';
|
||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||
|
||||
@ -18,20 +19,33 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledOutput = styled.div<{ status?: ServerlessFunctionExecutionStatus }>`
|
||||
type OutputAccent = 'default' | 'success' | 'error';
|
||||
|
||||
const StyledInfoContainer = styled.div`
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
`;
|
||||
|
||||
const StyledOutput = styled.div<{ accent?: OutputAccent }>`
|
||||
align-items: center;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme, status }) =>
|
||||
status === ServerlessFunctionExecutionStatus.SUCCESS
|
||||
color: ${({ theme, accent }) =>
|
||||
accent === 'success'
|
||||
? theme.color.turquoise
|
||||
: theme.color.red};
|
||||
: accent === 'error'
|
||||
? theme.color.red
|
||||
: theme.font.color.secondary};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const ServerlessFunctionExecutionResult = ({
|
||||
serverlessFunctionTestData,
|
||||
isTesting = false,
|
||||
isBuilding = false,
|
||||
}: {
|
||||
serverlessFunctionTestData: ServerlessFunctionTestData;
|
||||
isTesting?: boolean;
|
||||
isBuilding?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -40,25 +54,60 @@ export const ServerlessFunctionExecutionResult = ({
|
||||
serverlessFunctionTestData.output.error ||
|
||||
'';
|
||||
|
||||
const leftNode =
|
||||
serverlessFunctionTestData.output.data === DEFAULT_OUTPUT_VALUE ? (
|
||||
'Output'
|
||||
) : (
|
||||
<StyledOutput status={serverlessFunctionTestData.output.status}>
|
||||
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
||||
{serverlessFunctionTestData.output.status ===
|
||||
ServerlessFunctionExecutionStatus.SUCCESS
|
||||
? '200 OK'
|
||||
: '500 Error'}
|
||||
{' - '}
|
||||
{serverlessFunctionTestData.output.duration}ms
|
||||
</StyledOutput>
|
||||
);
|
||||
const SuccessLeftNode = (
|
||||
<StyledOutput accent="success">
|
||||
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
||||
200 OK - {serverlessFunctionTestData.output.duration}ms
|
||||
</StyledOutput>
|
||||
);
|
||||
|
||||
const ErrorLeftNode = (
|
||||
<StyledOutput accent="error">
|
||||
<IconSquareRoundedX size={theme.icon.size.md} />
|
||||
500 Error - {serverlessFunctionTestData.output.duration}ms
|
||||
</StyledOutput>
|
||||
);
|
||||
|
||||
const IdleLeftNode = 'Output';
|
||||
|
||||
const PendingLeftNode = (isTesting || isBuilding) && (
|
||||
<StyledOutput>
|
||||
<AnimatedCircleLoading>
|
||||
{isTesting ? (
|
||||
<IconLoader size={theme.icon.size.md} />
|
||||
) : (
|
||||
<IconSettings size={theme.icon.size.md} />
|
||||
)}
|
||||
</AnimatedCircleLoading>
|
||||
<StyledInfoContainer>
|
||||
{isTesting ? 'Running function' : 'Building function'}
|
||||
</StyledInfoContainer>
|
||||
</StyledOutput>
|
||||
);
|
||||
|
||||
const computeLeftNode = () => {
|
||||
if (isTesting || isBuilding) {
|
||||
return PendingLeftNode;
|
||||
}
|
||||
if (
|
||||
serverlessFunctionTestData.output.status ===
|
||||
ServerlessFunctionExecutionStatus.ERROR
|
||||
) {
|
||||
return ErrorLeftNode;
|
||||
}
|
||||
if (
|
||||
serverlessFunctionTestData.output.status ===
|
||||
ServerlessFunctionExecutionStatus.SUCCESS
|
||||
) {
|
||||
return SuccessLeftNode;
|
||||
}
|
||||
return IdleLeftNode;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<CoreEditorHeader
|
||||
leftNodes={[leftNode]}
|
||||
leftNodes={[computeLeftNode()]}
|
||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||
/>
|
||||
<CodeEditor
|
||||
@ -66,6 +115,7 @@ export const ServerlessFunctionExecutionResult = ({
|
||||
language={serverlessFunctionTestData.language}
|
||||
height={serverlessFunctionTestData.height}
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
isLoading={isTesting || isBuilding}
|
||||
withHeader
|
||||
/>
|
||||
</StyledContainer>
|
||||
|
||||
@ -2,55 +2,76 @@ import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useState } from 'react';
|
||||
import { useBuildDraftServerlessFunction } from '@/settings/serverless-functions/hooks/useBuildDraftServerlessFunction';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const useTestServerlessFunction = ({
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion = 'draft',
|
||||
callback,
|
||||
}: {
|
||||
serverlessFunctionId: string;
|
||||
serverlessFunctionVersion?: string;
|
||||
callback?: (testResult: object) => void;
|
||||
}) => {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [isBuilding, setIsBuilding] = useState(false);
|
||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||
const { buildDraftServerlessFunction } = useBuildDraftServerlessFunction();
|
||||
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||
|
||||
const testServerlessFunction = async () => {
|
||||
const result = await executeOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
payload: serverlessFunctionTestData.input,
|
||||
version: serverlessFunctionVersion,
|
||||
});
|
||||
const testServerlessFunction = async (shouldBuild = true) => {
|
||||
try {
|
||||
if (shouldBuild) {
|
||||
setIsBuilding(true);
|
||||
await buildDraftServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
setIsBuilding(false);
|
||||
}
|
||||
setIsTesting(true);
|
||||
await sleep(200); // Delay artificially to avoid flashing the UI
|
||||
const result = await executeOneServerlessFunction({
|
||||
id: serverlessFunctionId,
|
||||
payload: serverlessFunctionTestData.input,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
if (isDefined(result?.data?.executeOneServerlessFunction?.data)) {
|
||||
callback?.(result?.data?.executeOneServerlessFunction?.data);
|
||||
setIsTesting(false);
|
||||
|
||||
if (isDefined(result?.data?.executeOneServerlessFunction?.data)) {
|
||||
callback?.(result?.data?.executeOneServerlessFunction?.data);
|
||||
}
|
||||
|
||||
setServerlessFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
language: 'json',
|
||||
height: 300,
|
||||
output: {
|
||||
data: result?.data?.executeOneServerlessFunction?.data
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneServerlessFunction?.data,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||
status: result?.data?.executeOneServerlessFunction?.status,
|
||||
error: result?.data?.executeOneServerlessFunction?.error
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneServerlessFunction?.error,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
setIsBuilding(false);
|
||||
setIsTesting(false);
|
||||
throw error;
|
||||
}
|
||||
|
||||
setServerlessFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
language: 'json',
|
||||
height: 300,
|
||||
output: {
|
||||
data: result?.data?.executeOneServerlessFunction?.data
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneServerlessFunction?.data,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
duration: result?.data?.executeOneServerlessFunction?.duration,
|
||||
status: result?.data?.executeOneServerlessFunction?.status,
|
||||
error: result?.data?.executeOneServerlessFunction?.error
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneServerlessFunction?.error,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return { testServerlessFunction };
|
||||
return { testServerlessFunction, isTesting, isBuilding };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user