task view page with logs

This commit is contained in:
mbecker20
2025-11-23 23:45:28 -08:00
parent 7ff2dba82f
commit aec8fa2bf1
3 changed files with 227 additions and 121 deletions

View File

@@ -17,6 +17,9 @@ export const SwarmTasks = ({
_search: [string, Dispatch<SetStateAction<string>>];
}) => {
const [search, setSearch] = _search;
const nodes =
useRead("ListSwarmNodes", { swarm: id }, { refetchInterval: 10_000 })
.data ?? [];
const services =
useRead("ListSwarmServices", { swarm: id }, { refetchInterval: 10_000 })
.data ?? [];
@@ -27,6 +30,7 @@ export const SwarmTasks = ({
const tasks = _tasks.map((task) => {
return {
...task,
node: nodes.find((node) => task.NodeID === node.ID),
service: services.find((service) => task.ServiceID === service.ID),
};
});
@@ -89,6 +93,21 @@ export const SwarmTasks = ({
),
size: 200,
},
{
accessorKey: "node.Hostname",
header: ({ column }) => (
<SortableHeader column={column} title="Node" />
),
cell: ({ row }) => (
<SwarmLink
type="Node"
swarm_id={id}
resource_id={row.original.node?.ID}
name={row.original.node?.Hostname}
/>
),
size: 200,
},
{
accessorKey: "State",
header: ({ column }) => (

View File

@@ -3,7 +3,6 @@ import {
ResourceLink,
ResourcePageHeader,
} from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import {
useLocalStorage,
usePermissions,
@@ -12,7 +11,7 @@ import {
} from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, Loader2 } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
import { ExportButton } from "@components/export";
@@ -27,99 +26,29 @@ import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
import { SwarmServiceLogs } from "./log";
import { Section } from "@components/layouts";
function SwarmServicePage() {
export default function SwarmServicePage() {
const { id, service: __service } = useParams() as {
id: string;
service: string;
};
const _service = decodeURIComponent(__service);
const swarm = useSwarm(id);
const { data: service, isPending } = useRead("InspectSwarmService", {
const { data: services, isPending } = useRead("ListSwarmServices", {
swarm: id,
service: _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>;
}
const Icon = SWARM_ICONS.Service;
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">
<Icon size={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>
);
}
export default function SwarmServicePage2() {
const { id, service: __service } = useParams() as {
id: string;
service: string;
};
const _service = decodeURIComponent(__service);
const swarm = useSwarm(id);
const { canWrite } = usePermissions({
type: "Swarm",
id,
});
const { data: service, isPending } = useRead("InspectSwarmService", {
swarm: id,
service: _service,
});
const service = services?.find((service) => service.ID === _service);
const tasks =
useRead("ListSwarmTasks", {
swarm: id,
}).data?.filter((task) => service?.ID && task.ServiceID === service.ID) ??
[];
const { canWrite } = usePermissions({
type: "Swarm",
id,
});
useSetTitle(
`${swarm?.name} | Service | ${service?.Spec?.Name ?? service?.ID ?? "Unknown"}`
`${swarm?.name} | Service | ${service?.Name ?? service?.ID ?? "Unknown"}`
);
const nav = useNavigate();
if (isPending) {
return (
@@ -161,7 +90,7 @@ export default function SwarmServicePage2() {
intent={intention}
icon={<Icon size={8} className={strokeColor} />}
resource={undefined}
name={service.Spec?.Name}
name={service.Name}
state={state}
status={`${tasks.length} Tasks`}
/>
@@ -201,7 +130,7 @@ const SwarmServiceTabs = ({
service: string;
}) => {
const [_view, setView] = useLocalStorage<SwarmServiceTabsView>(
`swarm-${swarm.id}-${service}-tabs-v2`,
`swarm-${swarm.id}-service-${service}-tabs-v2`,
"Log"
);
const { specificLogs, specificInspect } = usePermissions({
@@ -296,8 +225,7 @@ const get_service_state_from_tasks = (
tasks: Types.SwarmTaskListItem[]
): Types.SwarmState => {
for (const task of tasks) {
const current = task.State?.split(" ")[0];
if (current !== task.DesiredState) {
if (task.State !== task.DesiredState) {
return Types.SwarmState.Unhealthy;
}
}

View File

@@ -1,11 +1,30 @@
import { ResourceLink } from "@components/resources/common";
import { PageHeaderName } from "@components/util";
import { useRead, useSetTitle } from "@lib/hooks";
import {
ResourceDescription,
ResourceLink,
ResourcePageHeader,
} from "@components/resources/common";
import {
useLocalStorage,
usePermissions,
useRead,
useSetTitle,
} from "@lib/hooks";
import { Button } from "@ui/button";
import { ChevronLeft, Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import { MonacoEditor } from "@components/monaco";
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
import { SWARM_ICONS, SwarmLink, useSwarm } from "@components/resources/swarm";
import { Types } from "komodo_client";
import {
stroke_color_class_by_intention,
swarm_state_intention,
} from "@lib/color";
import { ReactNode, useMemo } from "react";
import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
import { SwarmServiceLogs } from "./log";
import { Section } from "@components/layouts";
import { ExportButton } from "@components/export";
import { ResourceNotifications } from "@pages/resource-notifications";
export default function SwarmTaskPage() {
const { id, task: __task } = useParams() as {
@@ -14,12 +33,21 @@ export default function SwarmTaskPage() {
};
const _task = decodeURIComponent(__task);
const swarm = useSwarm(id);
const { data: task, isPending } = useRead("InspectSwarmTask", {
const { data: tasks, isPending } = useRead("ListSwarmTasks", {
swarm: id,
task: _task,
});
const task = tasks?.find((task) => task.ID === _task);
const node = useRead("ListSwarmNodes", { swarm: id }).data?.find(
(node) => node.ID === task?.NodeID
);
const service = useRead("ListSwarmServices", { swarm: id }).data?.find(
(service) => service.ID === task?.ServiceID
);
const { canWrite } = usePermissions({
type: "Swarm",
id,
});
useSetTitle(`${swarm?.name} | Task | ${task?.ID ?? "Unknown"}`);
const nav = useNavigate();
if (isPending) {
return (
@@ -34,42 +62,173 @@ export default function SwarmTaskPage() {
}
const Icon = SWARM_ICONS.Task;
const state =
task.State === task.DesiredState
? Types.SwarmState.Healthy
: Types.SwarmState.Unhealthy;
const intention = swarm_state_intention(state);
const strokeColor = stroke_color_class_by_intention(intention);
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
<div>
<div className="w-full flex items-center justify-between mb-12">
<Link to={"/swarms/" + id}>
<Button className="gap-2" variant="secondary">
<ChevronLeft className="w-4" />
Back
</Button>
</div>
{/* TITLE */}
</Link>
<div className="flex items-center gap-4">
<div className="mt-1">
<Icon size={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} />
<ExportButton targets={[{ type: "Swarm", id }]} />
</div>
</div>
<div className="flex flex-col xl:flex-row gap-4">
{/* HEADER */}
<div className="w-full flex flex-col gap-4">
<div className="flex flex-col gap-2 border rounded-md">
<ResourcePageHeader
type={undefined}
id={undefined}
intent={intention}
icon={<Icon size={8} className={strokeColor} />}
resource={undefined}
name={task.ID}
state={state}
status={task.State}
/>
<div className="flex flex-col pb-2 px-4">
<div className="flex items-center gap-x-4 gap-y-0 flex-wrap text-muted-foreground">
<div>Swarm Task</div>
|
<ResourceLink type="Swarm" id={id} />
|
<SwarmLink
type="Service"
swarm_id={id}
resource_id={service?.ID}
name={service?.Name}
/>
|
<SwarmLink
type="Node"
swarm_id={id}
resource_id={node?.ID}
name={node?.Hostname}
/>
</div>
</div>
</div>
<ResourceDescription type="Swarm" id={id} disabled={!canWrite} />
</div>
{/** NOTIFICATIONS */}
<ResourceNotifications type="Swarm" id={id} />
</div>
<MonacoEditor
value={JSON.stringify(task, null, 2)}
language="json"
readOnly
/>
<div className="mt-8 flex flex-col gap-12">
{/* Actions */}
{/* Tabs */}
<div className="pt-4">
{swarm && <SwarmTaskTabs swarm={swarm} task={_task} />}
</div>
</div>
</div>
);
}
type SwarmTaskTabsView = "Log" | "Inspect";
const SwarmTaskTabs = ({
swarm,
task,
}: {
swarm: Types.SwarmListItem;
task: string;
}) => {
const [_view, setView] = useLocalStorage<SwarmTaskTabsView>(
`swarm-${swarm.id}-task-${task}-tabs-v2`,
"Log"
);
const { specificLogs, specificInspect } = usePermissions({
type: "Swarm",
id: swarm.id,
});
const view = !specificInspect && _view === "Inspect" ? "Log" : _view;
const tabs = useMemo(
() => [
{
value: "Log",
disabled: !specificLogs,
},
{
value: "Inspect",
disabled: !specificInspect,
},
],
[specificLogs, specificInspect]
);
const Selector = (
<MobileFriendlyTabsSelector
tabs={tabs}
value={view}
onValueChange={setView as any}
tabsTriggerClassname="w-[110px]"
/>
);
switch (view) {
case "Log":
return (
<SwarmServiceLogs
id={swarm.id}
service={task}
titleOther={Selector}
disabled={!specificLogs}
/>
);
case "Inspect":
return (
<SwarmTaskInspect swarm={swarm.id} task={task} titleOther={Selector} />
);
}
};
const SwarmTaskInspect = ({
swarm,
task,
titleOther,
}: {
swarm: string;
task: string;
titleOther: ReactNode;
}) => {
const { data: inspect, isPending } = useRead("InspectSwarmTask", {
swarm,
task,
});
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 (
<Section titleOther={titleOther}>
<MonacoEditor
value={JSON.stringify(inspect, null, 2)}
language="json"
readOnly
/>
</Section>
);
};