feat(web): show patch policy coverage on servers and open the run after Apply updates
This commit is contained in:
@@ -130,7 +130,13 @@ export default function ServerDetailPage() {
|
||||
|
||||
const { mutate: applyUpdates, isPending: isApplying } = useMutation({
|
||||
mutationFn: () => api.applyUpdates(serverId),
|
||||
onSuccess: () => toast.success("Update command sent. Patching runs in the background and may take several minutes."),
|
||||
onSuccess: (res) => {
|
||||
if (res.run_id) {
|
||||
router.push(`/patching/runs/${res.run_id}`);
|
||||
} else {
|
||||
toast.success("Update command sent.");
|
||||
}
|
||||
},
|
||||
onError: toast.error,
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { Button, Card, Pagination, usePagination, useToast } from "@/components/ui";
|
||||
@@ -42,6 +43,7 @@ const FIX_FILTERS: { key: string; label: string; hasFix: boolean | undefined }[]
|
||||
export default function VulnerabilitiesPage() {
|
||||
const { isAdmin } = useAuth();
|
||||
const qc = useQueryClient();
|
||||
const router = useRouter();
|
||||
|
||||
const [state, setState] = useState<FindingState>("open");
|
||||
const [severity, setSeverity] = useState<Severity | "">("");
|
||||
@@ -112,10 +114,10 @@ export default function VulnerabilitiesPage() {
|
||||
});
|
||||
|
||||
const applyUpdates = useMutation({
|
||||
mutationFn: (serverId: string) => api.applyUpdates(serverId),
|
||||
// This one had no feedback of any kind: the button dispatched a patch
|
||||
// run to a whole server and the page did not change in any way.
|
||||
onSuccess: (_data, serverId) => toast.success(`Update command sent to ${serverName(serverId)}.`),
|
||||
mutationFn: (serverId: string) => api.applyUpdates(serverId, "vulnerabilities"),
|
||||
onSuccess: (res) => {
|
||||
if (res.run_id) router.push(`/patching/runs/${res.run_id}`);
|
||||
},
|
||||
onError: toast.error,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, ServerWithKeys } from "@/lib/api";
|
||||
import { Badge, Button, Card, ConfirmDialog, Table, Tbody, Td, Th, Thead, Tr } from "@/components/ui";
|
||||
import { matchesTags } from "@/lib/targets";
|
||||
import { RUN_STATUS } from "@/components/patching/status";
|
||||
|
||||
/*
|
||||
* Everything that changes what is installed on the machine: its OS packages,
|
||||
@@ -42,6 +46,17 @@ export function MaintenanceTab({
|
||||
const isWindows = server.os_info?.toLowerCase().includes("windows");
|
||||
const agentCurrent = !!latestVersion && !!server.agent_version && server.agent_version === latestVersion;
|
||||
|
||||
const { data: policies } = useQuery({ queryKey: ["patch-policies"], queryFn: () => api.listPatchPolicies() });
|
||||
const { data: windows } = useQuery({ queryKey: ["maintenance-windows"], queryFn: () => api.listMaintenanceWindows() });
|
||||
const { data: lastRuns } = useQuery({ queryKey: ["patch-runs", "server", server.server_id], queryFn: () => api.listPatchRuns({ server_id: server.server_id, limit: 1 }) });
|
||||
// Same selector rule as the scheduler: named, or carrying every tag.
|
||||
const covering = (policies ?? []).filter((p) => p.enabled && (p.target_server_ids.includes(server.server_id) || matchesTags(server, p.target_tags ?? {})));
|
||||
const next = covering
|
||||
.filter((p) => p.next_run_at)
|
||||
.sort((a, b) => (a.next_run_at! < b.next_run_at! ? -1 : 1))[0];
|
||||
const nextWindow = next && windows?.find((w) => w.window_id === next.window_id);
|
||||
const lastRun = lastRuns?.[0];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||
<Card padding={false}>
|
||||
@@ -55,6 +70,24 @@ export function MaintenanceTab({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border px-6 py-3 text-xs text-text-secondary">
|
||||
{next && nextWindow ? (
|
||||
<span>
|
||||
Covered by <span className="text-text-primary">{next.name}</span>, next window{" "}
|
||||
{new Date(next.next_run_at!).toLocaleString(undefined, { timeZone: nextWindow.tz, dateStyle: "medium", timeStyle: "short" })} ({nextWindow.tz})
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
Not covered by any patch policy. <Link href="/patching" className="text-accent hover:underline">Set one up</Link>
|
||||
</span>
|
||||
)}
|
||||
{lastRun && (
|
||||
<Link href={`/patching/runs/${lastRun.run_id}`} className="flex items-center gap-2 hover:text-accent">
|
||||
Last run <Badge variant={RUN_STATUS[lastRun.status].variant}>{RUN_STATUS[lastRun.status].label}</Badge>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{updates.length === 0 ? (
|
||||
<p className="px-6 py-10 text-center text-sm text-text-secondary">
|
||||
{isWindows ? "No pending Windows updates. The agent checks hourly." : "No pending package updates. The agent checks hourly."}
|
||||
@@ -94,7 +127,7 @@ export function MaintenanceTab({
|
||||
<Button variant="primary" loading={isApplying} onClick={onApplyUpdates} disabled={server.status !== "active"} title={server.status !== "active" ? "Agent must be online to apply updates" : undefined}>
|
||||
Apply updates
|
||||
</Button>
|
||||
<p className="text-xs text-text-tertiary">Upgrade runs in the background and may take several minutes.</p>
|
||||
<p className="text-xs text-text-tertiary">Installs all pending updates now, without rebooting, and records a run.</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user