From 54318992b99099cefbe9757a48c75dfbeee69eec Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 16 Jul 2026 19:48:34 +0200 Subject: [PATCH 01/13] feat: update UserEditDialogComponent with new icons and terminology adjustments --- .../components/UserEditDialogComponent.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/packages/ce/src/user/components/UserEditDialogComponent.tsx b/src/packages/ce/src/user/components/UserEditDialogComponent.tsx index 659f0f51..57c1ff87 100644 --- a/src/packages/ce/src/user/components/UserEditDialogComponent.tsx +++ b/src/packages/ce/src/user/components/UserEditDialogComponent.tsx @@ -31,7 +31,7 @@ import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/comp import {User, UsersUpdateInput} from "@code0-tech/sagittarius-graphql-types"; import {UserService} from "@edition/user/services/User.service"; import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; -import {IconAt, IconLock, IconMail, IconUser} from "@tabler/icons-react"; +import {IconAt, IconBackground, IconLock, IconMail, IconSettings2, IconShieldLock} from "@tabler/icons-react"; import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; import {motion} from "framer-motion"; @@ -172,31 +172,35 @@ export const UserEditDialogComponent: React.FC = ( Settings of @{user?.username ?? ""} - + Edit the general settings, permissions and security for user @{user?.username ?? ""} - + - - + + }> +
+ + {endpoint} + +
+ + + + + )} +
+ + {triggerSchema && triggerSchema.input !== "generic" && ( + <> + validate("input")} + /> + + + )} + + + + + + + +} From 2cac29a74c19dea7cd0f47c30a861db876871cea Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 17 Jul 2026 18:22:52 +0200 Subject: [PATCH 10/13] feat: enhance FlowExecutionResultView with pending execution indicators and loading animations --- .../flow/views/FlowExecutionResultView.tsx | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx b/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx index 9d0c5bc9..d374cb32 100644 --- a/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx +++ b/src/packages/ce/src/flow/views/FlowExecutionResultView.tsx @@ -57,6 +57,11 @@ import Link from "next/link"; import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; import {icon} from "@core/util/icons"; import {formatDistanceToNow} from "date-fns"; +import {useFlowExecutionStore} from "@edition/flow/hooks/Flow.execution.hook"; +import {IconLoader2} from "@tabler/icons-react"; +import {motion} from "framer-motion"; +import {LineWobble} from 'ldrs/react' +import 'ldrs/react/LineWobble.css' export interface NodeGanttItem extends GanttItem { data?: { @@ -155,6 +160,33 @@ export const FlowExecutionResultView: React.FC = () => { [flow?.executionResults?.nodes] ) + const executions = useFlowExecutionStore(s => s.executions) + const pendingExecutions = React.useMemo( + () => executions.filter(execution => execution.flowId === flowId), + [executions, flowId] + ) + + // When a manual execution is triggered elsewhere (the definition panel) its pending + // entry appears here; focus its loading tab so the user sees the spinner immediately. + const previousPendingRef = React.useRef([]) + React.useEffect(() => { + const currentIds = pendingExecutions.map(execution => execution.executionIdentifier) + const newlyAdded = currentIds.find(id => !previousPendingRef.current.includes(id)) + if (newlyAdded) setActiveTab(newlyAdded) + previousPendingRef.current = currentIds + }, [pendingExecutions]) + + // Once the pending execution resolves its loading tab disappears; fall back to the + // newest result so the freshly arrived execution result becomes visible. + React.useEffect(() => { + if (!activeTab) return + const stillPending = pendingExecutions.some(execution => execution.executionIdentifier === activeTab) + const isResult = flowExecutionResults.some(result => result?.id === activeTab) + if (!stillPending && !isResult) { + setActiveTab(flowExecutionResults[0]?.id ?? undefined) + } + }, [pendingExecutions, flowExecutionResults, activeTab]) + const ganttItems = React.useMemo>( () => { return new Map(flowExecutionResults.map(result => [result?.id, [ @@ -256,6 +288,21 @@ export const FlowExecutionResultView: React.FC = () => { ) : null } > + {pendingExecutions.map(execution => ( + + + + + + + Executing... + + + + ))} {Array.from(ganttItems)?.map(([id, items]) => { const execution = flowExecutionResults.find(execution => execution?.id === id) @@ -275,6 +322,34 @@ export const FlowExecutionResultView: React.FC = () => { })} }> <> + {pendingExecutions.map(execution => ( + + + + + + Executing flow + + + + We are running your flow. This may take a few moments to load every data object. + + + + ))} { ganttItems.size > 0 ? Array.from(ganttItems)?.map(([id, items]) => { return { }} - }) : ( + }) : pendingExecutions.length > 0 ? null : ( Date: Fri, 17 Jul 2026 18:23:00 +0200 Subject: [PATCH 11/13] feat: add FlowExecutionSubscriberComponent and FlowExecutionWatcherComponent for managing execution results --- .../FlowExecutionSubscriberComponent.tsx | 34 +++++++++++++++++++ .../FlowExecutionWatcherComponent.tsx | 23 +++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/packages/ce/src/flow/components/FlowExecutionSubscriberComponent.tsx create mode 100644 src/packages/ce/src/flow/components/FlowExecutionWatcherComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowExecutionSubscriberComponent.tsx b/src/packages/ce/src/flow/components/FlowExecutionSubscriberComponent.tsx new file mode 100644 index 00000000..f323e8fb --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowExecutionSubscriberComponent.tsx @@ -0,0 +1,34 @@ +"use client" + +import React from "react" +import {useSubscription} from "@apollo/client/react" +import {ExecutionResult, Subscription} from "@code0-tech/sagittarius-graphql-types" +import {FlowExecution, useFlowExecutionStore} from "@edition/flow/hooks/Flow.execution.hook" +import {useService} from "@code0-tech/pictor" +import {FlowService} from "@edition/flow/services/Flow.service" +import flowExecutionResultSubscription from "@edition/flow/services/subscriptions/Flow.executionResult.subscription.graphql" + +export interface FlowExecutionSubscriberComponentProps { + execution: FlowExecution +} + +export const FlowExecutionSubscriberComponent: React.FC = ({execution}) => { + + const flowService = useService(FlowService) + const removeExecution = useFlowExecutionStore(s => s.removeExecution) + + useSubscription(flowExecutionResultSubscription, { + variables: {executionIdentifier: execution.executionIdentifier}, + onData: (data) => { + const executionResult = data.data.data?.namespacesProjectsFlowsExecutionResult?.executionResult as ExecutionResult | undefined + if (executionResult) { + flowService.addExecutionResult(execution.flowId, executionResult) + removeExecution(execution.executionIdentifier) + } + }, + onComplete: () => removeExecution(execution.executionIdentifier), + onError: () => removeExecution(execution.executionIdentifier), + }) + + return null +} diff --git a/src/packages/ce/src/flow/components/FlowExecutionWatcherComponent.tsx b/src/packages/ce/src/flow/components/FlowExecutionWatcherComponent.tsx new file mode 100644 index 00000000..2e89e881 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowExecutionWatcherComponent.tsx @@ -0,0 +1,23 @@ +"use client" + +import React from "react" +import {useFlowExecutionStore} from "@edition/flow/hooks/Flow.execution.hook" +import {FlowExecutionSubscriberComponent} from "@edition/flow/components/FlowExecutionSubscriberComponent" + +/** + * Keeps one subscription alive per pending manual execution. Each subscriber writes + * the incoming execution result into the flow store and drops the pending entry. + */ +export const FlowExecutionWatcherComponent: React.FC = () => { + + const executions = useFlowExecutionStore(s => s.executions) + + return <> + {executions.map(execution => ( + + ))} + +} From 220863d4e79cae178fe2494b1e16abf85590ccdd Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 17 Jul 2026 18:23:10 +0200 Subject: [PATCH 12/13] feat: integrate FlowExecutionWatcherComponent and refactor tab management with Zustand store --- .../project/[projectId]/flow/[flowId]/page.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index 0b05d6fd..ef7f3e7d 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -14,6 +14,8 @@ import { } from "@code0-tech/pictor/dist/components/resizable/Resizable"; import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; import {FlowExecutionResultView} from "@edition/flow/views/FlowExecutionResultView"; +import {FlowExecutionWatcherComponent} from "@edition/flow/components/FlowExecutionWatcherComponent"; +import {useFlowViewStore} from "@edition/flow/hooks/Flow.view.hook"; import {useHotkeys} from "react-hotkeys-hook"; import {Node, useReactFlow} from "@xyflow/react"; @@ -28,17 +30,18 @@ export default function Page() { const flowIndex = params.flowId as any as number const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` - const [tab, setTab] = React.useState(undefined); + const tab = useFlowViewStore(s => s.tab) + const toggleTab = useFlowViewStore(s => s.toggleTab) const reactFlow = useReactFlow() useHotkeys('shift+1', (keyboardEvent) => { - setTab(prevState => prevState === "file" ? undefined : "file") + toggleTab("file") keyboardEvent.stopPropagation() keyboardEvent.preventDefault() }, []) useHotkeys('shift+2', (keyboardEvent) => { - setTab(prevState => prevState === "execution" ? undefined : "execution") + toggleTab("execution") keyboardEvent.stopPropagation() keyboardEvent.preventDefault() }, []) @@ -57,15 +60,16 @@ export default function Page() { }, [tab, reactFlow]) return + + + + diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx deleted file mode 100644 index c18fe4c5..00000000 --- a/src/packages/ce/src/flow/components/panels/FlowPanelDefinitionComponent.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from "react"; -import {Panel} from "@xyflow/react"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import { - Button, - Dialog, - DialogContent, - DialogOverlay, - DialogPortal, - DialogTrigger, - Spacing, - Text, useService, useStore -} from "@code0-tech/pictor"; -import {InputWrapper} from "@code0-tech/pictor/dist/components/form/InputWrapper"; -import {IconCheck, IconCopy, IconPlayerPlay} from "@tabler/icons-react"; -import {useParams} from "next/navigation"; -import {FlowService} from "@edition/flow/services/Flow.service"; -import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {ModuleService} from "@edition/module/services/Module.service"; -import {useCopyToClipboard} from "@uidotdev/usehooks"; -import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; - -export const FlowPanelDefinitionComponent: React.FC = () => { - - const params = useParams() - const flowService = useService(FlowService) - const flowStore = useStore(FlowService) - const flowTypeService = useService(FlowTypeService) - const flowTypeStore = useStore(FlowTypeService) - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) - const moduleService = useService(ModuleService) - const moduleStore = useStore(ModuleService) - - const [copiedText, copyToClipboard] = useCopyToClipboard(); - const hasCopiedText = Boolean(copiedText); - - const namespaceIndex = params.namespaceId as any as number - const projectIndex = params.projectId as any as number - const flowIndex = params.flowId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` - const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` - - const flow = React.useMemo( - () => flowService.getById(flowId, { - namespaceId, - projectId - }), - [flowId, flowStore, namespaceId, projectId] - ) - - const project = React.useMemo( - () => projectService.getById(projectId, { - namespaceId - }), - [projectId, namespaceId, projectStore] - ) - - const flowType = React.useMemo( - () => flowTypeService.getById(flow?.type?.id, { - namespaceId, - projectId, - runtimeId: project?.primaryRuntime?.id - }), - [flow?.type?.id, namespaceId, projectId, project?.primaryRuntime?.id, flowTypeStore] - ) - - const module = React.useMemo( - () => moduleService.getById(flowType?.runtimeFlowType?.runtimeModule?.id, { - namespaceId: namespaceId, - projectId: projectId, - runtimeId: project?.primaryRuntime?.id - }), - [flowType?.runtimeFlowType?.runtimeModule?.id, namespaceId, projectId, project?.primaryRuntime?.id, moduleStore] - ) - - let endpoint = `http://${module?.definitions?.nodes?.[0]?.host}:${module?.definitions?.nodes?.[0]?.port}${module?.definitions?.nodes?.[0]?.endpoint}` - .replace("${{project_slug}}", project?.slug ?? "${{project_slug}}") - - flow?.settings?.nodes?.forEach(setting => { - endpoint = endpoint.replace(`\${{${setting?.flowSettingIdentifier}}}`, setting?.value) - }) - - const copyEndpoint = (event: React.MouseEvent) => { - if (!navigator?.clipboard?.writeText) { - // Without a secure context there is no Clipboard API and the hook falls back to a - // textarea on document.body, which the modal dialog's focus trap keeps unfocusable, - // so Firefox copies nothing. Run the same fallback inside the dialog instead; the - // copyToClipboard call below still records the copied state. - const dialog = event.currentTarget.closest("[role='dialog']") - if (dialog) { - const textArea = document.createElement("textarea") - textArea.value = endpoint - textArea.style.position = "fixed" - textArea.style.opacity = "0" - dialog.appendChild(textArea) - textArea.select() - document.execCommand("copy") - dialog.removeChild(textArea) - } - } - copyToClipboard(endpoint) - } - - return module?.definitions?.nodes?.[0] && - - - - - - - - - - - setting?.flowSettingIdentifier === "httpMethod")?.value ? ( - - {flow?.settings?.nodes?.find(setting => setting?.flowSettingIdentifier === "httpMethod")?.value} - - ) : undefined} - right={ - - - - }> -
- - {endpoint} - -
- -
-
-
-
-
-
- -} \ No newline at end of file