spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1"use client";23import Link from "next/link";4import { useMemo } from "react";5import type { EventDTO } from "@dci/core";6import { Badge, Button, EmptyState, EventBadge, RelativeTime, SkeletonRows } from "@/components/ui";7import { cn } from "@/components/ui/cn";8import { fmtInt, fmtValue } from "@/lib/format";9import { EVENT_LABEL } from "@/lib/labels";10import { entityHref } from "@/lib/routes";11import { ErrorNote, ExtLink, Input, Pager, RefreshButton, ReviewBadge, Segmented, Select } from "./primitives";12import { AdminPageHeader } from "./shell";13import { useToast } from "./toast";14import { useAdminMutation, useAdminQuery } from "./use-admin-query";15import { useUrlState } from "./use-url-state";1617const REVIEW = ["all", "pending", "approved", "auto"] as const;18type Review = (typeof REVIEW)[number];19const PER_PAGE = 50;2021export function EventsReview() {22 const [params, setParams] = useUrlState();23 const review = (REVIEW.includes(params.review as Review) ? params.review : "all") as Review;24 const page = Math.max(1, Number(params.page ?? 1) || 1);25 const type = params.type ?? "";26 const minSig = params.min_significance ?? "";27 const q = useAdminQuery<EventDTO[]>("v1/events", { query: { type: type || undefined, min_significance: minSig || undefined, page, per_page: PER_PAGE }, refreshMs: 60_000 });28 const { busy, run } = useAdminMutation();29 const toast = useToast();30 const total = Number(q.meta?.total ?? 0);31 const rows = useMemo(() => (q.data ?? []).filter((e) => review === "all" || e.reviewStatus === review), [q.data, review]);32 const counts = useMemo(() => {33 const c: Record<string, number> = {};34 for (const e of q.data ?? []) c[e.reviewStatus] = (c[e.reviewStatus] ?? 0) + 1;35 return c;36 }, [q.data]);3738 const setStatus = async (e: EventDTO, reviewStatus: "approved" | "rejected" | "pending") => {39 const prev = e.reviewStatus;40 q.mutate((list) => list?.map((x) => (x.id === e.id ? { ...x, reviewStatus } : x)) ?? list);41 const r = await run<{ id: string; reviewStatus: string }>(`${e.id}:${reviewStatus}`, `events/${encodeURIComponent(e.id)}`, { method: "PATCH", body: { reviewStatus } });42 if (r.ok) toast({ tone: "ok", title: `Event ${reviewStatus}`, description: e.title });43 else {44 q.mutate((list) => list?.map((x) => (x.id === e.id ? { ...x, reviewStatus: prev } : x)) ?? list);45 toast({ tone: "danger", title: "Review not saved", description: r.error });46 }47 };4849 return (50 <div>51 <AdminPageHeader title="Events review" description="Recent change-feed events with their review state. Rejected events disappear from the public feed (and from this list, which reads the public endpoint)." aside={<RefreshButton onClick={() => void q.refresh()} busy={q.refreshing} updatedAt={q.updatedAt} />} />52 <ErrorNote error={q.error && !q.data ? q.error : null} onRetry={() => void q.refresh()} className="mb-3" />53 <div className="mb-3 flex flex-col gap-2 lg:flex-row lg:items-center">54 <Segmented label="Review" value={review} onChange={(v) => setParams({ review: v === "all" ? null : v })} options={REVIEW.map((r) => ({ value: r, label: r === "all" ? "All (page)" : r[0]!.toUpperCase() + r.slice(1), count: r === "all" ? (q.data?.length ?? null) : (counts[r] ?? 0) }))} />55 <div className="flex flex-1 gap-2">56 <Select value={type} onChange={(e) => setParams({ type: e.target.value || null, page: null })} className="sm:w-[220px]" aria-label="Event type">57 <option value="">All event types</option>58 {Object.entries(EVENT_LABEL).map(([k, v]) => (59 <option key={k} value={k}>60 {v}61 </option>62 ))}63 </Select>64 <Input type="number" min={0} max={100} value={minSig} onChange={(e) => setParams({ min_significance: e.target.value || null, page: null })} placeholder="min significance" className="w-[150px]" mono />65 </div>66 </div>6768 {q.loading && !q.data ? (69 <SkeletonRows rows={10} />70 ) : rows.length === 0 ? (71 <EmptyState compact title="No events" description={review !== "all" ? "No events with this review state on the current page." : undefined} />72 ) : (73 <div className="rounded-sm border border-hair bg-surface">74 <ul className="divide-y divide-hair">75 {rows.map((e) => (76 <li key={e.id} className="flex flex-col gap-2 px-3 py-2.5 sm:flex-row sm:items-start">77 <div className="flex w-full shrink-0 flex-wrap items-center gap-1.5 sm:w-[210px] sm:flex-col sm:items-start">78 <EventBadge type={e.eventType} size="xs" />79 <ReviewBadge status={e.reviewStatus} />80 <span className="text-[11.5px] text-ink-3">81 <RelativeTime iso={e.detectedAt} />82 </span>83 <span className={cn("figure text-[11.5px]", e.significance >= 60 ? "text-accent" : "text-ink-4")}>sig {e.significance}</span>84 </div>85 <div className="min-w-0 flex-1 text-[13px]">86 <div className="font-medium text-ink">{e.title}</div>87 {e.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-ink-2">{e.summary}</p>}88 <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[12px] text-ink-3">89 {e.entity && (90 <Link href={entityHref(e.entityType, e.entity.slug, e.entity.name)} target="_blank" className="text-ink-2 hover:text-accent hover:underline">91 {e.entity.name}92 </Link>93 )}94 {e.operator && <span>{e.operator.name}</span>}95 {e.countryIso2 && <Badge size="xs">{e.countryIso2}</Badge>}96 <span>{e.sourceName}</span>97 <ExtLink href={e.url} />98 {(e.oldValue !== null && e.oldValue !== undefined) || (e.newValue !== null && e.newValue !== undefined) ? (99 <span className="figure">100 {e.oldValue !== null && e.oldValue !== undefined && <span className="text-danger">{fmtValue(e.oldValue)}</span>}101 {e.oldValue !== null && e.oldValue !== undefined && " → "}102 <span className="text-st-operational">{fmtValue(e.newValue)}</span>103 </span>104 ) : null}105 </div>106 </div>107 <div className="flex shrink-0 gap-1">108 <Button size="xs" variant={e.reviewStatus === "approved" ? "primary" : "secondary"} onClick={() => void setStatus(e, "approved")} disabled={busy !== null || e.reviewStatus === "approved"}>109 Approve110 </Button>111 <Button size="xs" variant="ghost" onClick={() => void setStatus(e, "pending")} disabled={busy !== null || e.reviewStatus === "pending"}>112 Pending113 </Button>114 <Button size="xs" variant="danger" onClick={() => void setStatus(e, "rejected")} disabled={busy !== null}>115 Reject116 </Button>117 </div>118 </li>119 ))}120 </ul>121 <div className="px-3">122 <Pager page={page} perPage={PER_PAGE} total={total} onPage={(p) => setParams({ page: p === 1 ? null : p })} />123 </div>124 </div>125 )}126 <p className="mt-2 text-[11.5px] text-ink-4">{fmtInt(total)} events match the type / significance filters. The review filter applies to the current page.</p>127 </div>128 );129}130