Initial curated EventLens source from 0d80d3b38dfb817285481665b9e71e4b8a0656a0
This commit is contained in:
commit
663ea8c829
85 files changed
+12395
No files matched your search
@@ -0,0 +1,157 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user