All checks were successful
Test / frontend-tests (pull_request) Successful in 1m38s
Test / frontend-typecheck (pull_request) Successful in 1m40s
Test / rust-fmt-check (pull_request) Successful in 14m53s
Test / rust-tests (pull_request) Successful in 18m2s
Test / rust-clippy (pull_request) Successful in 15m46s
PR Review Automation / review (pull_request) Successful in 3m46s
Replace hardcoded light-mode Tailwind colors with dark: variants across six components. Issues that broke readability: - PiiDiffViewer / Security: toggle knob was bg-white (invisible on bg-muted in dark mode) -> bg-background - ImageGallery: thumbnail container, filename labels, alert banners, and modal chrome all used hardcoded gray/white backgrounds with dark text; added full dark: variants throughout - ShellExecution TIER_CONFIG: tier cards used bg-green/yellow/red-50 (near-white) with dark text; added dark:bg-*-950/30 backgrounds and light text for all three tiers - ShellApprovalModal: tier 2 badge hardcoded bg-yellow-50/text-yellow-700; added dark: variants - LogUpload: PII warning alert used bg-amber-50/text-amber-800; added dark:bg-amber-900/20 and lighter text for dark mode
134 lines
4.5 KiB
TypeScript
134 lines
4.5 KiB
TypeScript
import React from "react";
|
|
import type { PiiSpan } from "@/lib/tauriCommands";
|
|
import { Badge } from "@/components/ui";
|
|
|
|
interface PiiDiffViewerProps {
|
|
originalText: string;
|
|
redactedText: string;
|
|
spans: PiiSpan[];
|
|
approvedSpans: PiiSpan[];
|
|
onToggleSpan: (span: PiiSpan, approved: boolean) => void;
|
|
}
|
|
|
|
export function PiiDiffViewer({
|
|
originalText,
|
|
redactedText,
|
|
spans,
|
|
approvedSpans,
|
|
onToggleSpan,
|
|
}: PiiDiffViewerProps) {
|
|
const isApproved = (span: PiiSpan) =>
|
|
approvedSpans.some((s) => s.start === span.start && s.end === span.end);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Side-by-side diff */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2 text-muted-foreground">Original</h4>
|
|
<div className="rounded-md border bg-card p-3 text-sm font-mono whitespace-pre-wrap max-h-64 overflow-y-auto">
|
|
{highlightSpans(originalText, spans, "original")}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2 text-muted-foreground">Redacted</h4>
|
|
<div className="rounded-md border bg-card p-3 text-sm font-mono whitespace-pre-wrap max-h-64 overflow-y-auto">
|
|
{redactedText || <span className="text-muted-foreground italic">No redactions applied</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* PII span list */}
|
|
{spans.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Detected PII ({spans.length} items)</h4>
|
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
|
{spans.map((span, idx) => (
|
|
<div
|
|
key={`${span.start}-${span.end}-${idx}`}
|
|
className="flex items-center justify-between rounded-md border p-2"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant={piiTypeBadgeVariant(span.pii_type)}>
|
|
{span.pii_type}
|
|
</Badge>
|
|
<span className="text-sm font-mono truncate max-w-[200px]">
|
|
{span.original}
|
|
</span>
|
|
<span className="text-muted-foreground text-xs">-></span>
|
|
<span className="text-sm font-mono text-muted-foreground truncate max-w-[200px]">
|
|
{span.replacement}
|
|
</span>
|
|
</div>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<span className="text-xs text-muted-foreground">
|
|
{isApproved(span) ? "Redact" : "Keep"}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={isApproved(span)}
|
|
onClick={() => onToggleSpan(span, !isApproved(span))}
|
|
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
|
isApproved(span) ? "bg-green-600" : "bg-muted"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 rounded-full bg-background transition-transform ${
|
|
isApproved(span) ? "translate-x-4" : "translate-x-0.5"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function highlightSpans(text: string, spans: PiiSpan[], _mode: "original" | "redacted") {
|
|
if (spans.length === 0) return text;
|
|
|
|
const sorted = [...spans].sort((a, b) => a.start - b.start);
|
|
const parts: React.ReactNode[] = [];
|
|
let lastEnd = 0;
|
|
|
|
sorted.forEach((span, idx) => {
|
|
if (span.start > lastEnd) {
|
|
parts.push(text.slice(lastEnd, span.start));
|
|
}
|
|
parts.push(
|
|
<mark key={idx} className="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5">
|
|
{text.slice(span.start, span.end)}
|
|
</mark>
|
|
);
|
|
lastEnd = span.end;
|
|
});
|
|
|
|
if (lastEnd < text.length) {
|
|
parts.push(text.slice(lastEnd));
|
|
}
|
|
|
|
return <>{parts}</>;
|
|
}
|
|
|
|
function piiTypeBadgeVariant(piiType: string): "default" | "secondary" | "destructive" | "outline" {
|
|
switch (piiType.toLowerCase()) {
|
|
case "email":
|
|
case "phone":
|
|
return "default";
|
|
case "ip_address":
|
|
case "hostname":
|
|
return "secondary";
|
|
case "ssn":
|
|
case "credit_card":
|
|
case "password":
|
|
return "destructive";
|
|
default:
|
|
return "outline";
|
|
}
|
|
}
|