swarm configs view

This commit is contained in:
mbecker20
2025-11-21 13:21:14 -08:00
parent 8e081fd09c
commit 32d48cdb02
10 changed files with 186 additions and 9 deletions
+1 -1
View File
@@ -122,7 +122,7 @@ impl Resolve<super::Args> for InspectSwarmConfig {
async fn resolve(
self,
_: &super::Args,
) -> anyhow::Result<SwarmConfig> {
) -> anyhow::Result<Vec<SwarmConfig>> {
inspect_swarm_config(&self.id).await
}
}
+7 -3
View File
@@ -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<SwarmConfig> {
) -> anyhow::Result<Vec<SwarmConfig>> {
let res = run_komodo_standard_command(
"Inspect Swarm Config",
None,
+1 -1
View File
@@ -260,4 +260,4 @@ pub struct InspectSwarmConfig {
}
#[typeshare]
pub type InspectSwarmConfigResponse = SwarmConfig;
pub type InspectSwarmConfigResponse = Vec<SwarmConfig>;
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -67,7 +67,7 @@ pub struct InspectSwarmSecret {
// ========
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(SwarmConfig)]
#[response(Vec<SwarmConfig>)]
#[error(anyhow::Error)]
pub struct InspectSwarmConfig {
pub id: String,
+1 -1
View File
@@ -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 {
/**
@@ -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<SetStateAction<string>>];
}) => {
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 (
<Section
titleOther={titleOther}
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="relative">
<Search className="w-4 absolute top-[50%] left-3 -translate-y-[50%] text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="search..."
className="pl-8 w-[200px] lg:w-[300px]"
/>
</div>
</div>
}
>
<DataTable
containerClassName="min-h-[60vh]"
tableKey="swarm-configs"
data={filtered}
columns={[
{
accessorKey: "Name",
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
cell: ({ row }) => (
<Link
to={`/swarms/${id}/swarm-config/${row.original.ID}`}
className="flex gap-2 items-center hover:underline"
>
<KeyRound className="w-4 h-4" />
{row.original.Name ?? "Unknown"}
</Link>
),
size: 200,
},
{
accessorKey: "ID",
header: ({ column }) => (
<SortableHeader column={column} title="Id" />
),
cell: ({ row }) => row.original.ID ?? "Unknown",
size: 200,
},
]}
/>
</Section>
);
};
@@ -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 <SwarmTasks id={id} titleOther={Selector} _search={_search} />;
case "Secrets":
return <SwarmSecrets id={id} titleOther={Selector} _search={_search} />;
case "Configs":
return <SwarmConfigs id={id} titleOther={Selector} _search={_search} />;
}
};
+76
View File
@@ -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 (
<div className="flex justify-center w-full py-4">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
);
}
if (!config) {
return <div className="flex w-full py-4">Failed to inspect config.</div>;
}
return (
<div className="flex flex-col gap-16 mb-24">
{/* HEADER */}
<div className="flex flex-col gap-4">
{/* BACK */}
<div className="flex items-center justify-between mb-4">
<Button
className="gap-2"
variant="secondary"
onClick={() => nav("/swarms/" + id)}
>
<ChevronLeft className="w-4" /> Back
</Button>
</div>
{/* TITLE */}
<div className="flex items-center gap-4">
<div className="mt-1">
<KeyRound className="w-8 h-8" />
</div>
<PageHeaderName name={config?.Spec?.Name ?? config?.ID} />
</div>
{/* INFO */}
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
Swarm Config
<ResourceLink type="Swarm" id={id} />
</div>
</div>
<MonacoEditor
value={JSON.stringify(config, null, 2)}
language="json"
readOnly
/>
</div>
);
}
+5
View File
@@ -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={<SwarmSecretPage />}
/>
<Route
path=":id/swarm-config/:config"
element={<SwarmConfigPage />}
/>
{/* Terminal Page */}
<Route
path=":id/terminal/:terminal"