Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8726 workflow add a test button in workflow code step #9016

Merged
merged 31 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2f5b4a4
Clean code
martmull Dec 6, 2024
c690eb0
Clean code again and add tabs
martmull Dec 6, 2024
0b6fb07
Add Variables to renderFields
martmull Dec 6, 2024
1cb7109
Wip: Add ExecutionResult component
martmull Dec 9, 2024
b7d8f32
Fix button style
martmull Dec 9, 2024
252602b
Remove button from code tab
martmull Dec 9, 2024
d7ef96f
WIP: Improve functionInput computation
martmull Dec 10, 2024
984c7e6
Remove code introspection module
martmull Dec 10, 2024
7bd28e5
Simplify component
martmull Dec 10, 2024
285f352
Simplify component 2
martmull Dec 10, 2024
577afd9
Simplify useTabList
martmull Dec 11, 2024
2a13749
Add test button to code tab
martmull Dec 11, 2024
cfc4e17
Fix test
martmull Dec 11, 2024
d55144a
Fix outputSchema computing
martmull Dec 11, 2024
b41d42f
Remove Action command from footer
martmull Dec 11, 2024
e427018
Merge branch 'main' into 8726-workflow-add-a-test-button-in-workflow-…
martmull Dec 11, 2024
f12418a
Fix test tab redirect in settings
martmull Dec 11, 2024
601bee3
Fix lint
martmull Dec 11, 2024
096994f
Fix missing changes
martmull Dec 11, 2024
4136651
Code review returns: greptile
martmull Dec 11, 2024
3d0c160
Fix ci
martmull Dec 11, 2024
43366b9
Code review returns
martmull Dec 11, 2024
5d7b7a4
Fix ci
martmull Dec 11, 2024
1c19366
Fix memory heap for storybook build
martmull Dec 12, 2024
2a938f1
Code review returns
martmull Dec 12, 2024
c2cca64
Merge branch 'main' into 8726-workflow-add-a-test-button-in-workflow-…
martmull Dec 12, 2024
c83bb61
Fix tests
martmull Dec 12, 2024
70dc0a6
Code review returns
martmull Dec 13, 2024
8742e73
Export component instead of StyledComponent
martmull Dec 13, 2024
74d38f0
Fix main failing test
martmull Dec 13, 2024
c2907bd
Fix Workflow execution
martmull Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1571,8 +1571,6 @@ export type UpdateServerlessFunctionInput = {
export type UpdateWorkflowVersionStepInput = {
/** Step to update in JSON format */
step: Scalars['JSON']['input'];
/** Boolean to check if we need to update stepOutput */
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']['input']>;
/** Workflow version ID */
workflowVersionId: Scalars['String']['input'];
};
Expand Down Expand Up @@ -1707,6 +1705,7 @@ export type Workspace = {
__typename?: 'Workspace';
activationStatus: WorkspaceActivationStatus;
allowImpersonation: Scalars['Boolean']['output'];
billingCustomers?: Maybe<Array<BillingCustomer>>;
billingEntitlements?: Maybe<Array<BillingEntitlement>>;
billingSubscriptions?: Maybe<Array<BillingSubscription>>;
createdAt: Scalars['DateTime']['output'];
Expand All @@ -1732,6 +1731,12 @@ export type Workspace = {
};


export type WorkspaceBillingCustomersArgs = {
filter?: BillingCustomerFilter;
sorting?: Array<BillingCustomerSort>;
};


export type WorkspaceBillingEntitlementsArgs = {
filter?: BillingEntitlementFilter;
sorting?: Array<BillingEntitlementSort>;
Expand Down Expand Up @@ -1819,6 +1824,27 @@ export type WorkspaceNameAndId = {
id: Scalars['String']['output'];
};

export type BillingCustomer = {
__typename?: 'billingCustomer';
id: Scalars['UUID']['output'];
};

export type BillingCustomerFilter = {
and?: InputMaybe<Array<BillingCustomerFilter>>;
id?: InputMaybe<UuidFilterComparison>;
or?: InputMaybe<Array<BillingCustomerFilter>>;
};

export type BillingCustomerSort = {
direction: SortDirection;
field: BillingCustomerSortFields;
nulls?: InputMaybe<SortNulls>;
};

export enum BillingCustomerSortFields {
Id = 'id'
}

export type BillingEntitlement = {
__typename?: 'billingEntitlement';
id: Scalars['UUID']['output'];
Expand Down Expand Up @@ -2251,4 +2277,4 @@ export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitio
export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindManyAvailablePackagesQuery, FindManyAvailablePackagesQueryVariables>;
export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetManyServerlessFunctionsQuery, GetManyServerlessFunctionsQueryVariables>;
export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables>;
export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindOneServerlessFunctionSourceCodeQuery, FindOneServerlessFunctionSourceCodeQueryVariables>;
export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<FindOneServerlessFunctionSourceCodeQuery, FindOneServerlessFunctionSourceCodeQueryVariables>;
2 changes: 0 additions & 2 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1277,8 +1277,6 @@ export type UpdateServerlessFunctionInput = {
};

export type UpdateWorkflowVersionStepInput = {
/** Boolean to check if we need to update stepOutput */
shouldUpdateStepOutput?: InputMaybe<Scalars['Boolean']>;
/** Step to update in JSON format */
step: Scalars['JSON'];
/** Workflow version ID */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from 'twenty-ui';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';

export const CmdEnterActionButton = ({
title,
onClick,
}: {
title: string;
onClick: () => void;
}) => {
useScopedHotkeys(
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
() => onClick(),
RightDrawerHotkeyScope.RightDrawer,
[onClick],
martmull marked this conversation as resolved.
Show resolved Hide resolved
);

return (
<Button
title={title}
variant="primary"
accent="blue"
size="medium"
onClick={onClick}
shortcut={'⌘⏎'}
martmull marked this conversation as resolved.
Show resolved Hide resolved
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
Expand Down Expand Up @@ -43,8 +42,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
activityObjectNameSingular: CoreObjectNameSingular.Task,
});

const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState);
const { activeTabId } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);

const isLoading =
(activeTabId !== 'done' && tasksLoading) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ const SettingsServerlessFunctions = lazy(() =>
).then((module) => ({ default: module.SettingsServerlessFunctions })),
);

const SettingsServerlessFunctionDetailWrapper = lazy(() =>
const SettingsServerlessFunctionDetail = lazy(() =>
import(
'~/pages/settings/serverless-functions/SettingsServerlessFunctionDetailWrapper'
'~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail'
).then((module) => ({
default: module.SettingsServerlessFunctionDetailWrapper,
default: module.SettingsServerlessFunctionDetail,
})),
);

Expand Down Expand Up @@ -353,7 +353,7 @@ export const SettingsRoutes = ({
/>
<Route
path={SettingsPath.ServerlessFunctionDetail}
element={<SettingsServerlessFunctionDetailWrapper />}
element={<SettingsServerlessFunctionDetail />}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export const useTextVariableEditor = ({
const { tr } = state;

// Insert hard break using the view's state and dispatch
const transaction = tr.replaceSelectionWith(
state.schema.nodes.hardBreak.create(),
);
if (isDefined(state.schema.nodes.hardBreak)) {
martmull marked this conversation as resolved.
Show resolved Hide resolved
const transaction = tr.replaceSelectionWith(
state.schema.nodes.hardBreak.create(),
);

view.dispatch(transaction);
view.dispatch(transaction);
}
martmull marked this conversation as resolved.
Show resolved Hide resolved

return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import styled from '@emotion/styled';

import { useTheme } from '@emotion/react';
import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql';
import {
CodeEditor,
CoreEditorHeader,
IconSquareRoundedCheck,
} from 'twenty-ui';
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
import {
DEFAULT_OUTPUT_VALUE,
ServerlessFunctionTestData,
} from '@/workflow/states/serverlessFunctionTestDataFamilyState';

const StyledContainer = styled.div``;
martmull marked this conversation as resolved.
Show resolved Hide resolved

const StyledOutput = styled.div<{ status?: ServerlessFunctionExecutionStatus }>`
align-items: center;
gap: ${({ theme }) => theme.spacing(1)};
color: ${({ theme, status }) =>
status === ServerlessFunctionExecutionStatus.Success
? theme.color.turquoise
: theme.color.red};
display: flex;
`;

export const ServerlessFunctionExecutionResult = ({
serverlessFunctionTestData,
}: {
serverlessFunctionTestData: ServerlessFunctionTestData;
}) => {
const theme = useTheme();

const result =
serverlessFunctionTestData.output.data ||
serverlessFunctionTestData.output.error ||
'';
martmull marked this conversation as resolved.
Show resolved Hide resolved

const leftNode =
serverlessFunctionTestData.output.data === DEFAULT_OUTPUT_VALUE ? (
'Output'
) : (
<StyledOutput status={serverlessFunctionTestData.output.status}>
<IconSquareRoundedCheck size={theme.icon.size.md} />
{serverlessFunctionTestData.output.status ===
ServerlessFunctionExecutionStatus.Success
? '200 OK'
: '500 Error'}
{' - '}
{serverlessFunctionTestData.output.duration}ms
</StyledOutput>
martmull marked this conversation as resolved.
Show resolved Hide resolved
);

return (
<StyledContainer>
<CoreEditorHeader
leftNodes={[leftNode]}
rightNodes={[<LightCopyIconButton copyText={result} />]}
/>
<CodeEditor
value={result}
language={serverlessFunctionTestData.language}
height={serverlessFunctionTestData.height}
options={{ readOnly: true, domReadOnly: true }}
withHeader
/>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const INDEX_FILE_PATH = 'src/index.ts';
martmull marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
import { useRecoilState } from 'recoil';
import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isDefined } from 'twenty-ui';

export const useTestServerlessFunction = (
serverlessFunctionId: string,
callback?: (testResult: object) => Promise<void>,
martmull marked this conversation as resolved.
Show resolved Hide resolved
) => {
const { enqueueSnackBar } = useSnackBar();
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
useRecoilState(serverlessFunctionTestDataFamilyState(serverlessFunctionId));

const testServerlessFunction = async () => {
try {
const result = await executeOneServerlessFunction({
id: serverlessFunctionId,
payload: serverlessFunctionTestData.input,
version: 'draft',
});

if (isDefined(result?.data?.executeOneServerlessFunction?.data)) {
await callback?.(result?.data?.executeOneServerlessFunction?.data);
}
martmull marked this conversation as resolved.
Show resolved Hide resolved

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 (err) {
enqueueSnackBar(
(err as Error)?.message || 'An error occurred while executing function',
martmull marked this conversation as resolved.
Show resolved Hide resolved
{
variant: SnackBarVariant.Error,
},
);
}
};

return { testServerlessFunction };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { InputSchema } from '@/workflow/types/InputSchema';
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';

describe('getDefaultFunctionInputFromInputSchema', () => {
it('should init function input properly', () => {
const inputSchema = [
{
type: 'object',
properties: {
a: {
type: 'string',
},
b: {
type: 'number',
},
c: {
type: 'array',
items: { type: 'string' },
},
d: {
type: 'object',
properties: {
da: { type: 'string', enum: ['my', 'enum'] },
martmull marked this conversation as resolved.
Show resolved Hide resolved
db: { type: 'number' },
},
},
e: { type: 'object' },
},
},
] as InputSchema;
const expectedResult = [
{
a: null,
b: null,
c: [],
d: { da: 'my', db: null },
e: {},
},
];
expect(getDefaultFunctionInputFromInputSchema(inputSchema)).toEqual(
expectedResult,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';

describe('getFunctionInputFromSourceCode', () => {
it('should throw if params missing', () => {
const fileContents = [
'function testFunction() { return }',
'function testFunction(params1: {}, params2: {}) { return }',
'function testFunction(params: string) { return }',
];
for (const fileContent of fileContents) {
expect(() => getFunctionInputFromSourceCode(fileContent)).toThrow(
'Function should have one object parameter',
);
}
});
it('should return input from source code', () => {
const fileContent = `
function testFunction(
params: {
param1: string;
param2: number;
param3: boolean;
param4: object;
param5: { subParam1: string };
param6: "my" | "enum";
param7: string[];
}
): void {
return
martmull marked this conversation as resolved.
Show resolved Hide resolved
}
`;

const result = getFunctionInputFromSourceCode(fileContent);
expect(result).toEqual({
param1: null,
param2: null,
param3: null,
param4: {},
param5: {
subParam1: null,
},
param6: 'my',
param7: [],
});
});
});
Loading
Loading