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:
parent
0bfaaba9ee
commit
a1bc0a94d5
2 changed files with 72 additions and 110 deletions
|
|
@ -13,6 +13,7 @@ import { OIDCPanel } from './OIDCPanel';
|
||||||
import { QuickSecuritySetup } from './QuickSecuritySetup';
|
import { QuickSecuritySetup } from './QuickSecuritySetup';
|
||||||
import { SecurityPostureSummary } from './SecurityPostureSummary';
|
import { SecurityPostureSummary } from './SecurityPostureSummary';
|
||||||
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
|
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
|
||||||
|
import { SettingsSectionNav } from './SettingsSectionNav';
|
||||||
import { SettingsAPI } from '@/api/settings';
|
import { SettingsAPI } from '@/api/settings';
|
||||||
import { NodesAPI } from '@/api/nodes';
|
import { NodesAPI } from '@/api/nodes';
|
||||||
import { UpdatesAPI } from '@/api/updates';
|
import { UpdatesAPI } from '@/api/updates';
|
||||||
|
|
@ -394,37 +395,6 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
const [selectedAgent, setSelectedAgent] = createSignal<AgentKey>('pve');
|
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> = {
|
const agentPaths: Record<AgentKey, string> = {
|
||||||
pve: '/settings/pve',
|
pve: '/settings/pve',
|
||||||
pbs: '/settings/pbs',
|
pbs: '/settings/pbs',
|
||||||
|
|
@ -2129,82 +2099,15 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
<div class="p-6 lg:p-8">
|
<div class="p-6 lg:p-8">
|
||||||
<Show when={activeTab() === 'proxmox'}>
|
<Show when={activeTab() === 'proxmox'}>
|
||||||
<SectionHeader
|
<SettingsSectionNav
|
||||||
title="Select an integration"
|
current={selectedAgent()}
|
||||||
description="Choose the platform you want to configure. The configuration interface will update based on your selection."
|
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>
|
</Show>
|
||||||
{/* PVE Nodes Tab */}
|
{/* PVE Nodes Tab */}
|
||||||
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pve'}>
|
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pve'}>
|
||||||
<div class="space-y-6 mt-6">
|
<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">
|
<div class="space-y-4">
|
||||||
<Show when={!initialLoadComplete()}>
|
<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">
|
<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 */}
|
{/* PBS Nodes Tab */}
|
||||||
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pbs'}>
|
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pbs'}>
|
||||||
<div class="space-y-6 mt-6">
|
<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">
|
<div class="space-y-4">
|
||||||
<Show when={!initialLoadComplete()}>
|
<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">
|
<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 */}
|
{/* PMG Nodes Tab */}
|
||||||
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pmg'}>
|
<Show when={activeTab() === 'proxmox' && selectedAgent() === 'pmg'}>
|
||||||
<div class="space-y-6 mt-6">
|
<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">
|
<div class="space-y-4">
|
||||||
<Show when={!initialLoadComplete()}>
|
<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">
|
<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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue