Replace the Presets side panel with an inline profile dropdown
Presets is no longer a collapsible NavMenu tab. It's now a compact dropdown in the sidebar listing profiles, with a "+" button that prompts for a name and creates+activates a copy of the current profile, and a "×" button that deletes the active profile (disabled for Default). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
12381545c8
commit
e69ce4e0ba
@@ -7,11 +7,10 @@ import { DocsPanel } from './Docs'
|
|||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
import { BUILTIN_THEMES, downloadThemeFile, generateThemeCss, parseThemeCss } from '../lib/themes'
|
import { BUILTIN_THEMES, downloadThemeFile, generateThemeCss, parseThemeCss } from '../lib/themes'
|
||||||
|
|
||||||
type PanelName = 'settings' | 'presets' | 'docs' | null
|
type PanelName = 'settings' | 'docs' | null
|
||||||
|
|
||||||
const PANEL_TITLES: Record<Exclude<PanelName, null>, string> = {
|
const PANEL_TITLES: Record<Exclude<PanelName, null>, string> = {
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
presets: 'Presets',
|
|
||||||
docs: 'Docs',
|
docs: 'Docs',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,17 +58,9 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
|||||||
<div className="flex h-full w-48 shrink-0 flex-col justify-between border-r border-border p-3">
|
<div className="flex h-full w-48 shrink-0 flex-col justify-between border-r border-border p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 text-lg font-semibold text-text">ottyl</div>
|
<div className="mb-4 text-lg font-semibold text-text">ottyl</div>
|
||||||
<nav className="flex flex-col gap-1 text-sm text-text-muted">
|
<PresetsDropdown panel={panel} />
|
||||||
|
<nav className="mt-2 flex flex-col gap-1 text-sm text-text-muted">
|
||||||
<span className="rounded bg-surface-2 px-2 py-1 text-text">Dashboard</span>
|
<span className="rounded bg-surface-2 px-2 py-1 text-text">Dashboard</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => togglePanel('presets')}
|
|
||||||
className={`rounded px-2 py-1 text-left hover:bg-surface-2 ${
|
|
||||||
openPanel === 'presets' ? 'bg-surface-2 text-text' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Presets
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => togglePanel('settings')}
|
onClick={() => togglePanel('settings')}
|
||||||
@@ -114,7 +105,6 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<h2 className="text-sm font-semibold text-text">{PANEL_TITLES[openPanel]}</h2>
|
<h2 className="text-sm font-semibold text-text">{PANEL_TITLES[openPanel]}</h2>
|
||||||
{openPanel === 'settings' && <HelpIcon sectionId="settings" label="Help: Settings" />}
|
{openPanel === 'settings' && <HelpIcon sectionId="settings" label="Help: Settings" />}
|
||||||
{openPanel === 'presets' && <HelpIcon sectionId="presets" label="Help: Presets" />}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -131,7 +121,6 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{openPanel === 'settings' && <SettingsPanel panel={panel} />}
|
{openPanel === 'settings' && <SettingsPanel panel={panel} />}
|
||||||
{openPanel === 'presets' && <PresetsPanel panel={panel} />}
|
|
||||||
{openPanel === 'docs' && <DocsPanel target={docsPanelTarget} />}
|
{openPanel === 'docs' && <DocsPanel target={docsPanelTarget} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -139,6 +128,62 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replaces the old Presets side-panel tab: a compact dropdown of profiles plus "+" (create a copy of
|
||||||
|
/// the active profile, prompting for its name) and "×" (delete the active profile — disabled for
|
||||||
|
/// `Default`, which can never be deleted).
|
||||||
|
function PresetsDropdown({ panel }: { panel: ReturnType<typeof useSettingsPanel> }) {
|
||||||
|
const activeProfile = panel.settings?.active_profile ?? null
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
const name = window.prompt('New profile name (copies the current profile):')?.trim()
|
||||||
|
if (!name) return
|
||||||
|
await panel.addProfile(name)
|
||||||
|
await panel.switchProfile(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!activeProfile || activeProfile === DEFAULT_PROFILE_NAME) return
|
||||||
|
await panel.removeProfile(activeProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<select
|
||||||
|
value={activeProfile ?? ''}
|
||||||
|
onChange={(e) => panel.switchProfile(e.target.value)}
|
||||||
|
className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-1.5 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||||
|
>
|
||||||
|
{activeProfile === null && <option value="">No active profile</option>}
|
||||||
|
{panel.profiles.map((profile) => (
|
||||||
|
<option key={profile.name} value={profile.name}>
|
||||||
|
{profile.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="shrink-0 rounded border border-border-soft px-1.5 py-1 text-xs text-text-secondary hover:bg-surface-2"
|
||||||
|
aria-label="New profile"
|
||||||
|
title="New profile (copies the current one)"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={!activeProfile || activeProfile === DEFAULT_PROFILE_NAME}
|
||||||
|
className="shrink-0 rounded border border-border-soft px-1.5 py-1 text-xs text-text-secondary hover:bg-surface-2 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
aria-label="Delete current profile"
|
||||||
|
title={activeProfile === DEFAULT_PROFILE_NAME ? "The Default profile can't be deleted" : 'Delete current profile'}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<HelpIcon sectionId="presets" label="Help: Presets" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }) {
|
function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }) {
|
||||||
const [filename, setFilename] = useState('')
|
const [filename, setFilename] = useState('')
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
@@ -510,90 +555,3 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PresetsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }) {
|
|
||||||
const [newName, setNewName] = useState('')
|
|
||||||
|
|
||||||
async function handleCreate(e: React.FormEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!newName.trim()) return
|
|
||||||
await panel.addProfile(newName.trim())
|
|
||||||
setNewName('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4 text-sm text-text-secondary">
|
|
||||||
<form onSubmit={handleCreate} className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="new profile name"
|
|
||||||
value={newName}
|
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
|
||||||
className="flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{panel.profiles.length === 0 ? (
|
|
||||||
<div className="text-xs text-text-faint">No profiles yet.</div>
|
|
||||||
) : (
|
|
||||||
<ul className="flex flex-col gap-1">
|
|
||||||
{panel.profiles.map((profile) => {
|
|
||||||
const isActive = panel.settings?.active_profile === profile.name
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={profile.name}
|
|
||||||
className={`rounded border px-2 py-1.5 text-xs ${
|
|
||||||
isActive ? 'border-border-strong bg-surface-2' : 'border-border'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => !isActive && panel.switchProfile(profile.name)}
|
|
||||||
disabled={isActive}
|
|
||||||
className={`truncate text-left font-medium ${
|
|
||||||
isActive ? 'cursor-default text-text' : 'text-text hover:underline'
|
|
||||||
}`}
|
|
||||||
title={isActive ? 'Active profile' : `Switch to ${profile.name}`}
|
|
||||||
>
|
|
||||||
{profile.name}
|
|
||||||
{isActive && <span className="ml-1 text-[10px] uppercase text-text-muted">active</span>}
|
|
||||||
</button>
|
|
||||||
{profile.name === DEFAULT_PROFILE_NAME ? (
|
|
||||||
<span className="ml-2 shrink-0 text-text-faintest" title="The Default profile can't be deleted">
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => panel.removeProfile(profile.name)}
|
|
||||||
className="ml-2 shrink-0 rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
|
||||||
aria-label={`Delete ${profile.name}`}
|
|
||||||
>
|
|
||||||
delete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-[10px] text-text-subtle">
|
|
||||||
{profile.param_count} params · {profile.metric_count} metrics · {profile.dashboard_count} dashboards
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
<p className="text-[11px] text-text-faint">
|
|
||||||
A new profile starts as a copy of the currently active one (or empty, if none is active).
|
|
||||||
</p>
|
|
||||||
<p className="text-[11px] text-text-faint">
|
|
||||||
Switching profiles hot-swaps the running engine's declared params/metrics immediately — no restart needed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user