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:
@ -34,6 +34,7 @@ const documents = {
|
|||||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
"\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 shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\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 isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\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 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 shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\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 isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\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 timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
|
"\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.BuildDraftServerlessFunctionDocument,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\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,
|
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
@ -143,6 +144,10 @@ 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.
|
* 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 runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\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.
|
||||||
|
*/
|
||||||
|
export function graphql(source: "\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation BuildDraftServerlessFunction(\n $input: BuildDraftServerlessFunctionInput!\n ) {\n buildDraftServerlessFunction(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.
|
* 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
@ -9,9 +9,11 @@ import { FeatureFlagKey } from '~/generated/graphql';
|
|||||||
export const CmdEnterActionButton = ({
|
export const CmdEnterActionButton = ({
|
||||||
title,
|
title,
|
||||||
onClick,
|
onClick,
|
||||||
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||||
@ -32,6 +34,7 @@ export const CmdEnterActionButton = ({
|
|||||||
accent="blue"
|
accent="blue"
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
hotkeys={[getOsControlSymbol(), '⏎']}
|
hotkeys={[getOsControlSymbol(), '⏎']}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
import {
|
import { ServerlessFunctionTestData } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
DEFAULT_OUTPUT_VALUE,
|
|
||||||
ServerlessFunctionTestData,
|
|
||||||
} from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import {
|
import {
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
CoreEditorHeader,
|
CoreEditorHeader,
|
||||||
IconSquareRoundedCheck,
|
IconSquareRoundedCheck,
|
||||||
|
IconSquareRoundedX,
|
||||||
|
IconLoader,
|
||||||
|
IconSettings,
|
||||||
|
AnimatedCircleLoading,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
@ -18,20 +19,33 @@ const StyledContainer = styled.div`
|
|||||||
flex-direction: column;
|
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;
|
align-items: center;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
color: ${({ theme, status }) =>
|
color: ${({ theme, accent }) =>
|
||||||
status === ServerlessFunctionExecutionStatus.SUCCESS
|
accent === 'success'
|
||||||
? theme.color.turquoise
|
? theme.color.turquoise
|
||||||
: theme.color.red};
|
: accent === 'error'
|
||||||
|
? theme.color.red
|
||||||
|
: theme.font.color.secondary};
|
||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ServerlessFunctionExecutionResult = ({
|
export const ServerlessFunctionExecutionResult = ({
|
||||||
serverlessFunctionTestData,
|
serverlessFunctionTestData,
|
||||||
|
isTesting = false,
|
||||||
|
isBuilding = false,
|
||||||
}: {
|
}: {
|
||||||
serverlessFunctionTestData: ServerlessFunctionTestData;
|
serverlessFunctionTestData: ServerlessFunctionTestData;
|
||||||
|
isTesting?: boolean;
|
||||||
|
isBuilding?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -40,25 +54,60 @@ export const ServerlessFunctionExecutionResult = ({
|
|||||||
serverlessFunctionTestData.output.error ||
|
serverlessFunctionTestData.output.error ||
|
||||||
'';
|
'';
|
||||||
|
|
||||||
const leftNode =
|
const SuccessLeftNode = (
|
||||||
serverlessFunctionTestData.output.data === DEFAULT_OUTPUT_VALUE ? (
|
<StyledOutput accent="success">
|
||||||
'Output'
|
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
||||||
) : (
|
200 OK - {serverlessFunctionTestData.output.duration}ms
|
||||||
<StyledOutput status={serverlessFunctionTestData.output.status}>
|
</StyledOutput>
|
||||||
<IconSquareRoundedCheck size={theme.icon.size.md} />
|
);
|
||||||
{serverlessFunctionTestData.output.status ===
|
|
||||||
ServerlessFunctionExecutionStatus.SUCCESS
|
const ErrorLeftNode = (
|
||||||
? '200 OK'
|
<StyledOutput accent="error">
|
||||||
: '500 Error'}
|
<IconSquareRoundedX size={theme.icon.size.md} />
|
||||||
{' - '}
|
500 Error - {serverlessFunctionTestData.output.duration}ms
|
||||||
{serverlessFunctionTestData.output.duration}ms
|
</StyledOutput>
|
||||||
</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 (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<CoreEditorHeader
|
<CoreEditorHeader
|
||||||
leftNodes={[leftNode]}
|
leftNodes={[computeLeftNode()]}
|
||||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||||
/>
|
/>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
@ -66,6 +115,7 @@ export const ServerlessFunctionExecutionResult = ({
|
|||||||
language={serverlessFunctionTestData.language}
|
language={serverlessFunctionTestData.language}
|
||||||
height={serverlessFunctionTestData.height}
|
height={serverlessFunctionTestData.height}
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
|
isLoading={isTesting || isBuilding}
|
||||||
withHeader
|
withHeader
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -2,55 +2,76 @@ import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
import { isDefined } from 'twenty-ui';
|
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 = ({
|
export const useTestServerlessFunction = ({
|
||||||
serverlessFunctionId,
|
serverlessFunctionId,
|
||||||
serverlessFunctionVersion = 'draft',
|
|
||||||
callback,
|
callback,
|
||||||
}: {
|
}: {
|
||||||
serverlessFunctionId: string;
|
serverlessFunctionId: string;
|
||||||
serverlessFunctionVersion?: string;
|
|
||||||
callback?: (testResult: object) => void;
|
callback?: (testResult: object) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [isBuilding, setIsBuilding] = useState(false);
|
||||||
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||||
|
const { buildDraftServerlessFunction } = useBuildDraftServerlessFunction();
|
||||||
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||||
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));
|
||||||
|
|
||||||
const testServerlessFunction = async () => {
|
const testServerlessFunction = async (shouldBuild = true) => {
|
||||||
const result = await executeOneServerlessFunction({
|
try {
|
||||||
id: serverlessFunctionId,
|
if (shouldBuild) {
|
||||||
payload: serverlessFunctionTestData.input,
|
setIsBuilding(true);
|
||||||
version: serverlessFunctionVersion,
|
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)) {
|
setIsTesting(false);
|
||||||
callback?.(result?.data?.executeOneServerlessFunction?.data);
|
|
||||||
|
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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const BUILD_DRAFT_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
mutation BuildDraftServerlessFunction(
|
||||||
|
$input: BuildDraftServerlessFunctionInput!
|
||||||
|
) {
|
||||||
|
buildDraftServerlessFunction(input: $input) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { BUILD_DRAFT_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/buildDraftServerlessFunction';
|
||||||
|
import {
|
||||||
|
BuildDraftServerlessFunctionMutation,
|
||||||
|
BuildDraftServerlessFunctionMutationVariables,
|
||||||
|
BuildDraftServerlessFunctionInput,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useBuildDraftServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
BuildDraftServerlessFunctionMutation,
|
||||||
|
BuildDraftServerlessFunctionMutationVariables
|
||||||
|
>(BUILD_DRAFT_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildDraftServerlessFunction = async (
|
||||||
|
input: BuildDraftServerlessFunctionInput,
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return { buildDraftServerlessFunction };
|
||||||
|
};
|
||||||
@ -25,6 +25,7 @@ export const useGetOneServerlessFunctionSourceCode = ({
|
|||||||
input: { id, version },
|
input: { id, version },
|
||||||
},
|
},
|
||||||
onCompleted,
|
onCompleted,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
});
|
});
|
||||||
return { code: data?.getServerlessFunctionSourceCode, loading };
|
return { code: data?.getServerlessFunctionSourceCode, loading };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,9 +6,6 @@ import {
|
|||||||
UpdateOneServerlessFunctionMutationVariables,
|
UpdateOneServerlessFunctionMutationVariables,
|
||||||
UpdateServerlessFunctionInput,
|
UpdateServerlessFunctionInput,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction';
|
|
||||||
import { sleep } from '~/utils/sleep';
|
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode';
|
||||||
|
|
||||||
@ -16,7 +13,6 @@ export const useUpdateOneServerlessFunction = (
|
|||||||
serverlessFunctionId: string,
|
serverlessFunctionId: string,
|
||||||
) => {
|
) => {
|
||||||
const apolloMetadataClient = useApolloMetadataClient();
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
const [isReady, setIsReady] = useState(false);
|
|
||||||
const [mutate] = useMutation<
|
const [mutate] = useMutation<
|
||||||
UpdateOneServerlessFunctionMutation,
|
UpdateOneServerlessFunctionMutation,
|
||||||
UpdateOneServerlessFunctionMutationVariables
|
UpdateOneServerlessFunctionMutationVariables
|
||||||
@ -27,7 +23,7 @@ export const useUpdateOneServerlessFunction = (
|
|||||||
const updateOneServerlessFunction = async (
|
const updateOneServerlessFunction = async (
|
||||||
input: Omit<UpdateServerlessFunctionInput, 'id'>,
|
input: Omit<UpdateServerlessFunctionInput, 'id'>,
|
||||||
) => {
|
) => {
|
||||||
const result = await mutate({
|
return await mutate({
|
||||||
variables: {
|
variables: {
|
||||||
input: { ...input, id: serverlessFunctionId },
|
input: { ...input, id: serverlessFunctionId },
|
||||||
},
|
},
|
||||||
@ -35,37 +31,7 @@ export const useUpdateOneServerlessFunction = (
|
|||||||
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setIsReady(false);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
return { updateOneServerlessFunction };
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
const pollFunctionStatus = async () => {
|
|
||||||
while (isMounted && !isReady) {
|
|
||||||
const { data } = await apolloMetadataClient.query({
|
|
||||||
query: FIND_ONE_SERVERLESS_FUNCTION,
|
|
||||||
variables: { input: { id: serverlessFunctionId } },
|
|
||||||
fetchPolicy: 'network-only', // Always fetch fresh data
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverlessFunction = data?.findOneServerlessFunction;
|
|
||||||
|
|
||||||
if (serverlessFunction?.syncStatus === 'READY') {
|
|
||||||
setIsReady(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await sleep(500);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pollFunctionStatus();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false; // Cleanup when the component unmounts
|
|
||||||
};
|
|
||||||
}, [serverlessFunctionId, apolloMetadataClient, isReady]);
|
|
||||||
|
|
||||||
return { updateOneServerlessFunction, isReady };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,10 @@ export type ServerlessFunctionTestData = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OUTPUT_VALUE = 'Enter an input above then press "Test"';
|
export const DEFAULT_OUTPUT_VALUE = {
|
||||||
|
data: 'Enter an input above then press "Test"',
|
||||||
|
status: ServerlessFunctionExecutionStatus.IDLE,
|
||||||
|
};
|
||||||
|
|
||||||
export const serverlessFunctionTestDataFamilyState = createFamilyState<
|
export const serverlessFunctionTestDataFamilyState = createFamilyState<
|
||||||
ServerlessFunctionTestData,
|
ServerlessFunctionTestData,
|
||||||
@ -24,6 +27,6 @@ export const serverlessFunctionTestDataFamilyState = createFamilyState<
|
|||||||
language: 'plaintext',
|
language: 'plaintext',
|
||||||
height: 64,
|
height: 64,
|
||||||
input: {},
|
input: {},
|
||||||
output: { data: DEFAULT_OUTPUT_VALUE },
|
output: DEFAULT_OUTPUT_VALUE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -86,12 +86,14 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
}: WorkflowEditActionFormServerlessFunctionProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
|
const [shouldBuildServerlessFunction, setShouldBuildServerlessFunction] =
|
||||||
|
useState(false);
|
||||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||||
const serverlessFunctionVersion =
|
const serverlessFunctionVersion =
|
||||||
action.settings.input.serverlessFunctionVersion;
|
action.settings.input.serverlessFunctionVersion;
|
||||||
const tabListId = `${WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}_${serverlessFunctionId}`;
|
const tabListId = `${WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}_${serverlessFunctionId}`;
|
||||||
const { activeTabId, setActiveTabId } = useTabList(tabListId);
|
const { activeTabId } = useTabList(tabListId);
|
||||||
const { updateOneServerlessFunction, isReady } =
|
const { updateOneServerlessFunction } =
|
||||||
useUpdateOneServerlessFunction(serverlessFunctionId);
|
useUpdateOneServerlessFunction(serverlessFunctionId);
|
||||||
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersion();
|
||||||
|
|
||||||
@ -126,13 +128,14 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { testServerlessFunction } = useTestServerlessFunction({
|
const { testServerlessFunction, isTesting, isBuilding } =
|
||||||
serverlessFunctionId,
|
useTestServerlessFunction({
|
||||||
serverlessFunctionVersion,
|
serverlessFunctionId,
|
||||||
callback: updateOutputSchemaFromTestResult,
|
callback: updateOutputSchemaFromTestResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSave = useDebouncedCallback(async () => {
|
const handleSave = useDebouncedCallback(async () => {
|
||||||
|
setShouldBuildServerlessFunction(true);
|
||||||
await updateOneServerlessFunction({
|
await updateOneServerlessFunction({
|
||||||
name: formValues.name,
|
name: formValues.name,
|
||||||
description: formValues.description,
|
description: formValues.description,
|
||||||
@ -231,8 +234,10 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRunFunction = async () => {
|
const handleRunFunction = async () => {
|
||||||
await testServerlessFunction();
|
if (!isTesting) {
|
||||||
setActiveTabId('test');
|
await testServerlessFunction(shouldBuildServerlessFunction);
|
||||||
|
setShouldBuildServerlessFunction(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditorDidMount = async (
|
const handleEditorDidMount = async (
|
||||||
@ -313,7 +318,6 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
readonly={actionOptions.readonly}
|
readonly={actionOptions.readonly}
|
||||||
/>
|
/>
|
||||||
<StyledCodeEditorContainer>
|
<StyledCodeEditorContainer>
|
||||||
<InputLabel>Code {!isReady && <span>•</span>}</InputLabel>
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
height={343}
|
height={343}
|
||||||
value={formValues.code?.[INDEX_FILE_PATH]}
|
value={formValues.code?.[INDEX_FILE_PATH]}
|
||||||
@ -339,6 +343,8 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
<InputLabel>Result</InputLabel>
|
<InputLabel>Result</InputLabel>
|
||||||
<ServerlessFunctionExecutionResult
|
<ServerlessFunctionExecutionResult
|
||||||
serverlessFunctionTestData={serverlessFunctionTestData}
|
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||||
|
isBuilding={isBuilding}
|
||||||
|
isTesting={isTesting}
|
||||||
/>
|
/>
|
||||||
</StyledCodeEditorContainer>
|
</StyledCodeEditorContainer>
|
||||||
</>
|
</>
|
||||||
@ -347,7 +353,11 @@ export const WorkflowEditActionFormServerlessFunction = ({
|
|||||||
{activeTabId === 'test' && (
|
{activeTabId === 'test' && (
|
||||||
<RightDrawerFooter
|
<RightDrawerFooter
|
||||||
actions={[
|
actions={[
|
||||||
<CmdEnterActionButton title="Test" onClick={handleRunFunction} />,
|
<CmdEnterActionButton
|
||||||
|
title="Test"
|
||||||
|
onClick={handleRunFunction}
|
||||||
|
disabled={isTesting || isBuilding}
|
||||||
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddNewSyncStatusToServerless1738233783889
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddNewSyncStatusToServerless1738233783889';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "metadata"."serverlessFunction_syncstatus_enum" RENAME TO "serverlessFunction_syncstatus_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "metadata"."serverlessFunction_syncstatus_enum" AS ENUM('NOT_READY', 'BUILDING', 'READY')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" TYPE "metadata"."serverlessFunction_syncstatus_enum" USING "syncStatus"::"text"::"metadata"."serverlessFunction_syncstatus_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" SET DEFAULT 'NOT_READY'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP TYPE "metadata"."serverlessFunction_syncstatus_enum_old"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "metadata"."serverlessFunction_syncstatus_enum_old" AS ENUM('NOT_READY', 'READY')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" TYPE "metadata"."serverlessFunction_syncstatus_enum_old" USING "syncStatus"::"text"::"metadata"."serverlessFunction_syncstatus_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ALTER COLUMN "syncStatus" SET DEFAULT 'NOT_READY'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP TYPE "metadata"."serverlessFunction_syncstatus_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "metadata"."serverlessFunction_syncstatus_enum_old" RENAME TO "serverlessFunction_syncstatus_enum"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -54,7 +54,7 @@ import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/ut
|
|||||||
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
|
||||||
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder';
|
||||||
|
|
||||||
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 30;
|
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
|
||||||
|
|
||||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||||
fileStorageService: FileStorageService;
|
fileStorageService: FileStorageService;
|
||||||
@ -133,7 +133,7 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
await lambdaBuildDirectoryManager.clean();
|
await lambdaBuildDirectoryManager.clean();
|
||||||
|
|
||||||
if (!isDefined(result.LayerVersionArn)) {
|
if (!isDefined(result.LayerVersionArn)) {
|
||||||
throw new Error('new layer version arn si undefined');
|
throw new Error('new layer version arn if undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.LayerVersionArn;
|
return result.LayerVersionArn;
|
||||||
@ -177,15 +177,13 @@ export class LambdaDriver implements ServerlessDriver {
|
|||||||
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version);
|
||||||
};
|
};
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity, version: string) {
|
async build(serverlessFunction: ServerlessFunctionEntity, version: 'draft') {
|
||||||
const computedVersion =
|
if (version !== 'draft') {
|
||||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
throw new Error("We can only build 'draft' version with lambda driver");
|
||||||
|
}
|
||||||
|
|
||||||
const inMemoryServerlessFunctionFolderPath =
|
const inMemoryServerlessFunctionFolderPath =
|
||||||
this.getInMemoryServerlessFunctionFolderPath(
|
this.getInMemoryServerlessFunctionFolderPath(serverlessFunction, version);
|
||||||
serverlessFunction,
|
|
||||||
computedVersion,
|
|
||||||
);
|
|
||||||
|
|
||||||
const folderPath = getServerlessFolder({
|
const folderPath = getServerlessFolder({
|
||||||
serverlessFunction,
|
serverlessFunction,
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ID, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class BuildDraftServerlessFunctionInput {
|
||||||
|
@IDField(() => ID, { description: 'The id of the function.' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { IsObject, IsOptional } from 'class-validator';
|
|||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
export enum ServerlessFunctionExecutionStatus {
|
export enum ServerlessFunctionExecutionStatus {
|
||||||
|
IDLE = 'IDLE',
|
||||||
SUCCESS = 'SUCCESS',
|
SUCCESS = 'SUCCESS',
|
||||||
ERROR = 'ERROR',
|
ERROR = 'ERROR',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import { Scope } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
||||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
|
||||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
|
||||||
import {
|
|
||||||
ServerlessFunctionEntity,
|
|
||||||
ServerlessFunctionSyncStatus,
|
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
|
|
||||||
export type BuildServerlessFunctionBatchEvent = {
|
|
||||||
serverlessFunctions: {
|
|
||||||
serverlessFunctionId: string;
|
|
||||||
serverlessFunctionVersion: string;
|
|
||||||
}[];
|
|
||||||
workspaceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Processor({
|
|
||||||
queueName: MessageQueue.serverlessFunctionQueue,
|
|
||||||
scope: Scope.REQUEST,
|
|
||||||
})
|
|
||||||
export class BuildServerlessFunctionJob {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
|
||||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
|
||||||
private readonly serverlessService: ServerlessService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Process(BuildServerlessFunctionJob.name)
|
|
||||||
async handle(batchEvent: BuildServerlessFunctionBatchEvent): Promise<void> {
|
|
||||||
for (const {
|
|
||||||
serverlessFunctionId,
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
} of batchEvent.serverlessFunctions) {
|
|
||||||
const serverlessFunction =
|
|
||||||
await this.serverlessFunctionRepository.findOneBy({
|
|
||||||
id: serverlessFunctionId,
|
|
||||||
workspaceId: batchEvent.workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(serverlessFunction)) {
|
|
||||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
|
||||||
});
|
|
||||||
await this.serverlessService.build(
|
|
||||||
serverlessFunction,
|
|
||||||
serverlessFunctionVersion,
|
|
||||||
);
|
|
||||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -13,6 +13,7 @@ const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
|
|||||||
|
|
||||||
export enum ServerlessFunctionSyncStatus {
|
export enum ServerlessFunctionSyncStatus {
|
||||||
NOT_READY = 'NOT_READY',
|
NOT_READY = 'NOT_READY',
|
||||||
|
BUILDING = 'BUILDING',
|
||||||
READY = 'READY',
|
READY = 'READY',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,5 +13,6 @@ export enum ServerlessFunctionExceptionCode {
|
|||||||
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||||
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
||||||
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
||||||
|
SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING',
|
||||||
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
|
SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED = 'SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.mod
|
|||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { BuildServerlessFunctionJob } from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -22,11 +21,7 @@ import { BuildServerlessFunctionJob } from 'src/engine/metadata-modules/serverle
|
|||||||
ThrottlerModule,
|
ThrottlerModule,
|
||||||
AnalyticsModule,
|
AnalyticsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
||||||
ServerlessFunctionService,
|
|
||||||
ServerlessFunctionResolver,
|
|
||||||
BuildServerlessFunctionJob,
|
|
||||||
],
|
|
||||||
exports: [ServerlessFunctionService],
|
exports: [ServerlessFunctionService],
|
||||||
})
|
})
|
||||||
export class ServerlessFunctionModule {}
|
export class ServerlessFunctionModule {}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||||
|
import { BuildDraftServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/build-draft-serverless-function.input';
|
||||||
|
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -204,4 +205,22 @@ export class ServerlessFunctionResolver {
|
|||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => ServerlessFunctionDTO)
|
||||||
|
async buildDraftServerlessFunction(
|
||||||
|
@Args('input') input: BuildDraftServerlessFunctionInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
const { id } = input;
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.buildDraftServerlessFunction(
|
||||||
|
id,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,10 +26,6 @@ import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/se
|
|||||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||||
import {
|
|
||||||
BuildServerlessFunctionBatchEvent,
|
|
||||||
BuildServerlessFunctionJob,
|
|
||||||
} from 'src/engine/metadata-modules/serverless-function/jobs/build-serverless-function.job';
|
|
||||||
import {
|
import {
|
||||||
ServerlessFunctionEntity,
|
ServerlessFunctionEntity,
|
||||||
ServerlessFunctionSyncStatus,
|
ServerlessFunctionSyncStatus,
|
||||||
@ -141,6 +137,16 @@ export class ServerlessFunctionService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
version === 'draft' &&
|
||||||
|
functionToExecute.syncStatus !== ServerlessFunctionSyncStatus.READY
|
||||||
|
) {
|
||||||
|
await this.buildDraftServerlessFunction(
|
||||||
|
functionToExecute.id,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const resultServerlessFunction = await this.serverlessService.execute(
|
const resultServerlessFunction = await this.serverlessService.execute(
|
||||||
functionToExecute,
|
functionToExecute,
|
||||||
payload,
|
payload,
|
||||||
@ -276,12 +282,6 @@ export class ServerlessFunctionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.buildServerlessFunction({
|
|
||||||
serverlessFunctionId: existingServerlessFunction.id,
|
|
||||||
serverlessFunctionVersion: 'draft',
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.serverlessFunctionRepository.findOneBy({
|
return this.serverlessFunctionRepository.findOneBy({
|
||||||
id: existingServerlessFunction.id,
|
id: existingServerlessFunction.id,
|
||||||
});
|
});
|
||||||
@ -322,6 +322,7 @@ export class ServerlessFunctionService {
|
|||||||
...serverlessFunctionInput,
|
...serverlessFunctionInput,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
layerVersion: LAST_LAYER_VERSION,
|
layerVersion: LAST_LAYER_VERSION,
|
||||||
|
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdServerlessFunction =
|
const createdServerlessFunction =
|
||||||
@ -341,12 +342,6 @@ export class ServerlessFunctionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.buildServerlessFunction({
|
|
||||||
serverlessFunctionId: createdServerlessFunction.id,
|
|
||||||
serverlessFunctionVersion: 'draft',
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.serverlessFunctionRepository.findOneBy({
|
return this.serverlessFunctionRepository.findOneBy({
|
||||||
id: createdServerlessFunction.id,
|
id: createdServerlessFunction.id,
|
||||||
});
|
});
|
||||||
@ -361,6 +356,10 @@ export class ServerlessFunctionService {
|
|||||||
version: string;
|
version: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}) {
|
}) {
|
||||||
|
if (version === 'draft') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const serverlessFunction = await this.findOneOrFail({
|
const serverlessFunction = await this.findOneOrFail({
|
||||||
id,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -381,10 +380,8 @@ export class ServerlessFunctionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.buildServerlessFunction({
|
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
||||||
serverlessFunctionId: id,
|
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||||
serverlessFunctionVersion: 'draft',
|
|
||||||
workspaceId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,24 +400,31 @@ export class ServerlessFunctionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildServerlessFunction({
|
async buildDraftServerlessFunction(id: string, workspaceId: string) {
|
||||||
serverlessFunctionId,
|
const functionToBuild = await this.findOneOrFail({
|
||||||
serverlessFunctionVersion,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
});
|
||||||
serverlessFunctionId: string;
|
|
||||||
serverlessFunctionVersion: string;
|
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.READY) {
|
||||||
workspaceId: string;
|
return functionToBuild;
|
||||||
}) {
|
}
|
||||||
await this.messageQueueService.add<BuildServerlessFunctionBatchEvent>(
|
|
||||||
BuildServerlessFunctionJob.name,
|
if (functionToBuild.syncStatus === ServerlessFunctionSyncStatus.BUILDING) {
|
||||||
{
|
throw new ServerlessFunctionException(
|
||||||
serverlessFunctions: [
|
'This function is currently building. Please try later',
|
||||||
{ serverlessFunctionId, serverlessFunctionVersion },
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING,
|
||||||
],
|
);
|
||||||
workspaceId,
|
}
|
||||||
},
|
|
||||||
{ id: `${serverlessFunctionId}-${serverlessFunctionVersion}` },
|
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
||||||
);
|
syncStatus: ServerlessFunctionSyncStatus.BUILDING,
|
||||||
|
});
|
||||||
|
await this.serverlessService.build(functionToBuild, 'draft');
|
||||||
|
await this.serverlessFunctionRepository.update(functionToBuild.id, {
|
||||||
|
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||||
|
});
|
||||||
|
|
||||||
|
return functionToBuild;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
|||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
||||||
throw new ConflictError(error.message);
|
throw new ConflictError(error.message);
|
||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
|
||||||
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
|
||||||
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
|
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
|
||||||
throw new ForbiddenError(error.message);
|
throw new ForbiddenError(error.message);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -173,6 +173,7 @@ export {
|
|||||||
IconListCheck,
|
IconListCheck,
|
||||||
IconListDetails,
|
IconListDetails,
|
||||||
IconListNumbers,
|
IconListNumbers,
|
||||||
|
IconLoader,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconLockOpen,
|
IconLockOpen,
|
||||||
IconMail,
|
IconMail,
|
||||||
@ -236,6 +237,7 @@ export {
|
|||||||
IconSquare,
|
IconSquare,
|
||||||
IconSquareKey,
|
IconSquareKey,
|
||||||
IconSquareRoundedCheck,
|
IconSquareRoundedCheck,
|
||||||
|
IconSquareRoundedX,
|
||||||
IconTable,
|
IconTable,
|
||||||
IconTag,
|
IconTag,
|
||||||
IconTags,
|
IconTags,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Loader } from '@ui/feedback/loader/components/Loader';
|
||||||
import { useTheme, css } from '@emotion/react';
|
import { useTheme, css } from '@emotion/react';
|
||||||
import Editor, { EditorProps, Monaco } from '@monaco-editor/react';
|
import Editor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||||
import { codeEditorTheme } from '@ui/input';
|
import { codeEditorTheme } from '@ui/input';
|
||||||
@ -10,8 +11,30 @@ type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
|||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
setMarkers?: (value: string) => editor.IMarkerData[];
|
setMarkers?: (value: string) => editor.IMarkerData[];
|
||||||
withHeader?: boolean;
|
withHeader?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledEditorLoader = styled.div<{
|
||||||
|
height: string | number;
|
||||||
|
withHeader?: boolean;
|
||||||
|
}>`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: ${({ height }) => height}px;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
${({ withHeader, theme }) =>
|
||||||
|
withHeader
|
||||||
|
? css`
|
||||||
|
border-radius: 0 0 ${theme.border.radius.sm} ${theme.border.radius.sm};
|
||||||
|
border-top: none;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledEditor = styled(Editor)<{ withHeader: boolean }>`
|
const StyledEditor = styled(Editor)<{ withHeader: boolean }>`
|
||||||
.monaco-editor {
|
.monaco-editor {
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
@ -42,6 +65,7 @@ export const CodeEditor = ({
|
|||||||
onValidate,
|
onValidate,
|
||||||
height = 450,
|
height = 450,
|
||||||
withHeader = false,
|
withHeader = false,
|
||||||
|
isLoading = false,
|
||||||
options,
|
options,
|
||||||
}: CodeEditorProps) => {
|
}: CodeEditorProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -64,12 +88,17 @@ export const CodeEditor = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return isLoading ? (
|
||||||
|
<StyledEditorLoader height={height} withHeader={withHeader}>
|
||||||
|
<Loader />
|
||||||
|
</StyledEditorLoader>
|
||||||
|
) : (
|
||||||
<StyledEditor
|
<StyledEditor
|
||||||
height={height}
|
height={height}
|
||||||
withHeader={withHeader}
|
withHeader={withHeader}
|
||||||
value={value}
|
value={isLoading ? '' : value}
|
||||||
language={language}
|
language={language}
|
||||||
|
loading=""
|
||||||
onMount={(editor, monaco) => {
|
onMount={(editor, monaco) => {
|
||||||
setMonaco(monaco);
|
setMonaco(monaco);
|
||||||
setEditor(editor);
|
setEditor(editor);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export const ANIMATION = {
|
|||||||
instant: 0.075,
|
instant: 0.075,
|
||||||
fast: 0.15,
|
fast: 0.15,
|
||||||
normal: 0.3,
|
normal: 0.3,
|
||||||
|
slow: 1.5,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
const StyledAnimatedContainer = styled(motion.div)`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AnimatedCircleLoading = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<StyledAnimatedContainer
|
||||||
|
initial={{ rotate: 0 }}
|
||||||
|
animate={{
|
||||||
|
rotate: 360,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: theme.animation.duration.slow,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledAnimatedContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,3 +4,4 @@ export * from './components/AnimatedEaseInOut';
|
|||||||
export * from './components/AnimatedFadeOut';
|
export * from './components/AnimatedFadeOut';
|
||||||
export * from './components/AnimatedTextWord';
|
export * from './components/AnimatedTextWord';
|
||||||
export * from './components/AnimatedTranslation';
|
export * from './components/AnimatedTranslation';
|
||||||
|
export * from './components/AnimatedCircleLoading';
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from './animation/components/AnimatedCircleLoading';
|
||||||
export * from './animation/components/AnimatedContainer';
|
export * from './animation/components/AnimatedContainer';
|
||||||
export * from './animation/components/AnimatedEaseIn';
|
export * from './animation/components/AnimatedEaseIn';
|
||||||
export * from './animation/components/AnimatedEaseInOut';
|
export * from './animation/components/AnimatedEaseInOut';
|
||||||
|
|||||||
Reference in New Issue
Block a user