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:
martmull
2025-01-31 17:12:42 +01:00
committed by GitHub
parent f47c0d45e3
commit ae62789159
28 changed files with 430 additions and 224 deletions

View File

@ -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

View File

@ -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(), '⏎']}
/> />
); );

View File

@ -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>

View File

@ -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 };
}; };

View File

@ -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
}
}
`;

View File

@ -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 };
};

View File

@ -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 };
}; };

View File

@ -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 };
}; };

View File

@ -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,
}, },
}); });

View File

@ -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}
/>,
]} ]}
/> />
)} )}

View File

@ -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"`,
);
}
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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',
} }

View File

@ -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,
});
}
}
}
}

View File

@ -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',
} }

View File

@ -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',
} }

View File

@ -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 {}

View File

@ -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);
}
}
} }

View File

@ -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;
} }
} }

View File

@ -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:

View File

@ -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,

View File

@ -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);

View File

@ -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,
}, },
}; };

View File

@ -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>
);
};

View File

@ -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';

View File

@ -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';