feat(web): patching API client, status vocabulary and navigation
This commit is contained in:
@@ -176,6 +176,14 @@ function TokenIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function PatchIcon() {
|
||||
return (
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6l-1.5 3h3l-1.5 3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const navGroups: NavGroup[] = [
|
||||
{
|
||||
label: "Fleet",
|
||||
@@ -184,6 +192,7 @@ const navGroups: NavGroup[] = [
|
||||
{ href: "/workloads", label: "Workloads", icon: <WorkloadIcon /> },
|
||||
{ href: "/monitors", label: "Monitors", icon: <MonitorIcon /> },
|
||||
{ href: "/vulnerabilities", label: "Vulnerabilities", icon: <ShieldIcon /> },
|
||||
{ href: "/patching", label: "Patching", icon: <PatchIcon /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { PatchRunStatus, PatchServerStatus } from "@/lib/api";
|
||||
|
||||
type Variant = "success" | "warning" | "danger" | "neutral" | "accent";
|
||||
|
||||
/*
|
||||
* One place that says how every patch state reads. Badge adds a dot to the
|
||||
* three state variants, so each label here is also readable without colour.
|
||||
*/
|
||||
export const RUN_STATUS: Record<PatchRunStatus, { label: string; variant: Variant }> = {
|
||||
running: { label: "running", variant: "accent" },
|
||||
succeeded: { label: "succeeded", variant: "success" },
|
||||
partial: { label: "partial", variant: "warning" },
|
||||
failed: { label: "failed", variant: "danger" },
|
||||
cancelled: { label: "cancelled", variant: "neutral" },
|
||||
};
|
||||
|
||||
export const SERVER_STATUS: Record<PatchServerStatus, { label: string; variant: Variant }> = {
|
||||
queued: { label: "queued", variant: "neutral" },
|
||||
waiting_offline: { label: "waiting for agent", variant: "warning" },
|
||||
patching: { label: "patching", variant: "accent" },
|
||||
rebooting: { label: "rebooting", variant: "accent" },
|
||||
succeeded: { label: "succeeded", variant: "success" },
|
||||
failed: { label: "failed", variant: "danger" },
|
||||
unsupported: { label: "unsupported", variant: "warning" },
|
||||
agent_too_old: { label: "agent too old", variant: "warning" },
|
||||
missed_offline: { label: "missed, offline", variant: "danger" },
|
||||
window_closed: { label: "window closed", variant: "danger" },
|
||||
cancelled: { label: "cancelled", variant: "neutral" },
|
||||
};
|
||||
|
||||
/*
|
||||
* Mirrors patchrun.MinAgentVersion and AgentSupportsPatchResults in the
|
||||
* server. The server is the authority; this only lets the policy editor warn
|
||||
* before saving. Change both together.
|
||||
*/
|
||||
export const MIN_AGENT_VERSION = "1.4.0";
|
||||
|
||||
export function agentSupportsPatchResults(version?: string): boolean {
|
||||
const m = /^v?(\d+)\.(\d+)\.(\d+)(-[^+]+)?/.exec((version ?? "").trim());
|
||||
if (!m) return false;
|
||||
const have = [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||
const want = MIN_AGENT_VERSION.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (have[i] !== want[i]) return have[i] > want[i];
|
||||
}
|
||||
return !m[4];
|
||||
}
|
||||
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
/** Plain words for the common shapes; anything else shows the expression itself. */
|
||||
export function describeCron(cron: string): string {
|
||||
const f = cron.trim().split(/\s+/);
|
||||
if (f.length !== 5 || !/^\d+$/.test(f[0]) || !/^\d+$/.test(f[1])) return cron;
|
||||
const at = `${f[1].padStart(2, "0")}:${f[0].padStart(2, "0")}`;
|
||||
const [, , dom, mon, dow] = f;
|
||||
if (dom === "*" && mon === "*" && dow === "*") return `Daily at ${at}`;
|
||||
if (dom === "*" && mon === "*" && /^[0-6]$/.test(dow)) return `${DAYS[Number(dow)]}s at ${at}`;
|
||||
if (/^\d+$/.test(dom) && mon === "*" && dow === "*") return `Day ${dom} of each month at ${at}`;
|
||||
return cron;
|
||||
}
|
||||
|
||||
export function formatDuration(minutes: number): string {
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = minutes % 60;
|
||||
if (h === 0) return `${m} min`;
|
||||
return m === 0 ? `${h} h` : `${h} h ${m} min`;
|
||||
}
|
||||
Reference in New Issue
Block a user