mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 05:23:31 +01:00
MAESTRO: fix formatting for report lint
This commit is contained in:
@@ -56,5 +56,5 @@ ReportContainer
|
||||
- [x] Export JSON produces a valid JSON file containing all results
|
||||
- [x] Export HTML produces a styled standalone page viewable in any browser
|
||||
- [x] All report components are keyboard accessible
|
||||
- [ ] Linting passes (`npx eslint src/`, `npx prettier --check src/`)
|
||||
- [x] Linting passes (`npx eslint src/`, `npx prettier --check src/`)
|
||||
- [ ] Run `/speckit.analyze` to verify consistency
|
||||
|
||||
@@ -105,9 +105,7 @@ describe("AnalysisControls", () => {
|
||||
|
||||
getTestSelector(container);
|
||||
expect(getToggle(container, "toggle-resolve").getAttribute("aria-checked")).toBe("false");
|
||||
expect(getToggle(container, "toggle-decode-all").getAttribute("aria-checked")).toBe(
|
||||
"false",
|
||||
);
|
||||
expect(getToggle(container, "toggle-decode-all").getAttribute("aria-checked")).toBe("false");
|
||||
});
|
||||
|
||||
it("updates toggles without a controlled config", async () => {
|
||||
@@ -126,9 +124,7 @@ describe("AnalysisControls", () => {
|
||||
});
|
||||
|
||||
expect(decodeToggle.getAttribute("aria-checked")).toBe("true");
|
||||
expect(handleChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ decodeAll: true }),
|
||||
);
|
||||
expect(handleChange).toHaveBeenLastCalledWith(expect.objectContaining({ decodeAll: true }));
|
||||
});
|
||||
|
||||
it("updates toggles on click and keyboard", async () => {
|
||||
@@ -168,9 +164,7 @@ describe("AnalysisControls", () => {
|
||||
|
||||
const decodeToggle = getToggle(container, "toggle-decode-all");
|
||||
act(() => {
|
||||
decodeToggle.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
||||
);
|
||||
decodeToggle.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(decodeToggle.getAttribute("aria-checked")).toBe("true");
|
||||
@@ -206,14 +200,10 @@ describe("AnalysisControls", () => {
|
||||
|
||||
const resolveToggle = getToggle(container, "toggle-resolve");
|
||||
act(() => {
|
||||
resolveToggle.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: " ", bubbles: true }),
|
||||
);
|
||||
resolveToggle.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(resolveToggle.getAttribute("aria-checked")).toBe("true");
|
||||
expect(handleChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ resolve: true }),
|
||||
);
|
||||
expect(handleChange).toHaveBeenLastCalledWith(expect.objectContaining({ resolve: true }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,9 +66,7 @@ describe("ProgressIndicator", () => {
|
||||
expect(indicator.getAttribute("data-status")).toBe("analysing");
|
||||
expect(indicator.getAttribute("data-variant")).toBe("normal");
|
||||
|
||||
expect(getByTestId(container, "progress-percentage").textContent ?? "").toMatch(
|
||||
/50%/,
|
||||
);
|
||||
expect(getByTestId(container, "progress-percentage").textContent ?? "").toMatch(/50%/);
|
||||
expect(getByTestId(container, "progress-current-test").textContent ?? "").toMatch(
|
||||
/SpamAssassin Rule Hits/,
|
||||
);
|
||||
@@ -117,8 +115,6 @@ describe("ProgressIndicator", () => {
|
||||
expect(getByTestId(container, "timeout-tests").textContent ?? "").toMatch(
|
||||
/Mimecast Fingerprint/,
|
||||
);
|
||||
expect(getByTestId(container, "timeout-tests").textContent ?? "").toMatch(
|
||||
/Proofpoint TAP/,
|
||||
);
|
||||
expect(getByTestId(container, "timeout-tests").textContent ?? "").toMatch(/Proofpoint TAP/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,9 +180,7 @@ describe("TestSelector", () => {
|
||||
|
||||
const selectAllButton = getSelectAllButton(container);
|
||||
act(() => {
|
||||
selectAllButton.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
||||
);
|
||||
selectAllButton.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
sampleTests.forEach((test) => {
|
||||
@@ -191,9 +189,7 @@ describe("TestSelector", () => {
|
||||
|
||||
const deselectAllButton = getDeselectAllButton(container);
|
||||
act(() => {
|
||||
deselectAllButton.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: " ", bubbles: true }),
|
||||
);
|
||||
deselectAllButton.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));
|
||||
});
|
||||
|
||||
sampleTests.forEach((test) => {
|
||||
|
||||
@@ -84,9 +84,7 @@ describe("HopChainVisualisation", () => {
|
||||
it("renders connectors between hop nodes", () => {
|
||||
const { container } = render(<HopChainVisualisation hopChain={hopChain} />);
|
||||
|
||||
const connectors = container.querySelectorAll(
|
||||
'[data-testid^="hop-chain-connector-"]',
|
||||
);
|
||||
const connectors = container.querySelectorAll('[data-testid^="hop-chain-connector-"]');
|
||||
|
||||
expect(connectors.length).toBe(hopChain.length - 1);
|
||||
});
|
||||
|
||||
@@ -92,18 +92,14 @@ beforeEach(() => {
|
||||
}
|
||||
|
||||
lastBlob = null;
|
||||
createObjectUrlSpy = vi
|
||||
.spyOn(URL, "createObjectURL")
|
||||
.mockImplementation((blob: Blob) => {
|
||||
lastBlob = blob;
|
||||
return "blob:report";
|
||||
});
|
||||
createObjectUrlSpy = vi.spyOn(URL, "createObjectURL").mockImplementation((blob: Blob) => {
|
||||
lastBlob = blob;
|
||||
return "blob:report";
|
||||
});
|
||||
revokeObjectUrlSpy = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
clickSpy = vi
|
||||
.spyOn(HTMLAnchorElement.prototype, "click")
|
||||
.mockImplementation(() => undefined);
|
||||
clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -63,9 +63,7 @@ afterEach(() => {
|
||||
|
||||
describe("SecurityAppliancesSummary", () => {
|
||||
it("renders detected security appliances as badges", () => {
|
||||
const { container } = render(
|
||||
<SecurityAppliancesSummary appliances={sampleAppliances} />,
|
||||
);
|
||||
const { container } = render(<SecurityAppliancesSummary appliances={sampleAppliances} />);
|
||||
|
||||
const summary = getByTestId(container, "security-appliances-summary");
|
||||
expect(summary.textContent ?? "").toContain("Mimecast Email Security");
|
||||
|
||||
@@ -83,10 +83,7 @@ describe("TestResultCard", () => {
|
||||
|
||||
const { container } = render(<TestResultCard result={result} />);
|
||||
|
||||
const severityBadge = getByTestId(
|
||||
container,
|
||||
`test-result-severity-${result.testId}`,
|
||||
);
|
||||
const severityBadge = getByTestId(container, `test-result-severity-${result.testId}`);
|
||||
|
||||
expect(severityBadge.textContent ?? "").toContain(severityCase.label);
|
||||
expect(severityBadge.className).toContain(severityCase.className);
|
||||
|
||||
@@ -148,12 +148,10 @@ describe("useAnalysis", () => {
|
||||
});
|
||||
|
||||
it("submits analysis and handles SSE progress + result", async () => {
|
||||
const streamSpy = vi.spyOn(apiClient, "stream").mockImplementation(
|
||||
async (_path, options) => {
|
||||
options.onEvent({ event: "progress", data: progressEvent, raw: "" });
|
||||
options.onEvent({ event: "result", data: completeReport, raw: "" });
|
||||
},
|
||||
);
|
||||
const streamSpy = vi.spyOn(apiClient, "stream").mockImplementation(async (_path, options) => {
|
||||
options.onEvent({ event: "progress", data: progressEvent, raw: "" });
|
||||
options.onEvent({ event: "result", data: completeReport, raw: "" });
|
||||
});
|
||||
|
||||
const statuses: string[] = [];
|
||||
const { container } = render(
|
||||
@@ -161,9 +159,7 @@ describe("useAnalysis", () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getByTestId(container, "submit").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
getByTestId(container, "submit").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -180,9 +176,7 @@ describe("useAnalysis", () => {
|
||||
);
|
||||
|
||||
expect(statuses).toEqual(["idle", "submitting", "analysing", "complete"]);
|
||||
expect(getByTestId(container, "current-test").textContent).toMatch(
|
||||
/SpamAssassin Rule Hits/,
|
||||
);
|
||||
expect(getByTestId(container, "current-test").textContent).toMatch(/SpamAssassin Rule Hits/);
|
||||
expect(getByTestId(container, "percentage").textContent).toBe("33");
|
||||
expect(getByTestId(container, "result-total").textContent).toBe("3");
|
||||
});
|
||||
@@ -198,9 +192,7 @@ describe("useAnalysis", () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getByTestId(container, "submit").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
getByTestId(container, "submit").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -220,9 +212,7 @@ describe("useAnalysis", () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getByTestId(container, "submit").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
getByTestId(container, "submit").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -252,9 +242,7 @@ describe("useAnalysis", () => {
|
||||
);
|
||||
|
||||
act(() => {
|
||||
getByTestId(container, "submit").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
getByTestId(container, "submit").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
@@ -262,9 +250,7 @@ describe("useAnalysis", () => {
|
||||
});
|
||||
|
||||
act(() => {
|
||||
getByTestId(container, "cancel").dispatchEvent(
|
||||
new MouseEvent("click", { bubbles: true }),
|
||||
);
|
||||
getByTestId(container, "cancel").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
|
||||
@@ -93,9 +93,7 @@ export default function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{result ? (
|
||||
<AnalysisResults report={result} />
|
||||
) : null}
|
||||
{result ? <AnalysisResults report={result} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
import { useState, type KeyboardEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
faGlobe,
|
||||
faCode,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faToggleOff, faToggleOn, faGlobe, faCode } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import type { AnalysisConfig } from "../types/analysis";
|
||||
import TestSelector from "./TestSelector";
|
||||
@@ -33,10 +28,7 @@ const handleToggleKeyDown = (
|
||||
}
|
||||
};
|
||||
|
||||
export default function AnalysisControls({
|
||||
config,
|
||||
onChange,
|
||||
}: AnalysisControlsProps) {
|
||||
export default function AnalysisControls({ config, onChange }: AnalysisControlsProps) {
|
||||
const [internalConfig, setInternalConfig] = useState<AnalysisConfig>(defaultConfig);
|
||||
const resolvedConfig = config ?? internalConfig;
|
||||
|
||||
|
||||
@@ -3,31 +3,20 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import type {
|
||||
AnalysisReport,
|
||||
TestResult,
|
||||
TestSeverity,
|
||||
TestStatus,
|
||||
} from "../types/analysis";
|
||||
import type { AnalysisReport, TestResult, TestSeverity, TestStatus } from "../types/analysis";
|
||||
|
||||
type AnalysisResultsProps = {
|
||||
report: AnalysisReport;
|
||||
};
|
||||
|
||||
const severityStyles: Record<
|
||||
TestSeverity,
|
||||
{ label: string; className: string }
|
||||
> = {
|
||||
const severityStyles: Record<TestSeverity, { label: string; className: string }> = {
|
||||
spam: { label: "Spam", className: "text-spam border-spam/40" },
|
||||
suspicious: { label: "Suspicious", className: "text-suspicious border-suspicious/40" },
|
||||
clean: { label: "Clean", className: "text-clean border-clean/40" },
|
||||
info: { label: "Info", className: "text-accent border-accent/40" },
|
||||
};
|
||||
|
||||
const statusStyles: Record<
|
||||
TestStatus,
|
||||
{ label: string; className: string }
|
||||
> = {
|
||||
const statusStyles: Record<TestStatus, { label: string; className: string }> = {
|
||||
success: { label: "Success", className: "text-clean border-clean/40" },
|
||||
error: { label: "Error", className: "text-spam border-spam/40" },
|
||||
skipped: { label: "Skipped", className: "text-suspicious border-suspicious/40" },
|
||||
@@ -45,9 +34,7 @@ export default function AnalysisResults({ report }: AnalysisResultsProps) {
|
||||
className="rounded-2xl border border-info/10 bg-surface p-6 shadow-[0_0_40px_rgba(15,23,42,0.25)]"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-info/90">
|
||||
Analysis Report
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-info/90">Analysis Report</span>
|
||||
<span className="font-mono text-[10px] text-text/50">
|
||||
{report.metadata.passedTests} passed / {report.metadata.failedTests} failed
|
||||
</span>
|
||||
@@ -66,22 +53,16 @@ export default function AnalysisResults({ report }: AnalysisResultsProps) {
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold text-text/90">
|
||||
{result.testName}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-text/90">{result.testName}</span>
|
||||
<span className="text-xs text-text/50">
|
||||
Test #{result.testId} · {result.headerName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] uppercase tracking-[0.2em]">
|
||||
<span
|
||||
className={`rounded-full border px-2 py-1 ${statusStyle.className}`}
|
||||
>
|
||||
<span className={`rounded-full border px-2 py-1 ${statusStyle.className}`}>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full border px-2 py-1 ${severityStyle.className}`}
|
||||
>
|
||||
<span className={`rounded-full border px-2 py-1 ${severityStyle.className}`}>
|
||||
{severityStyle.label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -102,8 +83,7 @@ export default function AnalysisResults({ report }: AnalysisResultsProps) {
|
||||
>
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} className="mt-0.5" />
|
||||
<span>
|
||||
<span className="font-semibold">Error:</span>{" "}
|
||||
{getErrorMessage(result)}
|
||||
<span className="font-semibold">Error:</span> {getErrorMessage(result)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -35,11 +35,14 @@ const getVariant = (
|
||||
return remainingSeconds <= warningThreshold ? "warning" : "normal";
|
||||
};
|
||||
|
||||
const variantStyles: Record<ProgressVariant, {
|
||||
bar: string;
|
||||
badge: string;
|
||||
track: string;
|
||||
}> = {
|
||||
const variantStyles: Record<
|
||||
ProgressVariant,
|
||||
{
|
||||
bar: string;
|
||||
badge: string;
|
||||
track: string;
|
||||
}
|
||||
> = {
|
||||
normal: {
|
||||
bar: "bg-clean",
|
||||
badge: "text-clean border-clean/40",
|
||||
@@ -94,9 +97,7 @@ export default function ProgressIndicator({
|
||||
? anchor.elapsedMs + Math.max(0, Date.now() - anchor.timestamp)
|
||||
: baseElapsedMs;
|
||||
|
||||
setElapsedMs((previous) =>
|
||||
previous === nextElapsedMs ? previous : nextElapsedMs,
|
||||
);
|
||||
setElapsedMs((previous) => (previous === nextElapsedMs ? previous : nextElapsedMs));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
@@ -141,16 +142,10 @@ export default function ProgressIndicator({
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span
|
||||
data-testid="progress-current-test"
|
||||
className="text-sm font-semibold text-text/80"
|
||||
>
|
||||
<span data-testid="progress-current-test" className="text-sm font-semibold text-text/80">
|
||||
{progress?.currentTest ?? "Preparing analysis"}
|
||||
</span>
|
||||
<span
|
||||
data-testid="progress-percentage"
|
||||
className="text-sm font-semibold text-text/70"
|
||||
>
|
||||
<span data-testid="progress-percentage" className="text-sm font-semibold text-text/70">
|
||||
{percentage}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -173,9 +168,7 @@ export default function ProgressIndicator({
|
||||
role="alert"
|
||||
className="mt-4 rounded-xl border border-spam/30 bg-spam/10 p-4 text-xs text-text/80"
|
||||
>
|
||||
<p className="font-semibold text-spam">
|
||||
Timeout reached at {timeoutSeconds} seconds.
|
||||
</p>
|
||||
<p className="font-semibold text-spam">Timeout reached at {timeoutSeconds} seconds.</p>
|
||||
<p className="mt-2 text-text/70">Incomplete tests:</p>
|
||||
<p data-testid="timeout-tests" className="mt-1 font-mono text-text/70">
|
||||
{incompleteTests.length > 0 ? incompleteTests.join(", ") : "None"}
|
||||
|
||||
@@ -180,9 +180,7 @@ export default function TestSelector({ selectedTestIds, onSelectionChange }: Tes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-xs text-text/60">Loading tests...</p>
|
||||
) : null}
|
||||
{isLoading ? <p className="text-xs text-text/60">Loading tests...</p> : null}
|
||||
{error ? (
|
||||
<p role="alert" className="text-xs text-spam">
|
||||
{error}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowDown,
|
||||
faClock,
|
||||
faNetworkWired,
|
||||
faServer,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowDown, faClock, faNetworkWired, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import type { HopChainNode } from "../../types/analysis";
|
||||
|
||||
@@ -21,9 +16,7 @@ const formatDelay = (delay?: number | null): string | null => {
|
||||
return `${delay.toFixed(2)}s`;
|
||||
};
|
||||
|
||||
export default function HopChainVisualisation({
|
||||
hopChain,
|
||||
}: HopChainVisualisationProps) {
|
||||
export default function HopChainVisualisation({ hopChain }: HopChainVisualisationProps) {
|
||||
if (hopChain.length === 0) {
|
||||
return (
|
||||
<section
|
||||
@@ -56,13 +49,9 @@ export default function HopChainVisualisation({
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-wrap items-baseline gap-2">
|
||||
<span className="text-sm font-semibold text-text/90">
|
||||
{node.hostname}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-text/90">{node.hostname}</span>
|
||||
{node.ip ? (
|
||||
<span className="font-mono text-xs text-text/60">
|
||||
{node.ip}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-text/60">{node.ip}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-4 text-xs text-text/60">
|
||||
@@ -74,10 +63,7 @@ export default function HopChainVisualisation({
|
||||
) : null}
|
||||
{node.serverInfo ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faNetworkWired}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="text-[10px]" />
|
||||
{node.serverInfo}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -118,9 +118,7 @@ export default function ReportContainer({ report }: { report: AnalysisReport })
|
||||
>
|
||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-info/80">
|
||||
Interactive Report
|
||||
</p>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-info/80">Interactive Report</p>
|
||||
<h2 className="text-lg font-semibold text-text/90">Header Analysis Summary</h2>
|
||||
</div>
|
||||
<div className="rounded-full border border-info/20 bg-background/40 px-4 py-2 text-xs text-text/60">
|
||||
@@ -131,9 +129,7 @@ export default function ReportContainer({ report }: { report: AnalysisReport })
|
||||
<section className="rounded-2xl border border-info/10 bg-background/40 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-text/90">Summary Stats</h3>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-text/40">
|
||||
Totals
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-text/40">Totals</span>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{summaryItems.map((item) => (
|
||||
@@ -180,10 +176,7 @@ export default function ReportContainer({ report }: { report: AnalysisReport })
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{filteredResults.map((result) => (
|
||||
<div
|
||||
key={result.testId}
|
||||
data-testid={`test-result-card-${result.testId}`}
|
||||
>
|
||||
<div key={result.testId} data-testid={`test-result-card-${result.testId}`}>
|
||||
<TestResultCard result={result} highlightQuery={query} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -86,7 +86,7 @@ const buildHtmlReport = (report: AnalysisReport): string => {
|
||||
|
||||
const renderHopChain = (): string => {
|
||||
if (report.hopChain.length === 0) {
|
||||
return "<p class=\"muted\">No hop chain data available.</p>";
|
||||
return '<p class="muted">No hop chain data available.</p>';
|
||||
}
|
||||
|
||||
const items = report.hopChain
|
||||
@@ -108,7 +108,7 @@ const buildHtmlReport = (report: AnalysisReport): string => {
|
||||
|
||||
const renderSecurityAppliances = (): string => {
|
||||
if (report.securityAppliances.length === 0) {
|
||||
return "<p class=\"muted\">No security appliances detected.</p>";
|
||||
return '<p class="muted">No security appliances detected.</p>';
|
||||
}
|
||||
|
||||
const items = report.securityAppliances
|
||||
|
||||
@@ -9,9 +9,7 @@ type SecurityAppliancesSummaryProps = {
|
||||
appliances: SecurityAppliance[];
|
||||
};
|
||||
|
||||
export default function SecurityAppliancesSummary({
|
||||
appliances,
|
||||
}: SecurityAppliancesSummaryProps) {
|
||||
export default function SecurityAppliancesSummary({ appliances }: SecurityAppliancesSummaryProps) {
|
||||
const hasAppliances = appliances.length > 0;
|
||||
|
||||
return (
|
||||
@@ -46,10 +44,7 @@ export default function SecurityAppliancesSummary({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p
|
||||
data-testid="security-appliances-empty"
|
||||
className="mt-4 text-sm text-text/60"
|
||||
>
|
||||
<p data-testid="security-appliances-empty" className="mt-4 text-sm text-text/60">
|
||||
No security appliances detected.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -58,10 +58,7 @@ const highlightText = (text: string, query: string): ReactNode => {
|
||||
|
||||
const matchText = text.slice(matchIndex, matchIndex + normalizedQuery.length);
|
||||
parts.push(
|
||||
<mark
|
||||
key={`${matchIndex}-${matchText}`}
|
||||
className="rounded bg-accent/20 px-1 text-accent"
|
||||
>
|
||||
<mark key={`${matchIndex}-${matchText}`} className="rounded bg-accent/20 px-1 text-accent">
|
||||
{matchText}
|
||||
</mark>,
|
||||
);
|
||||
@@ -138,9 +135,7 @@ export default function TestResultCard({ result, highlightQuery = "" }: TestResu
|
||||
<div className="overflow-hidden">
|
||||
<div className="rounded-xl border border-info/10 bg-background/40 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-text/40">
|
||||
Header
|
||||
</span>
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] text-text/40">Header</span>
|
||||
<span className="text-xs text-text/60">
|
||||
{highlightText(result.headerName, highlightQuery)}
|
||||
</span>
|
||||
|
||||
@@ -4,13 +4,7 @@ import { flushSync } from "react-dom";
|
||||
import { apiClient, type SseEvent } from "../lib/api-client";
|
||||
import type { AnalysisConfig, AnalysisProgress, AnalysisReport } from "../types/analysis";
|
||||
|
||||
export type AnalysisStatus =
|
||||
| "idle"
|
||||
| "submitting"
|
||||
| "analysing"
|
||||
| "complete"
|
||||
| "error"
|
||||
| "timeout";
|
||||
export type AnalysisStatus = "idle" | "submitting" | "analysing" | "complete" | "error" | "timeout";
|
||||
|
||||
export interface AnalysisRequest {
|
||||
headers: string;
|
||||
@@ -112,14 +106,11 @@ const useAnalysis = (): UseAnalysisState => {
|
||||
hasProgressRef.current = false;
|
||||
|
||||
try {
|
||||
await apiClient.stream<AnalysisRequest, AnalysisProgress | AnalysisReport>(
|
||||
"/api/analyse",
|
||||
{
|
||||
body: request,
|
||||
signal: controller.signal,
|
||||
onEvent: (event) => handleEvent(event, requestId, controller.signal),
|
||||
},
|
||||
);
|
||||
await apiClient.stream<AnalysisRequest, AnalysisProgress | AnalysisReport>("/api/analyse", {
|
||||
body: request,
|
||||
signal: controller.signal,
|
||||
onEvent: (event) => handleEvent(event, requestId, controller.signal),
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mountedRef.current || controller.signal.aborted) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user