mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
Explore swarm info
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { RequiredResourceComponents } from "@types";
|
||||
import { Boxes } from "lucide-react";
|
||||
import { Component } from "lucide-react";
|
||||
import { DeleteResource, NewResource, ResourcePageHeader } from "../common";
|
||||
import { SwarmTable } from "./table";
|
||||
import {
|
||||
@@ -27,7 +27,7 @@ export const useFullSwarm = (id: string) =>
|
||||
const SwarmIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
const state = useSwarm(id)?.info.state;
|
||||
const color = stroke_color_class_by_intention(swarm_state_intention(state));
|
||||
return <Boxes className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
return <Component className={cn(`w-${size} h-${size}`, state && color)} />;
|
||||
};
|
||||
|
||||
export const SwarmComponents: RequiredResourceComponents = {
|
||||
|
||||
@@ -11,8 +11,9 @@ import { SwarmNodes } from "./nodes";
|
||||
import { SwarmSecrets } from "./secrets";
|
||||
import { SwarmServices } from "./services";
|
||||
import { SwarmTasks } from "./tasks";
|
||||
import { SwarmInspect } from "./inspect";
|
||||
|
||||
type SwarmInfoView = "Nodes" | "Services" | "Tasks" | "Secrets";
|
||||
type SwarmInfoView = "Inspect" | "Nodes" | "Services" | "Tasks" | "Secrets";
|
||||
|
||||
export const SwarmInfo = ({
|
||||
id,
|
||||
@@ -25,7 +26,7 @@ export const SwarmInfo = ({
|
||||
const state = useSwarm(id)?.info.state ?? Types.SwarmState.Unknown;
|
||||
const [view, setView] = useLocalStorage<SwarmInfoView>(
|
||||
"swarm-info-view-v1",
|
||||
"Nodes"
|
||||
"Inspect"
|
||||
);
|
||||
|
||||
if (state === Types.SwarmState.Unknown) {
|
||||
@@ -40,6 +41,9 @@ export const SwarmInfo = ({
|
||||
|
||||
const tabsNoContent = useMemo<TabNoContent<SwarmInfoView>[]>(
|
||||
() => [
|
||||
{
|
||||
value: "Inspect",
|
||||
},
|
||||
{
|
||||
value: "Nodes",
|
||||
},
|
||||
@@ -67,6 +71,8 @@ export const SwarmInfo = ({
|
||||
|
||||
const Component = () => {
|
||||
switch (view) {
|
||||
case "Inspect":
|
||||
return <SwarmInspect id={id} titleOther={Selector} />;
|
||||
case "Nodes":
|
||||
return <SwarmNodes id={id} titleOther={Selector} _search={_search} />;
|
||||
case "Services":
|
||||
|
||||
25
frontend/src/components/resources/swarm/info/inspect.tsx
Normal file
25
frontend/src/components/resources/swarm/info/inspect.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { ReactNode } from "react";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
|
||||
export const SwarmInspect = ({
|
||||
id,
|
||||
titleOther,
|
||||
}: {
|
||||
id: string;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const inspect =
|
||||
useRead("InspectSwarm", { swarm: id }, { refetchInterval: 10_000 }).data ??
|
||||
[];
|
||||
|
||||
return (
|
||||
<Section titleOther={titleOther}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(inspect, undefined, 2)}
|
||||
language="json"
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -2,9 +2,10 @@ import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Diamond, Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const SwarmNodes = ({
|
||||
id,
|
||||
@@ -45,15 +46,23 @@ export const SwarmNodes = ({
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="server-nodes"
|
||||
tableKey="swarm-nodes"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
accessorKey: "Description.Hostname",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Name" />
|
||||
<SortableHeader column={column} title="Hostname" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={`/swarms/${id}/swarm-node/${row.original.ID}`}
|
||||
className="flex gap-2 items-center hover:underline"
|
||||
>
|
||||
<Diamond className="w-4 h-4" />
|
||||
{row.original.Description?.Hostname ?? "Unknown"}
|
||||
</Link>
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
@@ -65,7 +74,7 @@ export const SwarmNodes = ({
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "Status",
|
||||
accessorKey: "Status.State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-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 SwarmSecrets = ({
|
||||
id,
|
||||
@@ -45,7 +46,7 @@ export const SwarmSecrets = ({
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="server-secrets"
|
||||
tableKey="swarm-secrets"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
@@ -53,7 +54,15 @@ export const SwarmSecrets = ({
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="ID" />
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={`/swarms/${id}/swarm-secret/${row.original.ID}`}
|
||||
className="flex gap-2 items-center hover:underline"
|
||||
>
|
||||
<KeyRound className="w-4 h-4" />
|
||||
{row.original.Spec?.Name ?? "Unknown"}
|
||||
</Link>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { FolderCode, Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const SwarmServices = ({
|
||||
id,
|
||||
@@ -45,15 +46,23 @@ export const SwarmServices = ({
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="server-services"
|
||||
tableKey="swarm-services"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: "Name",
|
||||
accessorKey: "Spec.Name",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="ID" />
|
||||
<SortableHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={`/swarms/${id}/swarm-service/${row.original.ID}`}
|
||||
className="flex gap-2 items-center hover:underline"
|
||||
>
|
||||
<FolderCode className="w-4 h-4" />
|
||||
{row.original.Spec?.Name ?? "Unknown"}
|
||||
</Link>
|
||||
),
|
||||
cell: ({ row }) => row.original.Spec?.Name ?? "Unknown",
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
@@ -65,9 +74,9 @@ export const SwarmServices = ({
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "ServiceStatus",
|
||||
accessorKey: "UpdateStatus.State",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Status" />
|
||||
<SortableHeader column={column} title="State" />
|
||||
),
|
||||
cell: ({ row }) => row.original.UpdateStatus?.State ?? "Unknown",
|
||||
size: 200,
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Section } from "@components/layouts";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { DataTable, SortableHeader } from "@ui/data-table";
|
||||
import { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { ListTodo, Search } from "lucide-react";
|
||||
import { Input } from "@ui/input";
|
||||
import { filterBySplit } from "@lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const SwarmTasks = ({
|
||||
id,
|
||||
@@ -41,7 +42,7 @@ export const SwarmTasks = ({
|
||||
>
|
||||
<DataTable
|
||||
containerClassName="min-h-[60vh]"
|
||||
tableKey="server-tasks"
|
||||
tableKey="swarm-tasks"
|
||||
data={filtered}
|
||||
columns={[
|
||||
{
|
||||
@@ -49,7 +50,15 @@ export const SwarmTasks = ({
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Id" />
|
||||
),
|
||||
cell: ({ row }) => row.original.ID ?? "Unknown",
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to={`/swarms/${id}/swarm-task/${row.original.ID}`}
|
||||
className="flex gap-2 items-center hover:underline"
|
||||
>
|
||||
<ListTodo className="w-4 h-4" />
|
||||
{row.original.ID ?? "Unknown"}
|
||||
</Link>
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -782,7 +782,7 @@ export const StackServiceLink = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DockerResourcePageName = ({ name: _name }: { name?: string }) => {
|
||||
export const PageHeaderName = ({ name: _name }: { name?: string }) => {
|
||||
const name = _name ?? "Unknown";
|
||||
return (
|
||||
<h1
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DOCKER_LINK_ICONS,
|
||||
DockerContainersSection,
|
||||
DockerLabelsSection,
|
||||
DockerResourcePageName,
|
||||
PageHeaderName,
|
||||
ShowHideButton,
|
||||
} from "@components/util";
|
||||
import { fmt_date_with_minutes, format_size_bytes } from "@lib/formatting";
|
||||
@@ -126,7 +126,7 @@ const ImagePageInner = ({
|
||||
<div className="mt-1">
|
||||
<DOCKER_LINK_ICONS.image server_id={id} name={image.Id} size={8} />
|
||||
</div>
|
||||
<DockerResourcePageName name={image_name} />
|
||||
<PageHeaderName name={image_name} />
|
||||
{unused && <Badge variant="destructive">Unused</Badge>}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DockerLabelsSection,
|
||||
DockerOptions,
|
||||
DockerResourceLink,
|
||||
DockerResourcePageName,
|
||||
PageHeaderName,
|
||||
ShowHideButton,
|
||||
} from "@components/util";
|
||||
import { useExecute, usePermissions, useRead, useSetTitle } from "@lib/hooks";
|
||||
@@ -130,7 +130,7 @@ const NetworkPageInner = ({
|
||||
size={8}
|
||||
/>
|
||||
</div>
|
||||
<DockerResourcePageName name={network_name} />
|
||||
<PageHeaderName name={network_name} />
|
||||
{unused && <Badge variant="destructive">Unused</Badge>}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DockerContainersSection,
|
||||
DockerLabelsSection,
|
||||
DockerOptions,
|
||||
DockerResourcePageName,
|
||||
PageHeaderName,
|
||||
ShowHideButton,
|
||||
} from "@components/util";
|
||||
import { useExecute, usePermissions, useRead, useSetTitle } from "@lib/hooks";
|
||||
@@ -116,7 +116,7 @@ const VolumePageInner = ({
|
||||
size={8}
|
||||
/>
|
||||
</div>
|
||||
<DockerResourcePageName name={volume_name} />
|
||||
<PageHeaderName name={volume_name} />
|
||||
{containers && containers.length === 0 && (
|
||||
<Badge variant="destructive">Unused</Badge>
|
||||
)}
|
||||
75
frontend/src/pages/swarm/node.tsx
Normal file
75
frontend/src/pages/swarm/node.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, Diamond, 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 SwarmNodePage() {
|
||||
const { id, node: __node } = useParams() as {
|
||||
id: string;
|
||||
node: string;
|
||||
};
|
||||
const _node = decodeURIComponent(__node);
|
||||
const swarm = useSwarm(id);
|
||||
const { data, isPending } = useRead("ListSwarmNodes", { swarm: id });
|
||||
const node = data?.find((node) => node.ID === _node);
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Node | ${node?.Spec?.Name ?? node?.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 (!node) {
|
||||
return <div className="flex w-full py-4">Failed to inspect node.</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">
|
||||
<Diamond className="w-8 h-8" />
|
||||
</div>
|
||||
<PageHeaderName
|
||||
name={node.Spec?.Name ?? node.Description?.Hostname ?? node.ID}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* INFO */}
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
Swarm Node
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(node, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
frontend/src/pages/swarm/secret.tsx
Normal file
73
frontend/src/pages/swarm/secret.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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 SwarmSecretPage() {
|
||||
const { id, secret: __secret } = useParams() as {
|
||||
id: string;
|
||||
secret: string;
|
||||
};
|
||||
const _secret = decodeURIComponent(__secret);
|
||||
const swarm = useSwarm(id);
|
||||
const { data, isPending } = useRead("ListSwarmSecrets", { swarm: id });
|
||||
const secret = data?.find((secret) => secret.ID === _secret);
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Secret | ${secret?.Spec?.Name ?? secret?.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 (!secret) {
|
||||
return <div className="flex w-full py-4">Failed to inspect secret.</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={secret?.Spec?.Name ?? secret?.ID} />
|
||||
</div>
|
||||
|
||||
{/* INFO */}
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
Swarm Secret
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(secret, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
frontend/src/pages/swarm/service.tsx
Normal file
73
frontend/src/pages/swarm/service.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, FolderCode, 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 SwarmServicePage() {
|
||||
const { id, service: __service } = useParams() as {
|
||||
id: string;
|
||||
service: string;
|
||||
};
|
||||
const _service = decodeURIComponent(__service);
|
||||
const swarm = useSwarm(id);
|
||||
const { data, isPending } = useRead("ListSwarmServices", { swarm: id });
|
||||
const service = data?.find((service) => service.ID === _service);
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Service | ${service?.Spec?.Name ?? service?.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 (!service) {
|
||||
return <div className="flex w-full py-4">Failed to inspect service.</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">
|
||||
<FolderCode className="w-8 h-8" />
|
||||
</div>
|
||||
<PageHeaderName name={service.Spec?.Name ?? service.ID} />
|
||||
</div>
|
||||
|
||||
{/* INFO */}
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
Swarm Service
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(service, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
frontend/src/pages/swarm/task.tsx
Normal file
71
frontend/src/pages/swarm/task.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { ChevronLeft, ListTodo, 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 SwarmTaskPage() {
|
||||
const { id, task: __task } = useParams() as {
|
||||
id: string;
|
||||
task: string;
|
||||
};
|
||||
const _task = decodeURIComponent(__task);
|
||||
const swarm = useSwarm(id);
|
||||
const { data, isPending } = useRead("ListSwarmTasks", { swarm: id });
|
||||
const task = data?.find((task) => task.ID === _task);
|
||||
useSetTitle(`${swarm?.name} | Task | ${task?.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 (!task) {
|
||||
return <div className="flex w-full py-4">Failed to inspect task.</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">
|
||||
<ListTodo className="w-8 h-8" />
|
||||
</div>
|
||||
<PageHeaderName name={task.ID} />
|
||||
</div>
|
||||
|
||||
{/* INFO */}
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground">
|
||||
Swarm Task
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(task, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -24,14 +24,18 @@ const UserPage = lazy(() => import("@pages/user"));
|
||||
const UserGroupPage = lazy(() => import("@pages/user-group"));
|
||||
const Settings = lazy(() => import("@pages/settings"));
|
||||
const StackServicePage = lazy(() => import("@pages/stack-service"));
|
||||
const NetworkPage = lazy(() => import("@pages/server-info/network"));
|
||||
const ImagePage = lazy(() => import("@pages/server-info/image"));
|
||||
const VolumePage = lazy(() => import("@pages/server-info/volume"));
|
||||
const ContainerPage = lazy(() => import("@pages/server-info/container"));
|
||||
const NetworkPage = lazy(() => import("@pages/docker/network"));
|
||||
const ImagePage = lazy(() => import("@pages/docker/image"));
|
||||
const VolumePage = lazy(() => import("@pages/docker/volume"));
|
||||
const ContainerPage = lazy(() => import("@pages/docker/container"));
|
||||
const ContainersPage = lazy(() => import("@pages/containers"));
|
||||
const TerminalsPage = lazy(() => import("@pages/terminals"));
|
||||
const TerminalPage = lazy(() => import("@pages/terminal"));
|
||||
const SchedulesPage = lazy(() => import("@pages/schedules"));
|
||||
const SwarmNodePage = lazy(() => import("@pages/swarm/node"));
|
||||
const SwarmServicePage = lazy(() => import("@pages/swarm/service"));
|
||||
const SwarmTaskPage = lazy(() => import("@pages/swarm/task"));
|
||||
const SwarmSecretPage = lazy(() => import("@pages/swarm/secret"));
|
||||
|
||||
const sanitize_query = (search: URLSearchParams) => {
|
||||
search.delete("token");
|
||||
@@ -101,6 +105,7 @@ export const Router = () => {
|
||||
<Route path="alerts" element={<AlertsPage />} />
|
||||
<Route path="user-groups/:id" element={<UserGroupPage />} />
|
||||
<Route path="users/:id" element={<UserPage />} />
|
||||
{/* Updates */}
|
||||
<Route path="updates">
|
||||
<Route path="" element={<UpdatesPage />} />
|
||||
<Route path=":id" element={<UpdatePage />} />
|
||||
@@ -121,7 +126,24 @@ export const Router = () => {
|
||||
<Route path=":id/network/:network" element={<NetworkPage />} />
|
||||
<Route path=":id/image/:image" element={<ImagePage />} />
|
||||
<Route path=":id/volume/:volume" element={<VolumePage />} />
|
||||
{/* TerminalPage */}
|
||||
{/* Swarm Resource */}
|
||||
<Route
|
||||
path=":id/swarm-node/:node"
|
||||
element={<SwarmNodePage />}
|
||||
/>
|
||||
<Route
|
||||
path=":id/swarm-service/:service"
|
||||
element={<SwarmServicePage />}
|
||||
/>
|
||||
<Route
|
||||
path=":id/swarm-task/:task"
|
||||
element={<SwarmTaskPage />}
|
||||
/>
|
||||
<Route
|
||||
path=":id/swarm-secret/:secret"
|
||||
element={<SwarmSecretPage />}
|
||||
/>
|
||||
{/* Terminal Page */}
|
||||
<Route
|
||||
path=":id/terminal/:terminal"
|
||||
element={<TerminalPage />}
|
||||
|
||||
Reference in New Issue
Block a user