feat(awooop): show recurring alert links
This commit is contained in:
@@ -176,6 +176,50 @@ interface DossierCoverageResponse {
|
||||
providers: DossierCoverageProvider[];
|
||||
}
|
||||
|
||||
interface EventRecurrenceSummary {
|
||||
source_event_total: number;
|
||||
recurrence_group_total: number;
|
||||
recurrent_group_total: number;
|
||||
duplicate_event_total: number;
|
||||
linked_run_total: number;
|
||||
unlinked_event_total: number;
|
||||
latest_received_at?: string | null;
|
||||
}
|
||||
|
||||
interface EventRecurrenceItem {
|
||||
recurrence_key: string;
|
||||
provider?: string | null;
|
||||
alertname?: string | null;
|
||||
severity?: string | null;
|
||||
namespace?: string | null;
|
||||
target_resource?: string | null;
|
||||
fingerprint?: string | null;
|
||||
latest_event_id?: string | null;
|
||||
latest_provider_event_id?: string | null;
|
||||
latest_content_preview?: string | null;
|
||||
latest_run_id?: string | null;
|
||||
latest_run_state?: RunState | string | null;
|
||||
latest_agent_id?: string | null;
|
||||
occurrence_total: number;
|
||||
duplicate_total: number;
|
||||
linked_run_total: number;
|
||||
source_ref_total: number;
|
||||
missing_source_refs_total: number;
|
||||
sentry_ref_total: number;
|
||||
signoz_ref_total: number;
|
||||
alert_ref_total: number;
|
||||
run_state_counts: Record<string, number>;
|
||||
first_received_at?: string | null;
|
||||
latest_received_at?: string | null;
|
||||
}
|
||||
|
||||
interface EventRecurrenceResponse {
|
||||
project_id: string;
|
||||
limit: number;
|
||||
summary: EventRecurrenceSummary;
|
||||
items: EventRecurrenceItem[];
|
||||
}
|
||||
|
||||
interface CallbackReplyEvent {
|
||||
message_id: string;
|
||||
run_id: string;
|
||||
@@ -884,6 +928,179 @@ function SourceDossierCoveragePanel({
|
||||
);
|
||||
}
|
||||
|
||||
function recurrenceStateLabelKey(state?: string | null) {
|
||||
if (
|
||||
state === "pending" ||
|
||||
state === "running" ||
|
||||
state === "waiting_tool" ||
|
||||
state === "waiting_approval" ||
|
||||
state === "completed" ||
|
||||
state === "failed" ||
|
||||
state === "cancelled" ||
|
||||
state === "timeout"
|
||||
) {
|
||||
return `states.${state}`;
|
||||
}
|
||||
return "states.unlinked";
|
||||
}
|
||||
|
||||
function EventRecurrencePanel({
|
||||
recurrence,
|
||||
error,
|
||||
}: {
|
||||
recurrence: EventRecurrenceResponse | null;
|
||||
error: string | null;
|
||||
}) {
|
||||
const t = useTranslations("awooop.eventRecurrence");
|
||||
const summary = recurrence?.summary;
|
||||
const items = recurrence?.items ?? [];
|
||||
const latestAt = summary?.latest_received_at
|
||||
? new Date(summary.latest_received_at).toLocaleTimeString("zh-TW", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "--";
|
||||
const metrics = [
|
||||
{
|
||||
label: t("metrics.groups"),
|
||||
value: summary?.recurrence_group_total ?? 0,
|
||||
detail: t("details.sourceEvents", { count: summary?.source_event_total ?? 0 }),
|
||||
className: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
|
||||
},
|
||||
{
|
||||
label: t("metrics.recurrent"),
|
||||
value: summary?.recurrent_group_total ?? 0,
|
||||
detail: t("details.latest", { time: latestAt }),
|
||||
className: (summary?.recurrent_group_total ?? 0) > 0
|
||||
? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]"
|
||||
: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
},
|
||||
{
|
||||
label: t("metrics.duplicates"),
|
||||
value: summary?.duplicate_event_total ?? 0,
|
||||
detail: t("details.unlinked", { count: summary?.unlinked_event_total ?? 0 }),
|
||||
className: "border-[#d8d3c7] bg-white text-[#5f5b52]",
|
||||
},
|
||||
{
|
||||
label: t("metrics.linkedRuns"),
|
||||
value: summary?.linked_run_total ?? 0,
|
||||
detail: t("details.limit", { count: recurrence?.limit ?? 0 }),
|
||||
className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-[#8a5a08]" aria-hidden="true" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<p className="text-xs text-[#77736a]">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 text-xs font-semibold text-[#8a5a08]">
|
||||
{t("total", { count: items.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="px-4 py-4 text-sm text-[#9f2f25]">
|
||||
{t("error", { error })}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
|
||||
{metrics.map((item) => (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center border",
|
||||
item.className
|
||||
)}
|
||||
>
|
||||
<Activity className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-[#5f5b52]">{item.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="px-4 py-4 text-sm text-[#5f5b52]">
|
||||
{t("empty")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2 xl:grid-cols-3">
|
||||
{items.slice(0, 6).map((item) => {
|
||||
const latest = item.latest_received_at
|
||||
? new Date(item.latest_received_at).toLocaleTimeString("zh-TW", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "--";
|
||||
const stateKey = recurrenceStateLabelKey(item.latest_run_state);
|
||||
const stateLabel = t(stateKey as never);
|
||||
const runHref = item.latest_run_id
|
||||
? `/awooop/runs/${item.latest_run_id}?project_id=${encodeURIComponent(recurrence?.project_id ?? "awoooi")}`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<article key={item.recurrence_key} className="bg-white px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-mono text-xs font-semibold text-[#141413]">
|
||||
{item.alertname || item.provider || item.latest_provider_event_id || item.recurrence_key}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-[#77736a]">
|
||||
{item.namespace || "--"} · {item.target_resource || "--"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 border border-[#d9b36f] bg-[#fff7e8] px-2 py-0.5 font-mono text-xs font-semibold text-[#8a5a08]">
|
||||
{item.occurrence_total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-xs leading-5 text-[#5f5b52]">
|
||||
<p>{t("item.latest", { time: latest })}</p>
|
||||
<p>{t("item.duplicates", { count: item.duplicate_total })}</p>
|
||||
<p>{t("item.refs", { count: item.source_ref_total })}</p>
|
||||
<p>{t("item.linkedRuns", { count: item.linked_run_total })}</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
|
||||
{stateLabel}
|
||||
</span>
|
||||
{runHref ? (
|
||||
<Link
|
||||
href={runHref as never}
|
||||
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
|
||||
>
|
||||
<SearchCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("item.openRun")}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-xs text-[#77736a]">{t("item.noRun")}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupedAlertEventsPanel({ events }: { events: PlatformEvent[] }) {
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
@@ -1053,6 +1270,8 @@ export default function RunsPage() {
|
||||
const [groupedEvents, setGroupedEvents] = useState<PlatformEvent[]>([]);
|
||||
const [dossierCoverage, setDossierCoverage] = useState<DossierCoverageResponse | null>(null);
|
||||
const [dossierCoverageError, setDossierCoverageError] = useState<string | null>(null);
|
||||
const [eventRecurrence, setEventRecurrence] = useState<EventRecurrenceResponse | null>(null);
|
||||
const [eventRecurrenceError, setEventRecurrenceError] = useState<string | null>(null);
|
||||
const [callbackEvents, setCallbackEvents] = useState<CallbackReplyEvent[]>([]);
|
||||
const [callbackEventsTotal, setCallbackEventsTotal] = useState(0);
|
||||
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
|
||||
@@ -1150,6 +1369,21 @@ export default function RunsPage() {
|
||||
setDossierCoverageError(`HTTP ${dossierCoverageRes.status}`);
|
||||
}
|
||||
|
||||
const recurrenceParams = new URLSearchParams();
|
||||
recurrenceParams.set("limit", "100");
|
||||
if (projectFilter) recurrenceParams.set("project_id", projectFilter);
|
||||
const recurrenceRes = await fetch(
|
||||
`${API_BASE}/api/v1/platform/events/dossier/recurrence?${recurrenceParams.toString()}`
|
||||
);
|
||||
if (recurrenceRes.ok) {
|
||||
const recurrenceData: EventRecurrenceResponse = await recurrenceRes.json();
|
||||
setEventRecurrence(recurrenceData);
|
||||
setEventRecurrenceError(null);
|
||||
} else {
|
||||
setEventRecurrence(null);
|
||||
setEventRecurrenceError(`HTTP ${recurrenceRes.status}`);
|
||||
}
|
||||
|
||||
const callbackParams = new URLSearchParams();
|
||||
callbackParams.set("per_page", "6");
|
||||
if (projectFilter) callbackParams.set("project_id", projectFilter);
|
||||
@@ -1367,6 +1601,11 @@ export default function RunsPage() {
|
||||
error={dossierCoverageError}
|
||||
/>
|
||||
|
||||
<EventRecurrencePanel
|
||||
recurrence={eventRecurrence}
|
||||
error={eventRecurrenceError}
|
||||
/>
|
||||
|
||||
<GroupedAlertEventsPanel events={groupedEvents} />
|
||||
|
||||
<CallbackReplyEvidencePanel
|
||||
|
||||
Reference in New Issue
Block a user