Add Workflow Run show page (#7719)
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
This commit is contained in:
committed by
GitHub
parent
b914182b78
commit
e7eeb3b820
@ -8,6 +8,7 @@ import {
|
|||||||
IconMail,
|
IconMail,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
|
IconPrinter,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTimelineEvent,
|
IconTimelineEvent,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
@ -26,6 +27,10 @@ export const useRecordShowContainerTabs = (
|
|||||||
const isWorkflowVersion =
|
const isWorkflowVersion =
|
||||||
isWorkflowEnabled &&
|
isWorkflowEnabled &&
|
||||||
targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion;
|
targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion;
|
||||||
|
const isWorkflowRun =
|
||||||
|
isWorkflowEnabled &&
|
||||||
|
targetObjectNameSingular === CoreObjectNameSingular.WorkflowRun;
|
||||||
|
const isWorkflowRelated = isWorkflow || isWorkflowVersion || isWorkflowRun;
|
||||||
|
|
||||||
const isCompanyOrPerson = [
|
const isCompanyOrPerson = [
|
||||||
CoreObjectNameSingular.Company,
|
CoreObjectNameSingular.Company,
|
||||||
@ -54,7 +59,7 @@ export const useRecordShowContainerTabs = (
|
|||||||
id: 'timeline',
|
id: 'timeline',
|
||||||
title: 'Timeline',
|
title: 'Timeline',
|
||||||
Icon: IconTimelineEvent,
|
Icon: IconTimelineEvent,
|
||||||
hide: isInRightDrawer || isWorkflow || isWorkflowVersion,
|
hide: isInRightDrawer || isWorkflowRelated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
@ -63,8 +68,7 @@ export const useRecordShowContainerTabs = (
|
|||||||
hide:
|
hide:
|
||||||
targetObjectNameSingular === CoreObjectNameSingular.Note ||
|
targetObjectNameSingular === CoreObjectNameSingular.Note ||
|
||||||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
|
targetObjectNameSingular === CoreObjectNameSingular.Task ||
|
||||||
isWorkflow ||
|
isWorkflowRelated,
|
||||||
isWorkflowVersion,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
@ -73,14 +77,13 @@ export const useRecordShowContainerTabs = (
|
|||||||
hide:
|
hide:
|
||||||
targetObjectNameSingular === CoreObjectNameSingular.Note ||
|
targetObjectNameSingular === CoreObjectNameSingular.Note ||
|
||||||
targetObjectNameSingular === CoreObjectNameSingular.Task ||
|
targetObjectNameSingular === CoreObjectNameSingular.Task ||
|
||||||
isWorkflow ||
|
isWorkflowRelated,
|
||||||
isWorkflowVersion,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
title: 'Files',
|
title: 'Files',
|
||||||
Icon: IconPaperclip,
|
Icon: IconPaperclip,
|
||||||
hide: isWorkflow || isWorkflowVersion,
|
hide: isWorkflowRelated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'emails',
|
id: 'emails',
|
||||||
@ -102,9 +105,21 @@ export const useRecordShowContainerTabs = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'workflowVersion',
|
id: 'workflowVersion',
|
||||||
title: 'Workflow Version',
|
title: 'Flow',
|
||||||
Icon: IconSettings,
|
Icon: IconSettings,
|
||||||
hide: !isWorkflowVersion,
|
hide: !isWorkflowVersion,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'workflowRunOutput',
|
||||||
|
title: 'Output',
|
||||||
|
Icon: IconPrinter,
|
||||||
|
hide: !isWorkflowRun,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'workflowRunFlow',
|
||||||
|
title: 'Flow',
|
||||||
|
Icon: IconSettings,
|
||||||
|
hide: !isWorkflowRun,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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;
|
||||||
@ -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 { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
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 { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
|
import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui';
|
||||||
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
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)`
|
const StyledTabList = styled(TabList)`
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -107,7 +110,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
|||||||
rightNodes={[ResetButton, PublishButton, TestButton]}
|
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||||
/>
|
/>
|
||||||
{activeTabId && (
|
{activeTabId && (
|
||||||
<CodeEditor
|
<SettingsServerlessFunctionCodeEditor
|
||||||
files={files}
|
files={files}
|
||||||
currentFilePath={activeTabId}
|
currentFilePath={activeTabId}
|
||||||
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
|
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Section } from '@/ui/layout/section/components/Section';
|
|||||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||||
|
|
||||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
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 { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo';
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
@ -78,37 +79,30 @@ export const SettingsServerlessFunctionTestTab = ({
|
|||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<CodeEditor
|
<SettingsServerlessFunctionCodeEditorContainer>
|
||||||
files={[
|
<CodeEditor
|
||||||
{
|
value={settingsServerlessFunctionInput}
|
||||||
content: settingsServerlessFunctionInput,
|
language="json"
|
||||||
language: 'json',
|
height={200}
|
||||||
path: 'input.json',
|
onChange={setSettingsServerlessFunctionInput}
|
||||||
},
|
/>
|
||||||
]}
|
</SettingsServerlessFunctionCodeEditorContainer>
|
||||||
currentFilePath={'input.json'}
|
|
||||||
height={200}
|
|
||||||
onChange={setSettingsServerlessFunctionInput}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CoreEditorHeader
|
<CoreEditorHeader
|
||||||
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]}
|
||||||
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
rightNodes={[<LightCopyIconButton copyText={result} />]}
|
||||||
/>
|
/>
|
||||||
<CodeEditor
|
<SettingsServerlessFunctionCodeEditorContainer>
|
||||||
files={[
|
<CodeEditor
|
||||||
{
|
value={result}
|
||||||
content: result,
|
language={
|
||||||
language:
|
settingsServerlessFunctionCodeEditorOutputParams.language
|
||||||
settingsServerlessFunctionCodeEditorOutputParams.language,
|
}
|
||||||
path: 'result.any',
|
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||||
},
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
]}
|
/>
|
||||||
currentFilePath={'result.any'}
|
</SettingsServerlessFunctionCodeEditorContainer>
|
||||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</StyledInputsContainer>
|
</StyledInputsContainer>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,148 +1,51 @@
|
|||||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
|
||||||
import { codeEditorTheme } from '@/ui/input/code-editor/utils/codeEditorTheme';
|
import { codeEditorTheme } from '@/ui/input/code-editor/utils/codeEditorTheme';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import Editor, { EditorProps } from '@monaco-editor/react';
|
||||||
import Editor, { EditorProps, Monaco } from '@monaco-editor/react';
|
import { isDefined } from 'twenty-ui';
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import { MarkerSeverity, editor } from 'monaco-editor';
|
|
||||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
const StyledEditor = styled(Editor)`
|
|
||||||
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 type File = {
|
|
||||||
language: string;
|
|
||||||
content: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||||
currentFilePath: string;
|
|
||||||
files: File[];
|
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
setIsCodeValid?: (isCodeValid: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CodeEditor = ({
|
export const CodeEditor = ({
|
||||||
currentFilePath,
|
value,
|
||||||
files,
|
language,
|
||||||
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
setIsCodeValid,
|
onValidate,
|
||||||
height = 450,
|
height = 450,
|
||||||
options = undefined,
|
options,
|
||||||
}: CodeEditorProps) => {
|
}: CodeEditorProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
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,
|
|
||||||
) => {
|
|
||||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
|
||||||
monaco.editor.setTheme('codeEditorTheme');
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
isDefined(currentFile) &&
|
<Editor
|
||||||
isDefined(availablePackages) && (
|
height={height}
|
||||||
<StyledEditor
|
value={value}
|
||||||
height={height}
|
language={language}
|
||||||
value={currentFile.content}
|
onMount={(editor, monaco) => {
|
||||||
language={currentFile.language}
|
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||||
onMount={handleEditorDidMount}
|
monaco.editor.setTheme('codeEditorTheme');
|
||||||
onChange={(value?: string) => value && onChange?.(value)}
|
|
||||||
onValidate={handleEditorValidation}
|
onMount?.(editor, monaco);
|
||||||
options={{
|
}}
|
||||||
...options,
|
onChange={(value) => {
|
||||||
overviewRulerLanes: 0,
|
if (isDefined(value)) {
|
||||||
scrollbar: {
|
onChange?.(value);
|
||||||
vertical: 'hidden',
|
}
|
||||||
horizontal: 'hidden',
|
}}
|
||||||
},
|
onValidate={onValidate}
|
||||||
minimap: {
|
options={{
|
||||||
enabled: false,
|
overviewRulerLanes: 0,
|
||||||
},
|
scrollbar: {
|
||||||
}}
|
vertical: 'hidden',
|
||||||
/>
|
horizontal: 'hidden',
|
||||||
)
|
},
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,6 +18,8 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage
|
|||||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
|
import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer';
|
||||||
|
import { WorkflowRunVersionVisualizer } from '@/workflow/components/WorkflowRunVersionVisualizer';
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
||||||
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
|
import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer';
|
||||||
@ -182,6 +184,14 @@ export const ShowPageSubContainer = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
case 'workflowRunFlow':
|
||||||
|
return (
|
||||||
|
<WorkflowRunVersionVisualizer workflowRunId={targetableObject.id} />
|
||||||
|
);
|
||||||
|
case 'workflowRunOutput':
|
||||||
|
return (
|
||||||
|
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledSourceCodeContainer = styled.div`
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
margin: ${({ theme }) => theme.spacing(4)};
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowRunOutputVisualizer = ({
|
||||||
|
workflowRunId,
|
||||||
|
}: {
|
||||||
|
workflowRunId: string;
|
||||||
|
}) => {
|
||||||
|
const workflowRun = useWorkflowRun({ workflowRunId });
|
||||||
|
if (!isDefined(workflowRun)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSourceCodeContainer>
|
||||||
|
<CodeEditor
|
||||||
|
value={JSON.stringify(workflowRun.output, null, 2)}
|
||||||
|
language="json"
|
||||||
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
|
/>
|
||||||
|
</StyledSourceCodeContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer';
|
||||||
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect';
|
||||||
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const WorkflowRunVersionVisualizer = ({
|
||||||
|
workflowRunId,
|
||||||
|
}: {
|
||||||
|
workflowRunId: string;
|
||||||
|
}) => {
|
||||||
|
const workflowRun = useWorkflowRun({
|
||||||
|
workflowRunId,
|
||||||
|
});
|
||||||
|
if (!isDefined(workflowRun)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkflowVersionVisualizerEffect
|
||||||
|
workflowVersionId={workflowRun.workflowVersionId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WorkflowVersionVisualizer
|
||||||
|
workflowVersionId={workflowRun.workflowVersionId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
|
||||||
|
export const useWorkflowRun = ({
|
||||||
|
workflowRunId,
|
||||||
|
}: {
|
||||||
|
workflowRunId: string;
|
||||||
|
}) => {
|
||||||
|
const { record } = useFindOneRecord<WorkflowRun>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
|
||||||
|
objectRecordId: workflowRunId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return record;
|
||||||
|
};
|
||||||
@ -84,6 +84,28 @@ export type WorkflowVersion = {
|
|||||||
__typename: 'WorkflowVersion';
|
__typename: 'WorkflowVersion';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StepRunOutput = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
outputs: {
|
||||||
|
attemptCount: number;
|
||||||
|
result: object | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowRunOutput = {
|
||||||
|
steps: Record<string, StepRunOutput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowRun = {
|
||||||
|
__typename: 'WorkflowRun';
|
||||||
|
id: string;
|
||||||
|
workflowVersionId: string;
|
||||||
|
output: WorkflowRunOutput;
|
||||||
|
};
|
||||||
|
|
||||||
export type Workflow = {
|
export type Workflow = {
|
||||||
__typename: 'Workflow';
|
__typename: 'Workflow';
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -215,6 +215,7 @@ export {
|
|||||||
IconTimelineEvent,
|
IconTimelineEvent,
|
||||||
IconTool,
|
IconTool,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
|
IconPrinter,
|
||||||
IconUnlink,
|
IconUnlink,
|
||||||
IconUpload,
|
IconUpload,
|
||||||
IconUser,
|
IconUser,
|
||||||
|
|||||||
Reference in New Issue
Block a user