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