diff --git a/bin/periphery/src/api/swarm.rs b/bin/periphery/src/api/swarm.rs index 63020f253..6f20b3128 100644 --- a/bin/periphery/src/api/swarm.rs +++ b/bin/periphery/src/api/swarm.rs @@ -122,7 +122,7 @@ impl Resolve for InspectSwarmConfig { async fn resolve( self, _: &super::Args, - ) -> anyhow::Result { + ) -> anyhow::Result> { inspect_swarm_config(&self.id).await } } diff --git a/bin/periphery/src/docker/config.rs b/bin/periphery/src/docker/config.rs index 15b3e8202..732cbecd0 100644 --- a/bin/periphery/src/docker/config.rs +++ b/bin/periphery/src/docker/config.rs @@ -19,13 +19,17 @@ pub async fn list_swarm_configs() ))); } - serde_json::from_str(&res.stdout) - .context("Failed to parse 'docker config ls' response from json") + // The output is in JSONL, need to convert to standard JSON vec. + serde_json::from_str(&format!( + "[{}]", + res.stdout.trim().replace('\n', ",") + )) + .context("Failed to parse 'docker config ls' response from json") } pub async fn inspect_swarm_config( config: &str, -) -> anyhow::Result { +) -> anyhow::Result> { let res = run_komodo_standard_command( "Inspect Swarm Config", None, diff --git a/client/core/rs/src/api/read/swarm.rs b/client/core/rs/src/api/read/swarm.rs index 83a7c53ff..dbb27804d 100644 --- a/client/core/rs/src/api/read/swarm.rs +++ b/client/core/rs/src/api/read/swarm.rs @@ -260,4 +260,4 @@ pub struct InspectSwarmConfig { } #[typeshare] -pub type InspectSwarmConfigResponse = SwarmConfig; +pub type InspectSwarmConfigResponse = Vec; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 617ee0939..850408b29 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -3564,7 +3564,7 @@ export type InspectDockerVolumeResponse = Volume; export type InspectStackContainerResponse = Container; -export type InspectSwarmConfigResponse = SwarmConfig; +export type InspectSwarmConfigResponse = SwarmConfig[]; /** Orchestration configuration. */ export interface SwarmSpecOrchestration { diff --git a/client/periphery/rs/src/api/swarm.rs b/client/periphery/rs/src/api/swarm.rs index 3aea5b385..078d8929b 100644 --- a/client/periphery/rs/src/api/swarm.rs +++ b/client/periphery/rs/src/api/swarm.rs @@ -67,7 +67,7 @@ pub struct InspectSwarmSecret { // ======== #[derive(Debug, Clone, Serialize, Deserialize, Resolve)] -#[response(SwarmConfig)] +#[response(Vec)] #[error(anyhow::Error)] pub struct InspectSwarmConfig { pub id: String, diff --git a/frontend/public/client/types.d.ts b/frontend/public/client/types.d.ts index eee0af911..dd0c368f4 100644 --- a/frontend/public/client/types.d.ts +++ b/frontend/public/client/types.d.ts @@ -3602,7 +3602,7 @@ export interface Volume { } export type InspectDockerVolumeResponse = Volume; export type InspectStackContainerResponse = Container; -export type InspectSwarmConfigResponse = SwarmConfig; +export type InspectSwarmConfigResponse = SwarmConfig[]; /** Orchestration configuration. */ export interface SwarmSpecOrchestration { /** diff --git a/frontend/src/components/resources/swarm/info/configs.tsx b/frontend/src/components/resources/swarm/info/configs.tsx new file mode 100644 index 000000000..0a54ec02c --- /dev/null +++ b/frontend/src/components/resources/swarm/info/configs.tsx @@ -0,0 +1,80 @@ +import { Section } from "@components/layouts"; +import { useRead } from "@lib/hooks"; +import { DataTable, SortableHeader } from "@ui/data-table"; +import { Dispatch, ReactNode, SetStateAction } from "react"; +import { KeyRound, Search } from "lucide-react"; +import { Input } from "@ui/input"; +import { filterBySplit } from "@lib/utils"; +import { Link } from "react-router-dom"; + +export const SwarmConfigs = ({ + id, + titleOther, + _search, +}: { + id: string; + titleOther: ReactNode; + _search: [string, Dispatch>]; +}) => { + const [search, setSearch] = _search; + const configs = + useRead("ListSwarmConfigs", { swarm: id }, { refetchInterval: 10_000 }) + .data ?? []; + + const filtered = filterBySplit( + configs, + search, + (config) => config.Name ?? config.ID ?? "Unknown" + ); + + return ( +
+
+ + setSearch(e.target.value)} + placeholder="search..." + className="pl-8 w-[200px] lg:w-[300px]" + /> +
+ + } + > + ( + + ), + cell: ({ row }) => ( + + + {row.original.Name ?? "Unknown"} + + ), + size: 200, + }, + { + accessorKey: "ID", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.ID ?? "Unknown", + size: 200, + }, + ]} + /> +
+ ); +}; diff --git a/frontend/src/components/resources/swarm/info/index.tsx b/frontend/src/components/resources/swarm/info/index.tsx index a81e51e56..e0ac1c01c 100644 --- a/frontend/src/components/resources/swarm/info/index.tsx +++ b/frontend/src/components/resources/swarm/info/index.tsx @@ -12,8 +12,15 @@ import { SwarmSecrets } from "./secrets"; import { SwarmServices } from "./services"; import { SwarmTasks } from "./tasks"; import { SwarmInspect } from "./inspect"; +import { SwarmConfigs } from "./configs"; -type SwarmInfoView = "Inspect" | "Nodes" | "Services" | "Tasks" | "Secrets"; +type SwarmInfoView = + | "Inspect" + | "Nodes" + | "Services" + | "Tasks" + | "Secrets" + | "Configs"; export const SwarmInfo = ({ id, @@ -56,6 +63,9 @@ export const SwarmInfo = ({ { value: "Secrets", }, + { + value: "Configs", + }, ], [] ); @@ -83,6 +93,8 @@ export const SwarmInfo = ({ return ; case "Secrets": return ; + case "Configs": + return ; } }; diff --git a/frontend/src/pages/swarm/config.tsx b/frontend/src/pages/swarm/config.tsx new file mode 100644 index 000000000..6adf6d8d5 --- /dev/null +++ b/frontend/src/pages/swarm/config.tsx @@ -0,0 +1,76 @@ +import { ResourceLink } from "@components/resources/common"; +import { PageHeaderName } from "@components/util"; +import { useRead, useSetTitle } from "@lib/hooks"; +import { Button } from "@ui/button"; +import { ChevronLeft, KeyRound, Loader2 } from "lucide-react"; +import { useNavigate, useParams } from "react-router-dom"; +import { MonacoEditor } from "@components/monaco"; +import { useSwarm } from "@components/resources/swarm"; + +export default function SwarmConfigPage() { + const { id, config: __config } = useParams() as { + id: string; + config: string; + }; + const _config = decodeURIComponent(__config); + const swarm = useSwarm(id); + const { data, isPending } = useRead("InspectSwarmConfig", { + swarm: id, + config: _config, + }); + const config = data?.[0]; + useSetTitle( + `${swarm?.name} | Config | ${config?.Spec?.Name ?? config?.ID ?? "Unknown"}` + ); + const nav = useNavigate(); + + if (isPending) { + return ( +
+ +
+ ); + } + + if (!config) { + return
Failed to inspect config.
; + } + + return ( +
+ {/* HEADER */} +
+ {/* BACK */} +
+ +
+ + {/* TITLE */} +
+
+ +
+ +
+ + {/* INFO */} +
+ Swarm Config + +
+
+ + +
+ ); +} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 6a104ce19..83329e12c 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,5 +1,6 @@ import { Layout } from "@components/layouts"; import { LOGIN_TOKENS, useAuth, useUser } from "@lib/hooks"; +import SwarmConfigPage from "@pages/swarm/config"; import { Loader2 } from "lucide-react"; import { lazy, Suspense } from "react"; import { @@ -143,6 +144,10 @@ export const Router = () => { path=":id/swarm-secret/:secret" element={} /> + } + /> {/* Terminal Page */}