Files
EventLens-Public/frontend/src/workspaces/SettingsView.tsx
T

158 lines
7.7 KiB
TypeScript

import { WORKSPACES, type UiSettings } from "../app/model";
import { Icon } from "../components/icons";
import { WorkspaceHeader } from "../components/states";
type SettingsViewProps = {
settings: UiSettings;
saveNotice: string;
storageFailed: boolean;
storageConfirmed: boolean;
onChange: (patch: Partial<UiSettings>) => void;
onReset: () => void;
};
type OptionCardProps = {
checked: boolean;
description: string;
name: string;
onChange: () => void;
title: string;
};
function OptionCard({ checked, description, name, onChange, title }: OptionCardProps) {
const inputId = `${name}-${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
return (
<label className={`preference-option ${checked ? "selected" : ""}`} htmlFor={inputId}>
<input className="sr-only" id={inputId} type="radio" name={name} checked={checked} onChange={onChange} />
<span className="preference-option-indicator" aria-hidden="true" />
<span>
<strong>{title}</strong>
<small>{description}</small>
</span>
</label>
);
}
export function SettingsView({ settings, saveNotice, storageFailed, storageConfirmed, onChange, onReset }: SettingsViewProps) {
const headerAction = (
<div className="settings-header-status">
{storageConfirmed && !storageFailed ? <span className="settings-saved-automatically"><Icon name="check" />Saved automatically in this browser</span> : null}
<span className="settings-save-feedback" role="status" aria-live="polite">{saveNotice}</span>
</div>
);
return (
<>
<WorkspaceHeader
eyebrow="Workspace 05 · Local preferences"
title="Settings"
description="Browser-local display defaults for this device. Investigation evidence is never stored here."
action={headerAction}
/>
<div className="settings-layout">
<div className="settings-preferences">
<section className="settings-section panel" aria-labelledby="display-time-heading">
<header>
<Icon name="clock" />
<div>
<h2 id="display-time-heading">Display & time</h2>
<p>Choose a stable forensic reference or a local reading context.</p>
</div>
</header>
<fieldset>
<legend>Timestamp display</legend>
<div className="preference-option-grid">
<OptionCard name="timestamp-display" title="UTC" description="Stable forensic reference time" checked={settings.timestampDisplay === "utc"} onChange={() => onChange({ timestampDisplay: "utc" })} />
<OptionCard name="timestamp-display" title="Local browser time" description="Useful for local investigation context" checked={settings.timestampDisplay === "local"} onChange={() => onChange({ timestampDisplay: "local" })} />
</div>
</fieldset>
<fieldset>
<legend>Interface density</legend>
<div className="preference-option-grid">
<OptionCard name="density" title="Comfortable" description="More breathing room for review" checked={settings.density === "comfortable"} onChange={() => onChange({ density: "comfortable" })} />
<OptionCard name="density" title="Compact" description="More evidence visible at once" checked={settings.density === "compact"} onChange={() => onChange({ density: "compact" })} />
</div>
</fieldset>
</section>
<section className="settings-section panel" aria-labelledby="investigation-defaults-heading">
<header>
<Icon name="analysis" />
<div>
<h2 id="investigation-defaults-heading">Investigation defaults</h2>
<p>New investigations use these defaults; matching display/export controls update immediately.</p>
</div>
</header>
<div className="preference-rows">
<label className="preference-row">
<span>
<strong>Informational noise</strong>
<small>Hide routine informational events by default and update the current Timeline filter.</small>
</span>
<select aria-label="Default noise mode" value={settings.defaultNoise} onChange={(event) => onChange({ defaultNoise: event.target.value as UiSettings["defaultNoise"] })}>
<option value="attention">Hide by default</option>
<option value="all">Show by default</option>
</select>
</label>
<label className="preference-row">
<span>
<strong>Default workspace</strong>
<small>Workspace opened when EventLens starts without a deep link.</small>
</span>
<select aria-label="Default workspace" value={settings.defaultWorkspace} onChange={(event) => onChange({ defaultWorkspace: event.target.value as UiSettings["defaultWorkspace"] })}>
{WORKSPACES.map((workspace) => <option key={workspace} value={workspace}>{workspace[0].toUpperCase() + workspace.slice(1)}</option>)}
</select>
</label>
<label className="preference-row preference-switch-row">
<span>
<strong>Export redaction</strong>
<small>Recommended · Redaction remains best effort and should be reviewed.</small>
</span>
<span className="preference-switch">
<input className="sr-only" type="checkbox" role="switch" aria-label="Export redaction by default" checked={settings.defaultRedaction} onChange={(event) => onChange({ defaultRedaction: event.target.checked })} />
<span className="preference-switch-track" aria-hidden="true"><span /></span>
<b aria-hidden="true">{settings.defaultRedaction ? "On" : "Off"}</b>
</span>
</label>
</div>
</section>
</div>
<aside className="settings-privacy-panel panel" aria-labelledby="local-privacy-heading">
<header>
<Icon name="privacy" />
<div>
<p className="eyebrow">{storageFailed ? "Session only" : "Browser-local only"}</p>
<h2 id="local-privacy-heading">Local data & privacy</h2>
</div>
</header>
<p className="privacy-lead">{storageFailed ? "Preferences are available for this session only because browser storage is unavailable." : "Only your interface preferences remain in this browser."}</p>
<div className="privacy-facts">
<section>
<strong>{storageFailed ? "Not saved to browser storage" : "Stored locally"}</strong>
<span>{storageFailed ? "UI preferences for this session only" : "UI preferences only"}</span>
</section>
<section>
<strong>Never stored there</strong>
<ul>
<li>Event text</li>
<li>Analysis results</li>
<li>Uploaded EVTX files</li>
<li>Incident evidence</li>
</ul>
</section>
</div>
<details className="settings-technical-details">
<summary>Technical details</summary>
<p>{storageFailed ? "When browser storage is available, preferences use this validated record:" : "The browser uses one validated preference record:"}</p>
<code>eventlens.preferences.v1</code>
</details>
<button type="button" className="secondary-button settings-reset" onClick={onReset}>
<Icon name="reset" />Reset all preferences
</button>
</aside>
</div>
</>
);
}