tftsr-devops_investigation/src/components/PiiDiffViewer.tsx
Shaun Arman d331e9c7c7
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
fix(ui): correct font contrast and background colors in dark mode
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
2026-06-07 21:30:50 -05:00

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">-&gt;</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";
}
}