mirror of
https://github.com/moghtech/komodo.git
synced 2026-08-24 00:00:16 +00:00
swarm configs view
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -260,4 +260,4 @@ pub struct InspectSwarmConfig {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
pub type InspectSwarmConfigResponse = SwarmConfig;
|
||||
pub type InspectSwarmConfigResponse = Vec<SwarmConfig>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Vendored
+1
-1
@@ -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} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user