158 lines
7.7 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|