diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 09f063c9..0b0650ad 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -21,6 +21,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", + "remove-markdown": "^0.5.0", "wouter": "^3.7.1" }, "devDependencies": { @@ -7396,6 +7397,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-markdown": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.5.tgz", + "integrity": "sha512-lMR8tOtDqazFT6W2bZidoXwkptMdF3pCxpri0AEokHg0sZlC2GdoLqnoaxsEj1o7/BtXV1MKtT3YviA1t7rW7g==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index 01281e05..f125fcd0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -27,6 +27,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", + "remove-markdown": "^0.5.0", "wouter": "^3.7.1" }, "devDependencies": { diff --git a/desktop/src-tauri/src/commands/executable.rs b/desktop/src-tauri/src/commands/executable.rs index be6e1697..9976d96f 100644 --- a/desktop/src-tauri/src/commands/executable.rs +++ b/desktop/src-tauri/src/commands/executable.rs @@ -25,18 +25,47 @@ impl ExecutableCommands { &self, workspace: Option<&str>, namespace: Option<&str>, + tags: Option<&[&str]>, + verb: Option<&str>, + filter: Option<&str>, ) -> CommandResult> { let mut args = vec!["browse", "--list"]; if let Some(ws) = workspace { - args.extend_from_slice(&["--workspace", ws]); + if !ws.is_empty() { + args.extend_from_slice(&["--workspace", ws]); + } } if let Some(ns) = namespace { - args.extend_from_slice(&["--namespace", ns]); + if !ns.is_empty() { + args.extend_from_slice(&["--namespace", ns]); + } else { + args.push("--all"); + } } else { args.push("--all"); } + + if let Some(tags) = tags { + for tag in tags { + if !tag.is_empty() { + args.extend_from_slice(&["--tag", tag]); + } + } + } + + if let Some(verb) = verb { + if !verb.is_empty() { + args.extend_from_slice(&["--verb", verb]); + } + } + + if let Some(filter) = filter { + if !filter.is_empty() { + args.extend_from_slice(&["--filter", filter]); + } + } let response: ExecutableResponse = self.executor.execute_json(&args).await?; Ok(response.executables) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 2983439f..a1a5835c 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -105,11 +105,26 @@ async fn get_executable(executable_ref: String) -> Result, namespace: Option, + tags: Option>, + verb: Option, + filter: Option, ) -> Result, String> { let runner = cli::cli_executor(); + + // Convert owned Strings into borrowed &str slices expected by the command layer + let tags_ref_vec: Option> = tags + .as_ref() + .map(|v| v.iter().map(|s| s.as_str()).collect()); + runner .executable - .list(workspace.as_deref(), namespace.as_deref()) + .list( + workspace.as_deref(), + namespace.as_deref(), + tags_ref_vec.as_deref(), + verb.as_deref(), + filter.as_deref(), + ) .await .map_err(|e| e.to_string()) } diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index ae73fd80..7f131b90 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -1,13 +1,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Route, Switch } from "wouter"; import "./App.css"; -import { AppProvider } from "./hooks/useAppContext.tsx"; +import { AppProvider } from "./hooks/useAppContext"; import { NotifierProvider } from "./hooks/useNotifier"; import { AppShell } from "./layout"; -import { PageWrapper } from "./components/PageWrapper.tsx"; -import {Settings, Welcome, Data, Workspaces} from "./pages"; -import { Workspace } from "./pages/Workspace/Workspace.tsx"; -import { ExecutableRoute } from "./pages/Executable/ExecutableRoute.tsx"; +import { PageWrapper } from "./components/PageWrapper"; +import {Settings, Welcome, Data, Workspaces, Executables, Executable} from "./pages"; +import { Workspace } from "./pages/Workspace/Workspace"; import { Text } from "@mantine/core"; const queryClient = new QueryClient({ @@ -35,13 +34,17 @@ function App() { path="/workspaces" component={Workspaces} /> + diff --git a/desktop/src/hooks/useAppContext.tsx b/desktop/src/hooks/useAppContext.tsx index e50e7552..547d7a1d 100644 --- a/desktop/src/hooks/useAppContext.tsx +++ b/desktop/src/hooks/useAppContext.tsx @@ -2,10 +2,8 @@ import React from "react"; import { createContext, useContext, useState, useEffect } from "react"; import { Config } from "../types/generated/config"; import { EnrichedWorkspace } from "../types/workspace"; -import { EnrichedExecutable } from "../types/executable"; import { useConfig } from "./useConfig"; import { useWorkspaces } from "./useWorkspace"; -import { useExecutables } from "./useExecutable"; import { invoke } from "@tauri-apps/api/core"; import { useQuery } from "@tanstack/react-query"; @@ -14,7 +12,6 @@ interface AppContextType { selectedWorkspace: string | null; setSelectedWorkspace: (workspaceName: string | null) => void; workspaces: EnrichedWorkspace[]; - executables: EnrichedExecutable[]; isLoading: boolean; hasError: Error | null; refreshAll: () => void; @@ -78,16 +75,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) { } }, [config, workspaces, selectedWorkspace]); - const { - executables, - isExecutablesLoading, - executablesError, - refreshExecutables, - } = useExecutables(selectedWorkspace, enabled); - const isLoading = - isConfigLoading || isWorkspacesLoading || isExecutablesLoading; - const hasError = configError || workspacesError || executablesError; + isConfigLoading || isWorkspacesLoading; + const hasError = configError || workspacesError; if (hasError) { console.error("Error", hasError); } @@ -95,7 +85,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) { const refreshAll = () => { refreshConfig(); refreshWorkspaces(); - refreshExecutables(); }; // If flow binary is not available, return early with error state @@ -107,7 +96,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) { workspaces: [], selectedWorkspace, setSelectedWorkspace, - executables: [], isLoading: false, hasError: binaryCheckError, refreshAll: () => {}, @@ -127,7 +115,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) { workspaces: [], selectedWorkspace, setSelectedWorkspace, - executables: [], isLoading: true, hasError: null, refreshAll: () => {}, @@ -145,7 +132,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) { workspaces: workspaces || [], selectedWorkspace, setSelectedWorkspace, - executables, isLoading, hasError, refreshAll, diff --git a/desktop/src/hooks/useExecutable.ts b/desktop/src/hooks/useExecutable.ts index 22fd169d..807a8406 100644 --- a/desktop/src/hooks/useExecutable.ts +++ b/desktop/src/hooks/useExecutable.ts @@ -47,32 +47,50 @@ export function useExecutable(executableRef: string) { } export function useExecutables( - selectedWorkspace: string | null, - enabled: boolean = true, + workspace: string | null, + namespace: string | null, + tags: string[] | null, + verb: string | null, + filter: string | null, ) { const queryClient = useQueryClient(); + const normalizedParams = React.useMemo(() => { + return { + workspace: workspace || undefined, + namespace: namespace || undefined, + tags: tags && tags.length > 0 ? tags : undefined, + verb: verb || undefined, + filter: filter || undefined, + }; + }, [workspace, namespace, tags, verb, filter]); + + const queryKey = React.useMemo(() => { + return ["executables", normalizedParams]; + }, [normalizedParams]); + const { data: executables, isLoading: isExecutablesLoading, error: executablesError, } = useQuery({ - queryKey: ["executables", selectedWorkspace], + queryKey, queryFn: async () => { - if (!selectedWorkspace) return []; + console.log('Fetching executables with params:', normalizedParams); return await invoke("list_executables", { - workspace: selectedWorkspace, + workspace: normalizedParams.workspace, + namespace: normalizedParams.namespace, + tags: normalizedParams.tags, + verb: normalizedParams.verb, + filter: normalizedParams.filter, }); }, - enabled: enabled && !!selectedWorkspace, // Only run when workspace is selected AND enabled }); const refreshExecutables = () => { - if (selectedWorkspace) { - queryClient.invalidateQueries({ - queryKey: ["executables", selectedWorkspace], - }); - } + void queryClient.invalidateQueries({ + queryKey, + }); }; return { @@ -81,4 +99,4 @@ export function useExecutables( executablesError, refreshExecutables, }; -} +} \ No newline at end of file diff --git a/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx b/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx deleted file mode 100644 index 506c0c1d..00000000 --- a/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { - Group, - RenderTreeNodePayload, - ScrollArea, - Text, - Tree, - TreeNodeData, - useTree, -} from "@mantine/core"; -import { - IconBlocks, - IconCircleCheckFilled, - IconCirclePlus, - IconFolder, - IconFolderOpen, - IconOctagon, - IconPlayerPlayFilled, - IconProgressDown, - IconProgressX, - IconRefresh, - IconReload, - IconSettingsAutomation, - IconWindowMaximize, -} from "@tabler/icons-react"; -import { useMemo, useCallback } from "react"; -import { - BuildVerbType, - ConfigurationVerbType, - CreationVerbType, - DeactivationVerbType, - DestructionVerbType, - EnrichedExecutable, - GetUIVerbType, - LaunchVerbType, - RestartVerbType, - RetrievalVerbType, - UpdateVerbType, - ValidationVerbType, -} from "../../../types/executable"; -import { useAppContext } from "../../../hooks/useAppContext.tsx"; -import { useLocation } from "wouter"; - -interface CustomTreeNodeData extends TreeNodeData { - isNamespace: boolean; - verbType: string | null; -} - -function getTreeData(executables: EnrichedExecutable[]): CustomTreeNodeData[] { - const execsByNamespace: Record = {}; - const rootExecutables: EnrichedExecutable[] = []; - - // Separate executables into namespaced and root level - for (const executable of executables) { - if (executable.namespace) { - if (!execsByNamespace[executable.namespace]) { - execsByNamespace[executable.namespace] = []; - } - execsByNamespace[executable.namespace].push(executable); - } else { - rootExecutables.push(executable); - } - } - - const treeData: CustomTreeNodeData[] = []; - - Object.entries(execsByNamespace) - .sort(([namespaceA], [namespaceB]) => namespaceA.localeCompare(namespaceB)) - .forEach(([namespace, executables]) => { - treeData.push({ - label: namespace, - value: namespace, - isNamespace: true, - verbType: null, - children: executables - .sort((a, b) => (a.id || "").localeCompare(b.id || "")) - .map((executable) => ({ - label: executable.name - ? executable.verb + " " + executable.name - : executable.verb, - value: executable.ref, - isNamespace: false, - verbType: GetUIVerbType(executable), - })), - }); - }); - - rootExecutables - .sort((a, b) => (a.id || "").localeCompare(b.id || "")) - .forEach((executable) => { - treeData.push({ - label: executable.name - ? executable.verb + " " + executable.name - : executable.verb, - value: executable.ref, - isNamespace: false, - verbType: GetUIVerbType(executable), - }); - }); - - return treeData; -} - -function Leaf({ - node, - selected, - expanded, - hasChildren, - elementProps, -}: RenderTreeNodePayload) { - const customNode = node as CustomTreeNodeData; - const [, setLocation] = useLocation(); - - const icon = useMemo(() => { - if (customNode.isNamespace && hasChildren) { - if (selected && expanded) { - return ; - } else { - return ; - } - } else { - switch (customNode.verbType) { - case DeactivationVerbType: - return ; - case ConfigurationVerbType: - return ; - case DestructionVerbType: - return ; - case RetrievalVerbType: - return ; - case UpdateVerbType: - return ; - case ValidationVerbType: - return ; - case LaunchVerbType: - return ; - case CreationVerbType: - return ; - case RestartVerbType: - return ; - case BuildVerbType: - return ; - default: - return ; - } - } - }, [hasChildren, selected, expanded]); - - const handleExecutableClick = useCallback(() => { - const encodedId = encodeURIComponent(customNode.value); - setLocation(`/executable/${encodedId}`); - }, [setLocation]); - - if (customNode.isNamespace) { - return ( - - {icon} - {customNode.label} - - ); - } - - return ( - - {icon} - {customNode.label} - - ); -} - -export function ExecutableTree() { - const { executables } = useAppContext(); - const tree = useTree(); - - const treeData = useMemo(() => getTreeData(executables), [executables]); - - return ( - <> - - EXECUTABLES ({executables.length}) - - {executables.length === 0 ? ( - - No executables found - - ) : ( - - - - )} - - ); -} diff --git a/desktop/src/layout/Sidebar/Sidebar.tsx b/desktop/src/layout/Sidebar/Sidebar.tsx index 3eda4384..2468dc4c 100644 --- a/desktop/src/layout/Sidebar/Sidebar.tsx +++ b/desktop/src/layout/Sidebar/Sidebar.tsx @@ -4,22 +4,24 @@ import { IconFolders, IconLogs, IconSettings, + IconTerminal2, } from "@tabler/icons-react"; import { useCallback } from "react"; import { Link, useLocation } from "wouter"; -import { useAppContext } from "../../hooks/useAppContext.tsx"; -import { ExecutableTree } from "./ExecutableTree/ExecutableTree"; import styles from "./Sidebar.module.css"; import iconImage from "/logo-dark.png"; export function Sidebar() { const [location, setLocation] = useLocation(); - const { executables } = useAppContext(); const navigateToWorkspaces = useCallback(() => { setLocation(`/workspaces`); }, [setLocation]); + const navigateToExecutables = useCallback(() => { + setLocation(`/executables`); + }, [setLocation]); + const navigateToLogs = useCallback(() => { setLocation("/logs"); }, [setLocation]); @@ -51,6 +53,14 @@ export function Sidebar() { onClick={navigateToWorkspaces} /> + } + active={location.startsWith("/executables")} + variant="filled" + onClick={navigateToExecutables} + /> + } @@ -88,7 +98,6 @@ export function Sidebar() { /> - {executables && executables.length > 0 && } ); diff --git a/desktop/src/pages/Executable/Executable.tsx b/desktop/src/pages/Executable/Executable.tsx index 54b63e4c..13fa968a 100644 --- a/desktop/src/pages/Executable/Executable.tsx +++ b/desktop/src/pages/Executable/Executable.tsx @@ -3,7 +3,6 @@ import { Button, Card, Code, - Divider, Drawer, Grid, Group, @@ -12,92 +11,48 @@ import { ThemeIcon, Title, Tooltip, + LoadingOverlay, + Alert, ButtonGroup, } from "@mantine/core"; import { + IconArrowsSplit, IconClock, IconExternalLink, IconEye, IconFile, IconLabel, IconPlayerPlay, + IconRoute, IconTag, + IconTemplate, IconTerminal, } from "@tabler/icons-react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { openPath } from "@tauri-apps/plugin-opener"; import { useEffect, useState } from "react"; +import { useParams } from "wouter"; import { MarkdownRenderer } from "../../components/MarkdownRenderer"; import { useNotifier } from "../../hooks/useNotifier"; import { useSettings } from "../../hooks/useSettings"; +import { useExecutable } from "../../hooks/useExecutable"; +import { PageWrapper } from "../../components/PageWrapper.tsx"; +import { Hero } from "../../components/Hero"; import { EnrichedExecutable } from "../../types/executable"; import { NotificationType } from "../../types/notification"; import { LogLine, LogViewer } from "../Logs/LogViewer"; import { ExecutableEnvironmentDetails } from "./ExecutableEnvironmentDetails"; import { ExecutableTypeDetails } from "./ExecutableTypeDetails"; import { ExecutionForm, ExecutionFormData } from "./ExecutionForm"; +import { stringToColor } from "../../utils/colors.ts"; -export type ExecutableProps = { - executable: EnrichedExecutable; -}; +export function Executable() { + const params = useParams(); + const executableId = decodeURIComponent(params.executableId || ""); + const { executable, executableError, isExecutableLoading } = + useExecutable(executableId); -function getExecutableTypeInfo(executable: EnrichedExecutable) { - if (executable.exec) - return { - type: "exec", - icon: IconTerminal, - description: "Command execution", - }; - if (executable.serial) - return { - type: "serial", - icon: IconTerminal, - description: "Sequential execution", - }; - if (executable.parallel) - return { - type: "parallel", - icon: IconTerminal, - description: "Parallel execution", - }; - if (executable.launch) - return { - type: "launch", - icon: IconExternalLink, - description: "Launch application/URI", - }; - if (executable.request) - return { - type: "request", - icon: IconExternalLink, - description: "HTTP request", - }; - if (executable.render) - return { - type: "render", - icon: IconTerminal, - description: "Render template", - }; - return { type: "unknown", icon: IconTerminal, description: "Unknown type" }; -} - -function getVisibilityColor(visibility?: string) { - switch (visibility) { - case "public": - return "green"; - case "private": - return "blue"; - case "internal": - return "orange"; - case "hidden": - return "red"; - default: - return "gray"; - } -} - -export function Executable({ executable }: ExecutableProps) { - const typeInfo = getExecutableTypeInfo(executable); + // Local UI state and helpers formerly in ExecutableView const { settings } = useSettings(); const { setNotification } = useNotifier(); const [output, setOutput] = useState([]); @@ -130,19 +85,16 @@ export function Executable({ executable }: ExecutableProps) { }); }; - setupListeners(); + void setupListeners(); return () => { - if (unlistenOutput) { - unlistenOutput(); - } - if (unlistenComplete) { - unlistenComplete(); - } + if (unlistenOutput) unlistenOutput(); + if (unlistenComplete) unlistenComplete(); }; }, [setNotification]); const onOpenFile = async () => { + if (!executable) return; try { await openPath(executable.flowfile, settings.executableApp || undefined); } catch (error) { @@ -150,21 +102,8 @@ export function Executable({ executable }: ExecutableProps) { } }; - const onExecute = async () => { - const hasPromptParams = executable.exec?.params?.some( - (param) => param.prompt, - ); - const hasArgs = executable.exec?.args && executable.exec.args.length > 0; - - if (hasPromptParams || hasArgs) { - setFormOpened(true); - return; - } - - await executeWithData({ params: {}, args: "" }); - }; - const executeWithData = async (formData: ExecutionFormData) => { + if (!executable) return; try { setOutput([]); @@ -208,149 +147,236 @@ export function Executable({ executable }: ExecutableProps) { } }; + const onExecute = async () => { + if (!executable) return; + const hasPromptParams = executable.exec?.params?.some((param) => param.prompt); + const hasArgs = executable.exec?.args && executable.exec.args.length > 0; + + if (hasPromptParams || hasArgs) { + setFormOpened(true); + return; + } + + await executeWithData({ params: {}, args: "" }); + }; + + const typeInfo = executable && getExecutableTypeInfo(executable); + return ( - - - - - - + + {isExecutableLoading && ( + + )} + {executableError && Error: {executableError.message}} + {executable ? ( + + + + - + {typeInfo && } -
- {executable.ref} - - {typeInfo.description} - -
+ {executable.ref}
- - - - - - - {executable.flowfile.split("/").pop() || - executable.flowfile} - - - - + {typeInfo?.description && ( + {typeInfo.description} + )} +
+ + + + + {executable.visibility || "public"} + + + {executable.timeout && ( + - - {executable.visibility || "public"} + + {executable.timeout} - {executable.timeout && ( - - - - {executable.timeout} - - - )} -
-
- - - - - -
+ )} + + + + + {executable.flowfile.split("/").pop() || executable.flowfile} + + + + + + + + + {executable.description && ( <> - {executable.description} )} -
-
- - {executable.aliases && executable.aliases.length > 0 && ( - - - - - <Group gap="xs"> - <IconLabel size={16} /> - Aliases - </Group> - - - {executable.aliases.map((alias, index) => ( - {alias} - ))} - - - - - )} + + {executable.aliases && executable.aliases.length > 0 && ( + + + + + <Group gap="xs"> + <IconLabel size={16} /> + Aliases + </Group> + + + {executable.aliases.map((alias, index) => ( + {alias} + ))} + + + + + )} - {executable.tags && executable.tags.length > 0 && ( - - - - - <Group gap="xs"> - <IconTag size={16} /> - Tags - </Group> - - - {executable.tags.map((tag, index) => ( - - {tag} - - ))} - - - - - )} - + {executable.verbAliases && executable.verbAliases.length > 0 && ( + + + + + <Group gap="xs"> + <IconLabel size={16} /> + Verb Aliases + </Group> + + + {executable.verbAliases.map((alias, index) => ( + {String(alias)} + ))} + + + + + )} - - + {executable.tags && executable.tags.length > 0 && ( + + + + + <Group gap="xs"> + <IconTag size={16} /> + Tags + </Group> + + + {executable.tags.map((tag, index) => ( + + {tag} + + ))} + + + + + )} + - {output.length > 0 && ( - setOutput([])} - title={Execution Output} - size="33%" - position="bottom" - > - - - )} + + - {formOpened && ( - setFormOpened(false)} - onSubmit={executeWithData} - executable={executable} - /> + {output.length > 0 && ( + setOutput([])} + title={Execution Output} + size="33%" + position="bottom" + > + + + )} + + {formOpened && ( + setFormOpened(false)} + onSubmit={executeWithData} + executable={executable} + /> + )} +
+ ) : ( + Error: Executable not found )} - +
); } + +function getExecutableTypeInfo(executable: EnrichedExecutable) { + if (executable.exec) + return { + type: "exec", + icon: IconTerminal, + description: "Command execution", + }; + if (executable.serial) + return { + type: "serial", + icon: IconRoute, + description: "Sequential execution", + }; + if (executable.parallel) + return { + type: "parallel", + icon: IconArrowsSplit, + description: "Parallel execution", + }; + if (executable.launch) + return { + type: "launch", + icon: IconExternalLink, + description: "Launch application/URI", + }; + if (executable.request) + return { + type: "request", + icon: IconExternalLink, + description: "HTTP request", + }; + if (executable.render) + return { + type: "render", + icon: IconTemplate, + description: "Render template", + }; + return { type: "unknown", icon: IconTerminal, description: "Unknown type" }; +} + +function getVisibilityColor(visibility?: string) { + switch (visibility) { + case "public": + return "green.3"; + case "private": + return "blue.3"; + case "internal": + return "orange.3"; + case "hidden": + return "red.3"; + default: + return "gray.3"; + } +} diff --git a/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx b/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx index 08f85e24..67d1d66e 100644 --- a/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx +++ b/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx @@ -1,5 +1,4 @@ import { - ActionIcon, Badge, Card, Code, @@ -9,9 +8,8 @@ import { Table, Text, Title, - Tooltip, } from "@mantine/core"; -import { IconFlag, IconInfoCircle, IconKey } from "@tabler/icons-react"; +import { IconFlag, IconKey, IconFile } from "@tabler/icons-react"; import { EnrichedExecutable } from "../../types/executable"; import { ExecutableArgument, @@ -22,9 +20,9 @@ export type ExecutableEnvironmentDetailsProps = { executable: EnrichedExecutable; }; -export function ExecutableEnvironmentDetails({ - executable, -}: ExecutableEnvironmentDetailsProps) { +type ParamType = "static" | "secret" | "prompt" | "file" | "unknown"; + +export function ExecutableEnvironmentDetails({executable}: ExecutableEnvironmentDetailsProps) { const env = executable.exec || executable.launch || @@ -50,54 +48,54 @@ export function ExecutableEnvironmentDetails({ Environment Parameters - +
- Variable + Destination Type Source - {env.params.map( - (param: ExecutableParameter, index: number) => { - const type = param.text - ? "static" - : param.secretRef - ? "secret" - : "prompt"; - const source = - param.text || param.secretRef || param.prompt; + {env.params.map((param: ExecutableParameter, index: number) => { + const type = getParamType(param); + const source = getParamSource(param); + const fileDestination = param.outputFile; - return ( - - - {param.envKey} - - - - {type} - - - - - {source} - - - - ); - }, - )} + return ( + + + {param.envKey ? ( + + {param.envKey} + ENV + + ) : fileDestination ? ( + + + {fileDestination} + + ) : ( + - + )} + + + + {type} + + + + + {source} + + + + ); + })}
@@ -115,49 +113,63 @@ export function ExecutableEnvironmentDetails({ Command Arguments - +
- Variable - Input + Destination + CLI Input Type Required - {env.args.map((arg: ExecutableArgument, index: number) => ( - - - {arg.envKey} - - - - - {arg.pos ? `pos-${arg.pos}` : `--${arg.flag}`} - - {arg.default && ( - - - - - + {env.args.map((arg: ExecutableArgument, index: number) => { + const fileDestination = arg.outputFile; + + return ( + + + {arg.envKey ? ( + + {arg.envKey} + ENV + + ) : fileDestination ? ( + + + {fileDestination} + + ) : ( + Not saved )} - - - - {arg.type || "string"} - - - - {arg.required ? "Yes" : "No"} - - - - ))} + + + + + {arg.pos ? `position=${arg.pos}` : `flag=${arg.flag}`} + + {arg.default && ( + + default: {arg.default} + + )} + + + + {arg.type || "string"} + + + + {arg.required ? "Yes" : "No"} + + + + ); + })}
@@ -167,3 +179,25 @@ export function ExecutableEnvironmentDetails({ ); } + +function getParamType(param: ExecutableParameter): ParamType { + if (param.text) return "static"; + if (param.secretRef) return "secret"; + if (param.prompt) return "prompt"; + if (param.envFile) return "file"; + return "unknown"; +} + +function getParamSource(param: ExecutableParameter): string { + return param.text || param.secretRef || param.prompt || param.envFile || "-"; +} + +function getParamTypeColor(type: ParamType): string { + switch (type) { + case "secret": return "red.5"; + case "prompt": return "blue.5"; + case "file": return "purple.5"; + case "static": return "gray.5"; + default: return "gray.5"; + } +} diff --git a/desktop/src/pages/Executable/ExecutableRoute.tsx b/desktop/src/pages/Executable/ExecutableRoute.tsx deleted file mode 100644 index 1783f752..00000000 --- a/desktop/src/pages/Executable/ExecutableRoute.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Text, LoadingOverlay } from "@mantine/core"; -import { useParams } from "wouter"; -import { useExecutable } from "../../hooks/useExecutable"; -import { PageWrapper } from "../../components/PageWrapper.tsx"; -import { Welcome } from "../Welcome/Welcome"; -import { Executable } from "./Executable"; - -export function ExecutableRoute() { - const params = useParams(); - const executableId = decodeURIComponent(params.executableId || ""); - const { executable, executableError, isExecutableLoading } = - useExecutable(executableId); - - return ( - - {isExecutableLoading && ( - - )} - {executableError && Error: {executableError.message}} - {executable ? ( - - ) : ( - - )} - - ); -} diff --git a/desktop/src/pages/Executable/types/ExecutableExecDetails.tsx b/desktop/src/pages/Executable/types/ExecutableExecDetails.tsx index b70e3efc..94eb43df 100644 --- a/desktop/src/pages/Executable/types/ExecutableExecDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableExecDetails.tsx @@ -1,5 +1,5 @@ -import { Badge, Card, Code, Group, Stack, Title } from "@mantine/core"; -import { IconTerminal } from "@tabler/icons-react"; +import { Badge, Card, Code, Divider, Group, Stack, Title } from "@mantine/core"; +import { IconHash } from "@tabler/icons-react"; import { useSettings } from "../../../hooks/useSettings"; import { EnrichedExecutable } from "../../../types/executable"; import { CodeHighlighter } from "../../../components/CodeHighlighter"; @@ -14,42 +14,50 @@ export function ExecutableExecDetails({ const { settings } = useSettings(); return ( - + - + <Group justify="space-between" align="center"> + <Title order={4}> + <Group gap="xs"> + <IconHash size={16} /> + Execution Configuration + </Group> + - - Execution Details + {executable.exec?.cmd && ( + cmd + )} + {executable.exec?.file && ( + file + )} + {executable.exec?.logMode && ( + format: {executable.exec?.logMode} + )} - - - {executable.exec?.cmd && ( -
- Command: - - {executable.exec.cmd} - -
- )} - {executable.exec?.file && ( -
- File: - {executable.exec.file} -
- )} - {executable.exec?.dir && ( -
- Directory: - {executable.exec.dir} -
- )} - {executable.exec?.logMode && ( -
- Log Mode: - {executable.exec.logMode} -
- )} -
+ + + {executable.exec?.cmd && ( + <> + + + {executable.exec.cmd} + + + )} + + {executable.exec?.file && ( + <> + + {executable.exec.file} + + )} + + {executable.exec?.dir && ( + <> + + {executable.exec.dir} + + )}
); diff --git a/desktop/src/pages/Executable/types/ExecutableLaunchDetails.tsx b/desktop/src/pages/Executable/types/ExecutableLaunchDetails.tsx index 280deb08..71fc9925 100644 --- a/desktop/src/pages/Executable/types/ExecutableLaunchDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableLaunchDetails.tsx @@ -1,4 +1,4 @@ -import { Card, Code, Group, Stack, Text, Title } from "@mantine/core"; +import { Badge, Card, Code, Divider, Group, Stack, Text, Title } from "@mantine/core"; import { IconExternalLink } from "@tabler/icons-react"; import { EnrichedExecutable } from "../../../types/executable"; @@ -10,35 +10,44 @@ export function ExecutableLaunchDetails({ executable, }: ExecutableLaunchDetailsProps) { return ( - + - + <Group justify="space-between" align="center"> + <Title order={4}> + <Group gap="xs"> + <IconExternalLink size={16} /> + Launch Configuration + </Group> + - - Launch Configuration + {typeof executable.launch?.app !== "undefined" && ( + + app: {executable.launch?.app ? "set" : "unset"} + + )} - - - {executable.launch?.app && ( -
- App: - {executable.launch.app} -
- )} - {executable.launch?.uri && ( -
- URI: - - {executable.launch.uri} - -
- )} -
+ + + {executable.launch?.uri && ( + <> + + + {executable.launch.uri} + + + )} + + {executable.launch?.app && ( + <> + + {executable.launch.app} + + )}
); diff --git a/desktop/src/pages/Executable/types/ExecutableParallelDetails.tsx b/desktop/src/pages/Executable/types/ExecutableParallelDetails.tsx index fe36e617..d5ceaaef 100644 --- a/desktop/src/pages/Executable/types/ExecutableParallelDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableParallelDetails.tsx @@ -1,8 +1,9 @@ -import { Badge, Card, Code, Group, Stack, Text, Title } from "@mantine/core"; -import { IconTerminal } from "@tabler/icons-react"; +import { Anchor, Badge, Card, Divider, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core"; +import { IconArrowsRight } from "@tabler/icons-react"; import { useSettings } from "../../../hooks/useSettings"; import { EnrichedExecutable } from "../../../types/executable"; import { CodeHighlighter } from "../../../components/CodeHighlighter"; +import { Link } from "wouter"; export type ExecutableParallelDetailsProps = { executable: EnrichedExecutable; @@ -13,79 +14,88 @@ export function ExecutableParallelDetails({ }: ExecutableParallelDetailsProps) { const { settings } = useSettings(); + const execs = executable.parallel?.execs || []; + return ( - + - + <Group justify="space-between"> + <Title order={4}> + <Group gap="xs"> + <IconArrowsRight size={16} /> + Parallel Configuration + </Group> + - - Parallel Configuration - - - - {executable.parallel?.maxThreads && - executable.parallel.maxThreads > 0 && ( -
- Max Threads: - {executable.parallel.maxThreads} -
- )} - {executable.parallel?.failFast !== undefined && ( -
- Fail Fast: - - {executable.parallel.failFast ? "enabled" : "disabled"} + {executable.parallel?.failFast && ( + + Fail fast: {executable.parallel?.failFast ? "on" : "off"} -
- )} - {executable.parallel?.execs && - executable.parallel.execs.length > 0 && ( -
- Executables: - - {executable.parallel.execs.map((exec, index) => ( -
- - {index + 1}. {exec.ref ? `ref: ${exec.ref}` : "cmd:"} - - {exec.cmd && ( - - {exec.cmd} - - )} - {exec.retries !== undefined && exec.retries > 0 && ( -
- - • Retries: {exec.retries} - -
- )} - {exec.args && exec.args.length > 0 && ( -
- - • Arguments: - - - {exec.args.map((arg, argIndex) => ( - - - {arg} - - ))} - -
- )} -
- ))} -
-
)} -
+ {executable.parallel?.maxThreads && executable.parallel.maxThreads > 0 && ( + Max threads: {executable.parallel.maxThreads} + )} + {execs.length > 0 && ( + Execs: {execs.length} + )} + + + + {execs.length > 0 ? ( + <> + + + {execs.map((exec, index) => ( + + + + + #{index + 1} + {exec.ref ? ( + <> + ref: + + {exec.ref} + + + ) : ( + cmd + )} + + + {exec.retries !== undefined && exec.retries > 0 && ( + retries: {exec.retries} + )} + {exec.args && exec.args.length > 0 && ( + args: {exec.args.length} + )} + + + + {exec.cmd && ( + + {exec.cmd} + + )} + + {exec.args && exec.args.length > 0 && ( + + Arguments + + {exec.args.map((arg, argIndex) => ( + - {arg} + ))} + + + )} + + + ))} + + + ) : ( + No executables defined. + )}
); diff --git a/desktop/src/pages/Executable/types/ExecutableRenderDetails.tsx b/desktop/src/pages/Executable/types/ExecutableRenderDetails.tsx index 050a8017..d236862e 100644 --- a/desktop/src/pages/Executable/types/ExecutableRenderDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableRenderDetails.tsx @@ -1,5 +1,6 @@ -import { Card, Code, Group, Stack, Title } from "@mantine/core"; -import { IconTerminal } from "@tabler/icons-react"; +import { Card, Code, Divider, Group, Stack, Text, Title } from "@mantine/core"; +import { IconArrowAutofitHeightFilled +} from "@tabler/icons-react"; import { EnrichedExecutable } from "../../../types/executable"; export type ExecutableRenderDetailsProps = { @@ -10,34 +11,43 @@ export function ExecutableRenderDetails({ executable, }: ExecutableRenderDetailsProps) { return ( - + - - <Group gap="xs"> - <IconTerminal size={16} /> - Render Configuration - </Group> - - - {executable.render?.dir && ( -
- Executed from: - {executable.render.dir} -
- )} - {executable.render?.templateFile && ( -
- Template File: - {executable.render.templateFile} -
- )} - {executable.render?.templateDataFile && ( -
- Template Store File: - {executable.render.templateDataFile} -
- )} -
+ + + <Group gap="xs"> + <IconArrowAutofitHeightFilled size={16} /> + Render Configuration + </Group> + + + + {(executable.render?.templateFile || executable.render?.templateDataFile) && ( + <> + + + {executable.render?.templateFile && ( + + Template file + {executable.render.templateFile} + + )} + {executable.render?.templateDataFile && ( + + Template data file + {executable.render.templateDataFile} + + )} + + + )} + + {executable.render?.dir && ( + <> + + {executable.render.dir} + + )}
); diff --git a/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx b/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx index 5b2526be..125dac86 100644 --- a/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx @@ -1,5 +1,5 @@ -import { Badge, Card, Code, Group, Stack, Text, Title } from "@mantine/core"; -import { IconExternalLink } from "@tabler/icons-react"; +import { Badge, Card, Code, Divider, Group, Stack, Table, Text, Title } from "@mantine/core"; +import { IconArrowForwardUpDouble } from "@tabler/icons-react"; import { useSettings } from "../../../hooks/useSettings"; import { EnrichedExecutable } from "../../../types/executable"; import { CodeHighlighter } from "../../../components/CodeHighlighter"; @@ -14,109 +14,124 @@ export function ExecutableRequestDetails({ const { settings } = useSettings(); return ( - + - + <Group justify="space-between" align="center"> + <Title order={4}> + <Group gap="xs"> + <IconArrowForwardUpDouble size={16} /> + Request Configuration + </Group> + - - Request Configuration - - - - {executable.request?.method && ( -
- Method: - {executable.request.method} -
- )} - {executable.request?.url && ( -
- URL: - - {executable.request.url} - -
- )} - {executable.request?.timeout && ( -
- Request Timeout: - {executable.request.timeout} -
- )} - {executable.request?.logResponse && ( -
- Log Response: - - enabled - -
- )} - {executable.request?.body && ( -
- Body: - - {executable.request.body} - -
- )} - {executable.request?.headers && - Object.keys(executable.request.headers).length > 0 && ( -
- Headers: - - {Object.entries(executable.request.headers).map( - ([key, value]) => ( -
- - {key}: - - {value} -
- ), - )} -
-
+ {executable.request?.method && ( + {executable.request.method} )} - {executable.request?.validStatusCodes && - executable.request.validStatusCodes.length > 0 && ( -
- Accepted Status Codes: - - {executable.request.validStatusCodes.map((code) => ( - - {code} - - ))} - -
+ {executable.request?.timeout && ( + timeout: {executable.request?.timeout} )} - {executable.request?.responseFile && ( -
- Response Saved To: - {executable.request.responseFile.filename} + + log response: {executable.request?.logResponse ? "on" : "off"} + + + + + {executable.request?.url && ( + <> + + + {executable.request.url} + + + )} + + {executable.request?.headers && Object.keys(executable.request.headers).length > 0 && ( + <> + + + + {Object.entries(executable.request.headers).map(([key, value]) => ( + + {key} + + {String(value)} + + + ))} + +
+ + )} + + {executable.request?.validStatusCodes && executable.request.validStatusCodes.length > 0 && ( + <> + + + {executable.request.validStatusCodes.map((code) => ( + {code} + ))} + + + )} + + {executable.request?.body && ( + <> + + + {executable.request.body} + + + )} + + {executable.request?.transformResponse && ( + <> + + + {executable.request.transformResponse} + + + )} + + {executable.request?.responseFile && ( + <> + + + + Filename + {executable.request.responseFile.filename} + {executable.request.responseFile.saveAs && ( -
- Response Saved As: + + Save as {executable.request.responseFile.saveAs} -
+ )} -
- )} - {executable.request?.transformResponse && ( -
- Transformation Expression: - - {executable.request.transformResponse} - -
- )} -
+ + + )}
); } + +const methodColor = (method?: string) => { + switch ((method || "").toUpperCase()) { + case "GET": + return "green.5"; + case "POST": + return "blue.5"; + case "PUT": + return "orange.5"; + case "PATCH": + return "yellow.5"; + case "DELETE": + return "red.5"; + default: + return "gray.5"; + } +}; diff --git a/desktop/src/pages/Executable/types/ExecutableSerialDetails.tsx b/desktop/src/pages/Executable/types/ExecutableSerialDetails.tsx index 8504cc34..e35d047d 100644 --- a/desktop/src/pages/Executable/types/ExecutableSerialDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableSerialDetails.tsx @@ -1,8 +1,9 @@ -import { Badge, Card, Group, Stack, Text, Title } from "@mantine/core"; -import { IconTerminal } from "@tabler/icons-react"; +import { Anchor, Badge, Card, Divider, Group, Stack, Text, Timeline, Title } from "@mantine/core"; +import { IconArrowRight } from "@tabler/icons-react"; import { useSettings } from "../../../hooks/useSettings"; import { EnrichedExecutable } from "../../../types/executable"; import { CodeHighlighter } from "../../../components/CodeHighlighter"; +import { Link } from "wouter"; export type ExecutableSerialDetailsProps = { executable: EnrichedExecutable; @@ -13,78 +14,90 @@ export function ExecutableSerialDetails({ }: ExecutableSerialDetailsProps) { const { settings } = useSettings(); + const execs = executable.serial?.execs || []; + return ( - + - + <Group justify="space-between"> + <Title order={4}> + <Group gap="xs"> + <IconArrowRight size={16} /> + Serial Configuration + </Group> + - - Serial Configuration - - - - {executable.serial?.failFast !== undefined && ( -
- Fail Fast: - - {executable.serial.failFast ? "enabled" : "disabled"} + {executable.serial?.failFast && ( + + Fail fast: {executable.serial?.failFast ? "on" : "off"} -
- )} - {executable.serial?.execs && executable.serial.execs.length > 0 && ( -
- Executables: - - {executable.serial.execs.map((exec, index) => ( -
- - {index + 1}. {exec.ref ? `ref: ${exec.ref}` : "cmd:"} - + )} + {execs.length > 0 && ( + Execs: {execs.length} + )} + + + + {execs.length > 0 ? ( + <> + + + {execs.map((exec, index) => ( + + #{index + 1} + {exec.ref ? ( + <> + ref: + + {exec.ref} + + + ) : ( + cmd + )} + + } + > + + + {exec.retries !== undefined && exec.retries > 0 && ( + retries: {exec.retries} + )} + {exec.reviewRequired && ( + review required + )} + {exec.args && exec.args.length > 0 && ( + args: {exec.args.length} + )} + + {exec.cmd && ( - + {exec.cmd} )} - {exec.retries !== undefined && exec.retries > 0 && ( -
- - • Retries: {exec.retries} - -
- )} - {exec.reviewRequired && ( -
- - • Review Required: {exec.reviewRequired.toString()} - -
- )} + {exec.args && exec.args.length > 0 && ( -
- - • Arguments: - - + + Arguments + {exec.args.map((arg, argIndex) => ( - - - {arg} - + - {arg} ))} -
+
)} -
- ))} -
-
- )} -
+
+ + ))} + + + ) : ( + No executables defined. + )}
); diff --git a/desktop/src/pages/Executables/Executables.module.css b/desktop/src/pages/Executables/Executables.module.css new file mode 100644 index 00000000..31143130 --- /dev/null +++ b/desktop/src/pages/Executables/Executables.module.css @@ -0,0 +1,50 @@ +.tableContainer { + position: relative; + overflow: hidden; +} + +.tableContainer tr { + padding-top: var(--mantine-spacing-sm); + padding-bottom: var(--mantine-spacing-sm); +} + +.cell { + vertical-align: top; +} + +.iconCell { + width: 20px; +} + +.tag { + font-size: var(--mantine-font-size-xs); +} + +.verbIcon { + padding-top: 4px; + color: var(--mantine-color-emphasis-0); +} + +.executableRef { + font-size: var(--mantine-font-size-sm) !important; + font-weight: 600 !important; + font-family: var(--mantine-font-family-monospace); + color: var(--mantine-color-tertiary-0) !important; + cursor: pointer; +} + +.filterTitle{ + font-weight: 600 !important; + color: var(--mantine-color-primary-0) !important; +} + +.filterLabel { + font-weight: 600 !important; + color: var(--mantine-color-tertiary-0) !important; +} + +.emptyState { + text-align: center; + padding: 3rem 1rem; + color: var(--mantine-color-dimmed); +} diff --git a/desktop/src/pages/Executables/Executables.tsx b/desktop/src/pages/Executables/Executables.tsx new file mode 100644 index 00000000..0846bacf --- /dev/null +++ b/desktop/src/pages/Executables/Executables.tsx @@ -0,0 +1,121 @@ +import { + Alert, + Badge, + Box, + Button, + Group, + Text, + Title, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { Hero } from "../../components/Hero"; +import { PageWrapper } from "../../components/PageWrapper"; +import { useLocation } from "wouter"; +import { useExecutablesPage } from "./useExecutablesPage"; +import { ExecutablesTable, ViewMode } from "./ExecutablesTable"; +import { FilterButton, FilterDrawer } from "./Filters"; +import classes from "./Executables.module.css"; +import { useState } from "react"; +import { IconArticle, IconList, IconRefresh } from "@tabler/icons-react"; + +export function Executables() { + const { + executables, + isLoading, + error, + filterState, + onFilterChange, + onClearAll, + activeFilterCount, + workspaceOptions, + namespaceOptions, + tagOptions, + verbOptions, + refresh, + } = useExecutablesPage(); + + if (error) { + console.log(error); + } + + const [opened, { open, close }] = useDisclosure(false); + const [viewMode, setViewMode] = useState(ViewMode.Card); + const [, setLocation] = useLocation(); + + const handleRowClick = (ref: string) => + setLocation(`/executable/${encodeURIComponent(ref)}`); + + return ( + + + + Executables + + Discover and run your development workflows + + + + + {isLoading ? "Loading..." : `${executables.length} total`} + + + + + + + + + + + + + + + + + + {error && ( + + Encountered and error while loading executables: + {error.name} + + )} + + + 0} + onRowClick={handleRowClick} + viewMode={viewMode} + /> + + + + + ); +} diff --git a/desktop/src/pages/Executables/ExecutablesTable.tsx b/desktop/src/pages/Executables/ExecutablesTable.tsx new file mode 100644 index 00000000..8803fbfe --- /dev/null +++ b/desktop/src/pages/Executables/ExecutablesTable.tsx @@ -0,0 +1,204 @@ +import { ActionIcon, Group, Menu, Badge, Table, Text, Stack } from "@mantine/core"; +import { + IconDotsVertical, + IconExternalLink, + IconFile, + IconPlayerPlayFilled, + IconOctagon, + IconSettingsAutomation, + IconProgressX, + IconProgressDown, + IconRefresh, + IconCircleCheckFilled, + IconWindowMaximize, + IconCirclePlus, + IconReload, + IconBlocks, + IconStar, +} from "@tabler/icons-react"; +import clsx from "clsx"; +import type { EnrichedExecutable } from "../../types/executable"; +import { GetUIVerbType, DeactivationVerbType, ConfigurationVerbType, DestructionVerbType, RetrievalVerbType, UpdateVerbType, ValidationVerbType, LaunchVerbType, CreationVerbType, RestartVerbType, BuildVerbType } from "../../types/executable"; +import { stringToColor } from "../../utils/colors"; +import { openPath } from "@tauri-apps/plugin-opener"; +import { useNotifier } from "../../hooks/useNotifier"; +import { NotificationType } from "../../types/notification"; +import { shortenCleanDescription } from "../../utils/text"; +import classes from "./Executables.module.css"; + +export enum ViewMode { + List = "list", + Card = "card", +} + +function IconForVerbType(verbType: string | null) { + switch (verbType) { + case DeactivationVerbType: + return ; + case ConfigurationVerbType: + return ; + case DestructionVerbType: + return ; + case RetrievalVerbType: + return ; + case UpdateVerbType: + return ; + case ValidationVerbType: + return ; + case LaunchVerbType: + return ; + case CreationVerbType: + return ; + case RestartVerbType: + return ; + case BuildVerbType: + return ; + default: + return ; + } +} + +export function ExecutablesTable({ + items, + loading, + hasActiveFilters, + onRowClick, + viewMode, + }: { + items: EnrichedExecutable[]; + loading: boolean; + hasActiveFilters: boolean; + onRowClick: (ref: string) => void; + viewMode: ViewMode; +}) { + const { setNotification } = useNotifier(); + + const rows = items.map((exec) => ( + onRowClick(exec.ref)} + style={{ cursor: 'pointer' }} + className={classes.tableRow} + > + + {IconForVerbType(GetUIVerbType(exec))} + + + + + + {exec.ref} + + + + + e.stopPropagation()} + > + + + + e.stopPropagation()}> + } onClick={() => onRowClick(exec.ref)}> + Open details + + } onClick={async () => { + try { + await openPath(exec.flowfile); + } catch (error) { + setNotification({ type: NotificationType.Error, title: "Failed to open flowfile", message: String(error) }); + } + }}> + Open flowfile + + + } onClick={() => { + setNotification({ type: NotificationType.Success, title: "Favorited", message: `${exec.ref} saved as favorite` }); + }}> + Save as favorite + + + + + + {viewMode === ViewMode.Card && exec.fullDescription && ( + + {shortenCleanDescription(exec.fullDescription, 180)} + + )} + + {viewMode === ViewMode.Card && ( + + + {getExecutableTypeLabel(exec)} + + {exec.tags && exec.tags.length > 0 && exec.tags.map((tag, i) => ( + + {tag} + + ))} + + )} + + + + )); + + return ( + + + {rows.length > 0 ? ( + rows + ) : ( + + + + + {loading + ? "Loading executables..." + : hasActiveFilters + ? "No executables match your filters" + : "No executables found"} + + + + + )} + +
+ ); +} + +function getExecutableTypeColor(exec: EnrichedExecutable): string { + if (exec.exec) return 'blue.5'; + if (exec.serial) return 'green.5'; + if (exec.parallel) return 'orange.5'; + if (exec.launch) return 'purple.5'; + if (exec.request) return 'teal.5'; + if (exec.render) return 'pink.5'; + return 'gray.5'; +} + +function getExecutableTypeLabel(exec: EnrichedExecutable): string { + if (exec.exec) return 'Command'; + if (exec.serial) return 'Serial Workflow'; + if (exec.parallel) return 'Parallel Workflow'; + if (exec.launch) return 'Launch'; + if (exec.request) return 'HTTP Request'; + if (exec.render) return 'Template'; + return 'Unknown'; +} \ No newline at end of file diff --git a/desktop/src/pages/Executables/Filters.tsx b/desktop/src/pages/Executables/Filters.tsx new file mode 100644 index 00000000..b5a9b9f3 --- /dev/null +++ b/desktop/src/pages/Executables/Filters.tsx @@ -0,0 +1,192 @@ +import { + Badge, + Box, + Button, + Drawer, + Group, + MultiSelect, + Select, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { + IconAbc, + IconBraces, + IconCategory, + IconEye, + IconFilter, + IconFolder, + IconSearch, + IconTags, +} from "@tabler/icons-react"; +import classes from "./Executables.module.css"; +import { CommonVisibility } from "../../types/generated/flowfile.ts"; + +export interface FilterState { + workspace: string; + tags: string[]; + namespace: string; + verb: string; + search: string; + type: string; + visibility: CommonVisibility | ''; +} + +export interface WorkspaceOption { + label: string; + value: string; +} + +export interface FilterDrawerProps { + opened: boolean; + onClose: () => void; + filterState: FilterState; + onFilterChange: (updates: Partial) => void; + onClearAll: () => void; + // Options for dropdowns + workspaceOptions: WorkspaceOption[]; + namespaceOptions: string[]; + tagOptions: string[]; + verbOptions: string[]; +} + +export function FilterButton({ + activeCount, + onClick +}: { + activeCount: number; + onClick: () => void; +}) { + return ( + + ); +} + +export function FilterDrawer({ + opened, + onClose, + filterState, + onFilterChange, + onClearAll, + workspaceOptions, + namespaceOptions, + tagOptions, + verbOptions, +}: FilterDrawerProps) { + const handleFilterChange = (field: keyof FilterState) => (value: any) => { + onFilterChange({ [field]: value }); + }; + + return ( + + + + + } + value={filterState.search} + onChange={(e) => handleFilterChange('search')(e.currentTarget.value)} + /> + } + data={tagOptions} + value={filterState.tags} + onChange={handleFilterChange('tags')} + clearable + searchable + /> + + + + Scope + } + data={namespaceOptions} + value={filterState.namespace} + onChange={handleFilterChange('namespace')} + clearable + searchable + /> + + + + Properties + } + data={["command", "launch", "render", "request", "serial", "parallel"]} + value={filterState.type} + onChange={handleFilterChange('type')} + clearable + /> +