diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index 1d029f4877f8..a1d6a81d2e2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -8,6 +8,7 @@ import { IconMail, IconNotes, IconPaperclip, + IconPrinter, IconSettings, IconTimelineEvent, } from 'twenty-ui'; @@ -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, @@ -54,7 +59,7 @@ export const useRecordShowContainerTabs = ( id: 'timeline', title: 'Timeline', Icon: IconTimelineEvent, - hide: isInRightDrawer || isWorkflow || isWorkflowVersion, + hide: isInRightDrawer || isWorkflowRelated, }, { id: 'tasks', @@ -63,8 +68,7 @@ export const useRecordShowContainerTabs = ( hide: targetObjectNameSingular === CoreObjectNameSingular.Note || targetObjectNameSingular === CoreObjectNameSingular.Task || - isWorkflow || - isWorkflowVersion, + isWorkflowRelated, }, { id: 'notes', @@ -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', @@ -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, + }, ]; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx new file mode 100644 index 000000000000..7641d8f46d38 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx @@ -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) && ( + + + + ) + ); +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx new file mode 100644 index 000000000000..4ad8afaee743 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx index 5f8886871359..c1131c1b66f5 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx @@ -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; @@ -107,7 +110,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ rightNodes={[ResetButton, PublishButton, TestButton]} /> {activeTabId && ( - onChange(activeTabId, newCodeValue)} diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx index b2d54cbc03f9..54a565215d4b 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx @@ -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'; @@ -78,37 +79,30 @@ export const SettingsServerlessFunctionTestTab = ({ />, ]} /> - + + +
]} rightNodes={[]} /> - + + +
diff --git a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx index 723b04a9f69b..dc846b9c0834 100644 --- a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx @@ -1,148 +1,51 @@ -import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { codeEditorTheme } from '@/ui/input/code-editor/utils/codeEditorTheme'; import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; -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; -}; +import Editor, { EditorProps } from '@monaco-editor/react'; +import { isDefined } from 'twenty-ui'; type CodeEditorProps = Omit & { - currentFilePath: string; - files: File[]; onChange?: (value: string) => void; - setIsCodeValid?: (isCodeValid: boolean) => void; }; export const CodeEditor = ({ - currentFilePath, - files, + value, + language, + onMount, onChange, - setIsCodeValid, + onValidate, height = 450, - options = undefined, + options, }: CodeEditorProps) => { 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), - ); + return ( + { + monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); + monaco.editor.setTheme('codeEditorTheme'); + + onMount?.(editor, monaco); + }} + onChange={(value) => { + if (isDefined(value)) { + onChange?.(value); } - }); - - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), - moduleResolution: - monaco.languages.typescript.ModuleResolutionKind.NodeJs, - baseUrl: 'file:///src', - paths: { - 'src/*': ['file:///src/*'], + }} + onValidate={onValidate} + options={{ + overviewRulerLanes: 0, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', }, - 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) && ( - value && onChange?.(value)} - onValidate={handleEditorValidation} - options={{ - ...options, - overviewRulerLanes: 0, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - }, - minimap: { - enabled: false, - }, - }} - /> - ) + minimap: { + enabled: false, + }, + ...options, + }} + /> ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index 3365a170e2a6..6f2c0aa442c9 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -18,6 +18,8 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; 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 { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect'; import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer'; @@ -182,6 +184,14 @@ export const ShowPageSubContainer = ({ /> ); + case 'workflowRunFlow': + return ( + + ); + case 'workflowRunOutput': + return ( + + ); default: return <>; } diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx new file mode 100644 index 000000000000..1a49c030ac60 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx new file mode 100644 index 000000000000..8d8f265c426c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx @@ -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 ( + <> + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx new file mode 100644 index 000000000000..9bb6fa5642ed --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx @@ -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({ + objectNameSingular: CoreObjectNameSingular.WorkflowRun, + objectRecordId: workflowRunId, + }); + + return record; +}; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 65b2e9a25a15..70e3ab197020 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -84,6 +84,28 @@ export type 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; +}; + +export type WorkflowRun = { + __typename: 'WorkflowRun'; + id: string; + workflowVersionId: string; + output: WorkflowRunOutput; +}; + export type Workflow = { __typename: 'Workflow'; id: string; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index ffef2fdeda79..5244849f69b1 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -215,6 +215,7 @@ export { IconTimelineEvent, IconTool, IconTrash, + IconPrinter, IconUnlink, IconUpload, IconUser,