refactor: condense settings UI with tab navigation

Replace large card-style buttons with compact tab navigation matching the Proxmox pages pattern. Remove redundant section headers that duplicated tab labels, significantly reducing visual bloat.
This commit is contained in:
rcourtman 2025-10-24 10:16:22 +00:00
parent 0bfaaba9ee
commit a1bc0a94d5
2 changed files with 72 additions and 110 deletions

View file

@ -13,6 +13,7 @@ import { OIDCPanel } from './OIDCPanel';
import { QuickSecuritySetup } from './QuickSecuritySetup';
import { SecurityPostureSummary } from './SecurityPostureSummary';
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
import { SettingsSectionNav } from './SettingsSectionNav';
import { SettingsAPI } from '@/api/settings';
import { NodesAPI } from '@/api/nodes';
import { UpdatesAPI } from '@/api/updates';
@ -394,37 +395,6 @@ const Settings: Component<SettingsProps> = (props) => {
const [selectedAgent, setSelectedAgent] = createSignal<AgentKey>('pve');
const agentGroups: {
title: string;
description?: string;
agents: Array<{ id: AgentKey; label: string; description: string; icon: JSX.Element; disabled?: boolean }>;
}[] = [
{
title: 'Proxmox Products',
description: 'Select which Proxmox product to configure',
agents: [
{
id: 'pve',
label: 'Virtual Environment',
description: 'VMs, containers, clusters, and storage',
icon: <Server class="w-6 h-6" strokeWidth={2} />,
},
{
id: 'pbs',
label: 'Backup Server',
description: 'Backup jobs, datastores, and snapshots',
icon: <HardDrive class="w-6 h-6" strokeWidth={2} />,
},
{
id: 'pmg',
label: 'Mail Gateway',
description: 'Mail flow, spam filtering, and queues',
icon: <Mail class="w-6 h-6" strokeWidth={2} />,
},
],
},
];
const agentPaths: Record<AgentKey, string> = {
pve: '/settings/pve',
pbs: '/settings/pbs',
@ -2129,82 +2099,15 @@ const Settings: Component<SettingsProps> = (props) => {
<div class="p-6 lg:p-8">
<Show when={activeTab() === 'proxmox'}>
<SectionHeader
title="Select an integration"
description="Choose the platform you want to configure. The configuration interface will update based on your selection."
<SettingsSectionNav
current={selectedAgent()}
onSelect={handleSelectAgent}
class="mb-6"
/>
<Card padding="lg" class="space-y-6 mt-6">
<For each={agentGroups}>
{(group) => (
<section class="space-y-3">
<header class="flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between">
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{group.title}
</h4>
<Show when={group.description}>
<p class="text-xs text-gray-600 dark:text-gray-400">
{group.description}
</p>
</Show>
</div>
</header>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<For each={group.agents}>
{(card) => {
const isActive = () => selectedAgent() === card.id;
return (
<button
type="button"
disabled={card.disabled}
class={`flex flex-col items-start gap-3 rounded-xl border transition-colors p-4 text-left shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900 ${
card.disabled
? 'cursor-not-allowed opacity-60 border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800'
: isActive()
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-blue-300 dark:hover:border-blue-500'
}`}
onClick={() => {
if (card.disabled) return;
handleSelectAgent(card.id);
}}
>
<div class="flex items-center gap-3">
<div
class={`rounded-md p-2 ${
isActive()
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200'
}`}
>
{card.icon}
</div>
<div class="flex-1">
<p class="font-semibold text-gray-900 dark:text-gray-100">
{card.label}
</p>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
{card.description}
</p>
</div>
</div>
</button>
);
}}
</For>
</div>
</section>
)}
</For>
</Card>
</Show>
{/* PVE Nodes Tab */}
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pve'}>
<div class="space-y-6 mt-6">
<SectionHeader
title="Connect Proxmox VE"
description="Link Pulse to your Proxmox VE cluster with a dedicated API token. Quick setup can scaffold users, roles, and permissions for you."
/>
<div class="space-y-4">
<Show when={!initialLoadComplete()}>
<div class="flex items-center justify-center rounded-lg border border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/40 py-12 text-sm text-gray-500 dark:text-gray-400">
@ -2482,10 +2385,6 @@ const Settings: Component<SettingsProps> = (props) => {
{/* PBS Nodes Tab */}
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pbs'}>
<div class="space-y-6 mt-6">
<SectionHeader
title="Connect Proxmox Backup Server"
description="Provide a PBS API token with backup read access. Use quick setup to generate the token and grant the minimum privileges."
/>
<div class="space-y-4">
<Show when={!initialLoadComplete()}>
<div class="flex items-center justify-center rounded-lg border border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/40 py-12 text-sm text-gray-500 dark:text-gray-400">
@ -2762,10 +2661,6 @@ const Settings: Component<SettingsProps> = (props) => {
{/* PMG Nodes Tab */}
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pmg'}>
<div class="space-y-6 mt-6">
<SectionHeader
title="Connect Proxmox Mail Gateway"
description="Onboard each Mail Gateway with a limited API token so Pulse can read queue depth and quarantine metrics."
/>
<div class="space-y-4">
<Show when={!initialLoadComplete()}>
<div class="flex items-center justify-center rounded-lg border border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/40 py-12 text-sm text-gray-500 dark:text-gray-400">

View file

@ -0,0 +1,67 @@
import type { Component } from 'solid-js';
import { For } from 'solid-js';
import Server from 'lucide-solid/icons/server';
import HardDrive from 'lucide-solid/icons/hard-drive';
import Mail from 'lucide-solid/icons/mail';
type SettingsSection = 'pve' | 'pbs' | 'pmg' | 'docker' | 'host' | 'podman' | 'kubernetes';
interface SettingsSectionNavProps {
current: SettingsSection;
onSelect: (section: SettingsSection) => void;
class?: string;
}
const allSections: Array<{
id: SettingsSection;
label: string;
icon: typeof Server;
}> = [
{
id: 'pve',
label: 'Virtual Environment',
icon: Server,
},
{
id: 'pbs',
label: 'Backup Server',
icon: HardDrive,
},
{
id: 'pmg',
label: 'Mail Gateway',
icon: Mail,
},
];
export const SettingsSectionNav: Component<SettingsSectionNavProps> = (props) => {
const baseClasses =
'inline-flex items-center gap-2 px-2 sm:px-3 py-1 text-xs sm:text-sm font-medium border-b-2 border-transparent text-gray-600 dark:text-gray-400 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900';
return (
<div class={`flex flex-wrap items-center gap-3 sm:gap-4 ${props.class ?? ''}`} aria-label="Settings sections">
<For each={allSections}>
{(section) => {
const isActive = section.id === props.current;
const classes = isActive
? `${baseClasses} text-blue-600 dark:text-blue-300 border-blue-500 dark:border-blue-400`
: `${baseClasses} hover:text-blue-500 dark:hover:text-blue-300 hover:border-blue-300/70 dark:hover:border-blue-500/50`;
const Icon = section.icon;
return (
<button
type="button"
class={classes}
onClick={() => props.onSelect(section.id)}
aria-pressed={isActive}
>
<Icon size={16} stroke-width={2} />
<span>{section.label}</span>
</button>
);
}}
</For>
</div>
);
};