mirror of
https://github.com/moghtech/komodo.git
synced 2026-09-07 00:01:00 +00:00
improve swarm node page
This commit is contained in:
@@ -1,13 +1,31 @@
|
||||
import { ResourceLink } from "@components/resources/common";
|
||||
import { PageHeaderName } from "@components/util";
|
||||
import { usePermissions, 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, Zap } from "lucide-react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { ChevronLeft, Loader2 } from "lucide-react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { MonacoEditor } from "@components/monaco";
|
||||
import { SWARM_ICONS, useSwarm } from "@components/resources/swarm";
|
||||
import { Section } from "@components/layouts";
|
||||
import { ExportButton } from "@components/export";
|
||||
import {
|
||||
stroke_color_class_by_intention,
|
||||
swarm_node_state_intention,
|
||||
} from "@lib/color";
|
||||
import { ResourceNotifications } from "@pages/resource-notifications";
|
||||
import { Types } from "komodo_client";
|
||||
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from "react";
|
||||
import { MobileFriendlyTabsSelector } from "@ui/mobile-friendly-tabs";
|
||||
import { RemoveSwarmResource } from "./remove";
|
||||
import { SwarmTasksTable } from "@components/resources/swarm/table";
|
||||
|
||||
export default function SwarmNodePage() {
|
||||
const { id, node: __node } = useParams() as {
|
||||
@@ -20,14 +38,14 @@ export default function SwarmNodePage() {
|
||||
swarm: id,
|
||||
node: _node,
|
||||
});
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Node | ${node?.Spec?.Name ?? node?.Description?.Hostname ?? node?.ID ?? "Unknown"}`
|
||||
);
|
||||
const { canExecute } = usePermissions({
|
||||
const state = node?.Status?.State;
|
||||
const { canWrite, canExecute } = usePermissions({
|
||||
type: "Swarm",
|
||||
id,
|
||||
});
|
||||
const nav = useNavigate();
|
||||
useSetTitle(
|
||||
`${swarm?.name} | Node | ${node?.Description?.Hostname ?? "Unknown"}`
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
@@ -42,57 +60,194 @@ export default function SwarmNodePage() {
|
||||
}
|
||||
|
||||
const Icon = SWARM_ICONS.Node;
|
||||
const intent = swarm_node_state_intention(state);
|
||||
|
||||
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={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} />
|
||||
<ExportButton targets={[{ type: "Swarm", id }]} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canExecute && node.ID && (
|
||||
<Section title="Execute" icon={<Zap className="w-4 h-4" />}>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Node"
|
||||
resource_id={node.ID}
|
||||
resource_name={node.Description?.Hostname}
|
||||
<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={intent}
|
||||
icon={
|
||||
<Icon
|
||||
size={8}
|
||||
className={stroke_color_class_by_intention(intent)}
|
||||
/>
|
||||
}
|
||||
resource={undefined}
|
||||
name={node.Description?.Hostname}
|
||||
state={state}
|
||||
status={node.Spec?.Role}
|
||||
/>
|
||||
<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 Node</div>
|
||||
|
|
||||
<ResourceLink type="Swarm" id={id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
<ResourceDescription type="Swarm" id={id} disabled={!canWrite} />
|
||||
</div>
|
||||
{/** NOTIFICATIONS */}
|
||||
<ResourceNotifications type="Swarm" id={id} />
|
||||
</div>
|
||||
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(node, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
<div className="mt-8 flex flex-col gap-12">
|
||||
{/* Actions */}
|
||||
{canExecute && node.ID && (
|
||||
<RemoveSwarmResource
|
||||
id={id}
|
||||
type="Node"
|
||||
resource_id={node.ID}
|
||||
resource_name={node.Description?.Hostname}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="pt-4">
|
||||
{swarm && <SwarmNodeTabs swarm={swarm} _node={_node} node={node} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SwarmNodeTabsView = "Tasks" | "Inspect";
|
||||
|
||||
const SwarmNodeTabs = ({
|
||||
swarm,
|
||||
_node,
|
||||
node,
|
||||
}: {
|
||||
swarm: Types.SwarmListItem;
|
||||
_node: string;
|
||||
node: Types.SwarmNode;
|
||||
}) => {
|
||||
const [_view, setView] = useLocalStorage<SwarmNodeTabsView>(
|
||||
`swarm-${swarm.id}-node-${node}-tabs-v1`,
|
||||
"Tasks"
|
||||
);
|
||||
const { specificInspect } = usePermissions({
|
||||
type: "Swarm",
|
||||
id: swarm.id,
|
||||
});
|
||||
const _search = useState("");
|
||||
|
||||
const view = !specificInspect && _view === "Inspect" ? "Tasks" : _view;
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: "Tasks",
|
||||
},
|
||||
{
|
||||
value: "Inspect",
|
||||
disabled: !specificInspect,
|
||||
},
|
||||
],
|
||||
[specificInspect]
|
||||
);
|
||||
|
||||
const Selector = (
|
||||
<MobileFriendlyTabsSelector
|
||||
tabs={tabs}
|
||||
value={view}
|
||||
onValueChange={setView as any}
|
||||
tabsTriggerClassname="w-[110px]"
|
||||
/>
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case "Tasks":
|
||||
return (
|
||||
<SwarmNodeTasks
|
||||
id={swarm.id}
|
||||
node_id={node.ID}
|
||||
Selector={Selector}
|
||||
_search={_search}
|
||||
/>
|
||||
);
|
||||
case "Inspect":
|
||||
return (
|
||||
<SwarmNodeInspect swarm={swarm.id} node={_node} titleOther={Selector} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SwarmNodeTasks = ({
|
||||
id,
|
||||
node_id,
|
||||
Selector,
|
||||
_search,
|
||||
}: {
|
||||
id: string;
|
||||
node_id: string | undefined;
|
||||
Selector: ReactNode;
|
||||
_search: [string, Dispatch<SetStateAction<string>>];
|
||||
}) => {
|
||||
const tasks =
|
||||
useRead("ListSwarmTasks", { swarm: id }).data?.filter(
|
||||
(service) => node_id && service.NodeID === node_id
|
||||
) ?? [];
|
||||
return (
|
||||
<SwarmTasksTable
|
||||
id={id}
|
||||
tasks={tasks}
|
||||
_search={_search}
|
||||
titleOther={Selector}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SwarmNodeInspect = ({
|
||||
swarm,
|
||||
node,
|
||||
titleOther,
|
||||
}: {
|
||||
swarm: string;
|
||||
node: string;
|
||||
titleOther: ReactNode;
|
||||
}) => {
|
||||
const { data: inspect, isPending } = useRead("InspectSwarmNode", {
|
||||
swarm,
|
||||
node,
|
||||
});
|
||||
|
||||
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 (
|
||||
<Section titleOther={titleOther}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(inspect, null, 2)}
|
||||
language="json"
|
||||
readOnly
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user