feat(awooop): add incident evidence headers
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m7s
CD Pipeline / build-and-deploy (push) Successful in 3m40s
CD Pipeline / post-deploy-checks (push) Successful in 1m28s

This commit is contained in:
Your Name
2026-05-17 23:37:53 +08:00
parent a6699c41f8
commit 69f2ec5ec9
7 changed files with 216 additions and 5 deletions

View File

@@ -0,0 +1,136 @@
"use client";
import { Link } from "@/i18n/routing";
import { cn } from "@/lib/utils";
import { Link2, SearchCheck, ShieldCheck } from "lucide-react";
import { useTranslations } from "next-intl";
const INCIDENT_ID_RE = /^INC-\d{8}-[A-Z0-9]{4,}$/;
function normalizeIncidentIds(incidentIds?: Array<string | null | undefined>) {
return Array.from(
new Set(
(incidentIds ?? [])
.map((incidentId) => String(incidentId || "").trim().toUpperCase())
.filter((incidentId) => INCIDENT_ID_RE.test(incidentId))
)
);
}
function boolLabel(value: boolean | null | undefined, emptyLabel: string) {
if (value === true) return "true";
if (value === false) return "false";
return emptyLabel;
}
export interface IncidentEvidenceHeaderProps {
projectId?: string | null;
runId?: string | null;
incidentIds?: Array<string | null | undefined>;
dryRunCount?: number | null;
latestRoute?: string | null;
writesIncidentState?: boolean | null;
writesAutoRepairResult?: boolean | null;
className?: string;
}
export function IncidentEvidenceHeader({
projectId,
runId,
incidentIds,
dryRunCount,
latestRoute,
writesIncidentState,
writesAutoRepairResult,
className,
}: IncidentEvidenceHeaderProps) {
const t = useTranslations("awooop.incidentEvidence");
const normalizedIncidentIds = normalizeIncidentIds(incidentIds);
const visibleIncidentIds = normalizedIncidentIds.slice(0, 3);
const hiddenCount = Math.max(normalizedIncidentIds.length - visibleIncidentIds.length, 0);
const emptyLabel = t("empty");
const route = latestRoute && latestRoute !== "--" ? latestRoute : emptyLabel;
const safeProjectId = projectId || "awoooi";
const runHref = runId
? `/awooop/runs/${runId}?project_id=${encodeURIComponent(safeProjectId)}`
: null;
return (
<section className={cn("border border-[#e0ddd4] bg-white", className)}>
<div className="flex flex-wrap items-start justify-between gap-4 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
<div className="flex min-w-0 items-start gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center border border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]">
<SearchCheck className="h-4 w-4" aria-hidden="true" />
</span>
<div className="min-w-0">
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
{runHref && (
<Link
href={runHref as never}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#d97757]"
>
{t("runLink")}
<Link2 className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
)}
</div>
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-[1.5fr_1fr_1fr_1fr]">
<div className="min-w-0 bg-white px-4 py-3">
<div className="text-xs font-semibold text-[#77736a]">{t("incidentLabel")}</div>
{visibleIncidentIds.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{visibleIncidentIds.map((incidentId) => (
<Link
key={incidentId}
href={`/awooop/runs?project_id=${encodeURIComponent(safeProjectId)}&incident_id=${encodeURIComponent(incidentId)}` as never}
className="inline-flex items-center border border-[#d8d3c7] bg-[#faf9f3] px-2 py-1 font-mono text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
title={t("filterTitle", { incidentId })}
>
{incidentId}
</Link>
))}
{hiddenCount > 0 && (
<span className="inline-flex items-center border border-[#d8d3c7] bg-white px-2 py-1 font-mono text-xs text-[#5f5b52]">
{t("more", { count: hiddenCount })}
</span>
)}
</div>
) : (
<p className="mt-2 text-sm text-[#77736a]">{t("notLinked")}</p>
)}
</div>
<div className="min-w-0 bg-white px-4 py-3">
<div className="flex items-center gap-1.5 text-xs font-semibold text-[#77736a]">
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
{t("dryRuns")}
</div>
<p className="mt-2 font-mono text-xl font-semibold text-[#141413]">
{typeof dryRunCount === "number" ? dryRunCount : emptyLabel}
</p>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<div className="text-xs font-semibold text-[#77736a]">{t("route")}</div>
<p className="mt-2 truncate font-mono text-sm text-[#141413]" title={route}>
{route}
</p>
</div>
<div className="min-w-0 bg-white px-4 py-3">
<div className="text-xs font-semibold text-[#77736a]">{t("writes")}</div>
<p className="mt-2 font-mono text-sm text-[#141413]">
{t("writeFlags", {
incident: boolLabel(writesIncidentState, emptyLabel),
autoRepair: boolLabel(writesAutoRepairResult, emptyLabel),
})}
</p>
</div>
</div>
</section>
);
}

View File

@@ -23,6 +23,7 @@
import React, { useEffect, useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
import { Terminal } from 'lucide-react'
import {
useTerminalStore,
useIsConnected,
@@ -99,13 +100,14 @@ export const OmniTerminal = () => {
return (
<button
onClick={openTerminal}
className="fixed bottom-4 right-4 px-3 py-2 sm:px-4 bg-white/70 backdrop-blur-[20px] border border-gray-200 rounded-md shadow-sm text-nothing-black hover:bg-white transition-all flex items-center gap-2 group"
className="fixed bottom-4 right-4 flex h-11 w-11 items-center justify-center rounded-md border border-gray-200 bg-white/80 text-nothing-black shadow-sm backdrop-blur-[20px] transition-all hover:bg-white sm:h-12 sm:w-12"
style={{ zIndex: Z_INDEX.OMNI_TERMINAL }}
aria-label="Open Omni-Terminal"
aria-label={t('open')}
title={`${t('open')} (${t('shortcut')})`}
>
<span className="w-2 h-2 rounded-full bg-[#4A90D9] animate-pulse"></span>
<span className="font-['VT323'] text-base sm:text-lg">
<span className="hidden sm:inline">Omni-Terminal </span>[J]
<Terminal className="h-5 w-5 text-[#4A90D9]" aria-hidden="true" />
<span className="absolute -bottom-1 -right-1 rounded-sm border border-gray-200 bg-white px-1 font-['VT323'] text-[10px] leading-4 text-[#5f5b52]">
{t('shortcut')}
</span>
</button>
)