Skip to content

Commit

Permalink
Add Workflow Run show page (#7719)
Browse files Browse the repository at this point in the history
In this PR:

- Display a workflow version visualizer for the version of the workflow
the run was executed on.
- Display the output of the run as code.


https://github.com/user-attachments/assets/d617300a-bff4-4328-a35c-291dc86d81cf
  • Loading branch information
Devessier authored Oct 21, 2024
1 parent b914182 commit e7eeb3b
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 168 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconMail,
IconNotes,
IconPaperclip,
IconPrinter,
IconSettings,
IconTimelineEvent,
} from 'twenty-ui';
Expand All @@ -26,6 +27,10 @@ export const useRecordShowContainerTabs = (
const isWorkflowVersion =
isWorkflowEnabled &&
targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion;
const isWorkflowRun =
isWorkflowEnabled &&
targetObjectNameSingular === CoreObjectNameSingular.WorkflowRun;
const isWorkflowRelated = isWorkflow || isWorkflowVersion || isWorkflowRun;

const isCompanyOrPerson = [
CoreObjectNameSingular.Company,
Expand Down Expand Up @@ -54,7 +59,7 @@ export const useRecordShowContainerTabs = (
id: 'timeline',
title: 'Timeline',
Icon: IconTimelineEvent,
hide: isInRightDrawer || isWorkflow || isWorkflowVersion,
hide: isInRightDrawer || isWorkflowRelated,
},
{
id: 'tasks',
Expand All @@ -63,8 +68,7 @@ export const useRecordShowContainerTabs = (
hide:
targetObjectNameSingular === CoreObjectNameSingular.Note ||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
isWorkflowRelated,
},
{
id: 'notes',
Expand All @@ -73,14 +77,13 @@ export const useRecordShowContainerTabs = (
hide:
targetObjectNameSingular === CoreObjectNameSingular.Note ||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
isWorkflow ||
isWorkflowVersion,
isWorkflowRelated,
},
{
id: 'files',
title: 'Files',
Icon: IconPaperclip,
hide: isWorkflow || isWorkflowVersion,
hide: isWorkflowRelated,
},
{
id: 'emails',
Expand All @@ -102,9 +105,21 @@ export const useRecordShowContainerTabs = (
},
{
id: 'workflowVersion',
title: 'Workflow Version',
title: 'Flow',
Icon: IconSettings,
hide: !isWorkflowVersion,
},
{
id: 'workflowRunOutput',
title: 'Output',
Icon: IconPrinter,
hide: !isWorkflowRun,
},
{
id: 'workflowRunFlow',
title: 'Flow',
Icon: IconSettings,
hide: !isWorkflowRun,
},
];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer';
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
import { EditorProps, Monaco } from '@monaco-editor/react';
import dotenv from 'dotenv';
import { editor, MarkerSeverity } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { isDefined } from '~/utils/isDefined';

export type File = {
language: string;
content: string;
path: string;
};

type SettingsServerlessFunctionCodeEditorProps = Omit<
EditorProps,
'onChange'
> & {
currentFilePath: string;
files: File[];
onChange: (value: string) => void;
setIsCodeValid: (isCodeValid: boolean) => void;
};

export const SettingsServerlessFunctionCodeEditor = ({
currentFilePath,
files,
onChange,
setIsCodeValid,
height = 450,
options = undefined,
}: SettingsServerlessFunctionCodeEditorProps) => {
const { availablePackages } = useGetAvailablePackages();

const currentFile = files.find((file) => file.path === currentFilePath);
const environmentVariablesFile = files.find((file) => file.path === '.env');

const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => {
if (files.length > 1) {
files.forEach((file) => {
const model = monaco.editor.getModel(monaco.Uri.file(file.path));
if (!isDefined(model)) {
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.file(file.path),
);
}
});

monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
baseUrl: 'file:///src',
paths: {
'src/*': ['file:///src/*'],
},
allowSyntheticDefaultImports: true,
esModuleInterop: true,
noEmit: true,
target: monaco.languages.typescript.ScriptTarget.ESNext,
});

if (isDefined(environmentVariablesFile)) {
const environmentVariables = dotenv.parse(
environmentVariablesFile.content,
);

const environmentDefinition = `
declare namespace NodeJS {
interface ProcessEnv {
${Object.keys(environmentVariables)
.map((key) => `${key}: string;`)
.join('\n')}
}
}
declare const process: {
env: NodeJS.ProcessEnv;
};
`;

monaco.languages.typescript.typescriptDefaults.addExtraLib(
environmentDefinition,
'ts:process-env.d.ts',
);
}

await AutoTypings.create(editor, {
monaco,
preloadPackages: true,
onlySpecifiedPackages: true,
versions: availablePackages,
debounceDuration: 0,
});
}
};

const handleEditorValidation = (markers: editor.IMarker[]) => {
for (const marker of markers) {
if (marker.severity === MarkerSeverity.Error) {
setIsCodeValid?.(false);
return;
}
}
setIsCodeValid?.(true);
};

return (
isDefined(currentFile) &&
isDefined(availablePackages) && (
<SettingsServerlessFunctionCodeEditorContainer>
<CodeEditor
height={height}
value={currentFile.content}
language={currentFile.language}
onMount={handleEditorDidMount}
onChange={onChange}
onValidate={handleEditorValidation}
options={options}
/>
</SettingsServerlessFunctionCodeEditorContainer>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from '@emotion/styled';

const StyledEditorContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top: none;
border-radius: 0 0 ${({ theme }) => theme.border.radius.sm}
${({ theme }) => theme.border.radius.sm};
`;

export const SettingsServerlessFunctionCodeEditorContainer =
StyledEditorContainer;
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import {
File,
SettingsServerlessFunctionCodeEditor,
} from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor';
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { Button } from '@/ui/input/button/components/Button';
import { CodeEditor, File } from '@/ui/input/code-editor/components/CodeEditor';
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
import { Section } from '@/ui/layout/section/components/Section';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import styled from '@emotion/styled';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useRecoilValue } from 'recoil';

const StyledTabList = styled(TabList)`
border-bottom: none;
Expand Down Expand Up @@ -107,7 +110,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
rightNodes={[ResetButton, PublishButton, TestButton]}
/>
{activeTabId && (
<CodeEditor
<SettingsServerlessFunctionCodeEditor
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Section } from '@/ui/layout/section/components/Section';
import { H2Title, IconPlayerPlay } from 'twenty-ui';

import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer';
import { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo';
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
Expand Down Expand Up @@ -78,37 +79,30 @@ export const SettingsServerlessFunctionTestTab = ({
/>,
]}
/>
<CodeEditor
files={[
{
content: settingsServerlessFunctionInput,
language: 'json',
path: 'input.json',
},
]}
currentFilePath={'input.json'}
height={200}
onChange={setSettingsServerlessFunctionInput}
/>
<SettingsServerlessFunctionCodeEditorContainer>
<CodeEditor
value={settingsServerlessFunctionInput}
language="json"
height={200}
onChange={setSettingsServerlessFunctionInput}
/>
</SettingsServerlessFunctionCodeEditorContainer>
</div>
<div>
<CoreEditorHeader
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
rightNodes={[<LightCopyIconButton copyText={result} />]}
/>
<CodeEditor
files={[
{
content: result,
language:
settingsServerlessFunctionCodeEditorOutputParams.language,
path: 'result.any',
},
]}
currentFilePath={'result.any'}
height={settingsServerlessFunctionCodeEditorOutputParams.height}
options={{ readOnly: true, domReadOnly: true }}
/>
<SettingsServerlessFunctionCodeEditorContainer>
<CodeEditor
value={result}
language={
settingsServerlessFunctionCodeEditorOutputParams.language
}
height={settingsServerlessFunctionCodeEditorOutputParams.height}
options={{ readOnly: true, domReadOnly: true }}
/>
</SettingsServerlessFunctionCodeEditorContainer>
</div>
</StyledInputsContainer>
</Section>
Expand Down
Loading

0 comments on commit e7eeb3b

Please sign in to comment.