mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
task view page with logs
This commit is contained in:
@@ -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 }) => (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user