refactor: unify agent hub layout and copy
This commit is contained in:
parent
30b78784e0
commit
f800d7d59d
4 changed files with 144 additions and 77 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js';
|
import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js';
|
||||||
import { createStore, reconcile } from 'solid-js/store';
|
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import type { VM, Container, Node } from '@/types/api';
|
import type { VM, Container, Node } from '@/types/api';
|
||||||
import { GuestRow } from './GuestRow';
|
import { GuestRow } from './GuestRow';
|
||||||
|
|
@ -42,15 +41,8 @@ export function Dashboard(props: DashboardProps) {
|
||||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||||
const [guestMetadata, setGuestMetadata] = createSignal<Record<string, GuestMetadata>>({});
|
const [guestMetadata, setGuestMetadata] = createSignal<Record<string, GuestMetadata>>({});
|
||||||
|
|
||||||
// Stable guest store using reconcile to prevent row remounting during websocket updates
|
// Combine VMs and containers into a single list for filtering
|
||||||
const guestKey = (g: VM | Container) =>
|
const allGuests = createMemo<(VM | Container)[]>(() => [...props.vms, ...props.containers]);
|
||||||
g.id ?? (g.instance === g.node ? `${g.node}-${g.vmid}` : `${g.instance}-${g.node}-${g.vmid}`);
|
|
||||||
const [guestStore, setGuestStore] = createStore<(VM | Container)[]>([]);
|
|
||||||
|
|
||||||
// Reconcile guests whenever props change
|
|
||||||
createEffect(() => {
|
|
||||||
setGuestStore(reconcile([...props.vms, ...props.containers], { key: guestKey, merge: true }));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize from localStorage with proper type checking
|
// Initialize from localStorage with proper type checking
|
||||||
const [viewMode, setViewMode] = usePersistentSignal<ViewMode>('dashboardViewMode', 'all', {
|
const [viewMode, setViewMode] = usePersistentSignal<ViewMode>('dashboardViewMode', 'all', {
|
||||||
|
|
@ -248,9 +240,6 @@ export function Dashboard(props: DashboardProps) {
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the stable guest store
|
|
||||||
const allGuests = createMemo(() => guestStore);
|
|
||||||
|
|
||||||
// Filter guests based on current settings
|
// Filter guests based on current settings
|
||||||
const filteredGuests = createMemo(() => {
|
const filteredGuests = createMemo(() => {
|
||||||
let guests = allGuests();
|
let guests = allGuests();
|
||||||
|
|
|
||||||
34
frontend-modern/src/components/Settings/AgentStepSection.tsx
Normal file
34
frontend-modern/src/components/Settings/AgentStepSection.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Component, JSX } from 'solid-js';
|
||||||
|
import { Show } from 'solid-js';
|
||||||
|
|
||||||
|
interface AgentStepSectionProps {
|
||||||
|
step: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentStepSection: Component<AgentStepSectionProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<section class="space-y-3">
|
||||||
|
<header class="flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-blue-600 dark:text-blue-300">
|
||||||
|
{props.step}
|
||||||
|
</p>
|
||||||
|
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
{props.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<Show when={props.description}>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-right">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</header>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentStepSection;
|
||||||
|
|
@ -46,8 +46,8 @@ export const DockerAgents: Component = () => {
|
||||||
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
||||||
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
||||||
|
|
||||||
const tokenStepLabel = 'Step 1 · Generate API token';
|
const tokenStepLabel = 'Step 2 · Generate API token';
|
||||||
const commandStepLabel = 'Step 2 · Install command';
|
const commandStepLabel = 'Step 3 · Install command';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
token: apiToken,
|
token: apiToken,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||||
import { Toggle } from '@/components/shared/Toggle';
|
import { Toggle } from '@/components/shared/Toggle';
|
||||||
import type { ToggleChangeEvent } from '@/components/shared/Toggle';
|
import type { ToggleChangeEvent } from '@/components/shared/Toggle';
|
||||||
import { formField, labelClass, controlClass, formHelpText } from '@/components/shared/Form';
|
import { formField, labelClass, controlClass, formHelpText } from '@/components/shared/Form';
|
||||||
|
import { AgentStepSection } from './AgentStepSection';
|
||||||
import Server from 'lucide-solid/icons/server';
|
import Server from 'lucide-solid/icons/server';
|
||||||
import HardDrive from 'lucide-solid/icons/hard-drive';
|
import HardDrive from 'lucide-solid/icons/hard-drive';
|
||||||
import Mail from 'lucide-solid/icons/mail';
|
import Mail from 'lucide-solid/icons/mail';
|
||||||
|
|
@ -448,7 +449,6 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const agentCards = agentGroups.flatMap((group) => group.agents);
|
|
||||||
|
|
||||||
const agentPaths: Record<AgentKey, string> = {
|
const agentPaths: Record<AgentKey, string> = {
|
||||||
pve: '/settings/pve',
|
pve: '/settings/pve',
|
||||||
|
|
@ -2106,66 +2106,85 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
<div class="p-3 sm:p-6 overflow-x-auto">
|
<div class="p-3 sm:p-6 overflow-x-auto">
|
||||||
<Show when={activeTab() === 'agent-hub'}>
|
<Show when={activeTab() === 'agent-hub'}>
|
||||||
<Card padding="lg" class="space-y-4">
|
<AgentStepSection
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
step="Step 1"
|
||||||
<div>
|
title="Choose a platform"
|
||||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
description="Pick the integration you want to configure. Cards switch the guidance shown in the next step."
|
||||||
Step 1 · Choose a platform
|
>
|
||||||
</h3>
|
<Card padding="lg" class="space-y-6">
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<For each={agentGroups}>
|
||||||
Select the integration you want to deploy or manage.
|
{(group) => (
|
||||||
</p>
|
<section class="space-y-3">
|
||||||
</div>
|
<header class="flex flex-col gap-1 sm:flex-row sm:items-baseline sm:justify-between">
|
||||||
</div>
|
<div>
|
||||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<For each={agentCards}>
|
{group.title}
|
||||||
{(card) => {
|
</h4>
|
||||||
const isActive = () => selectedAgent() === card.id;
|
<Show when={group.description}>
|
||||||
return (
|
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
<button
|
{group.description}
|
||||||
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>
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
</Show>
|
||||||
{card.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
</For>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
{/* PVE Nodes Tab */}
|
{/* PVE Nodes Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pve'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pve'}>
|
||||||
<div class="space-y-4">
|
<AgentStepSection
|
||||||
|
step="Step 2"
|
||||||
|
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()}>
|
<Show when={!initialLoadComplete()}>
|
||||||
<div class="flex items-center justify-center py-8">
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="text-gray-500">Loading configuration...</span>
|
<span class="text-gray-500">Loading configuration...</span>
|
||||||
|
|
@ -2682,11 +2701,17 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</AgentStepSection>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* PBS Nodes Tab */}
|
{/* PBS Nodes Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pbs'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pbs'}>
|
||||||
<div class="space-y-4">
|
<AgentStepSection
|
||||||
|
step="Step 2"
|
||||||
|
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()}>
|
<Show when={!initialLoadComplete()}>
|
||||||
<div class="flex items-center justify-center py-8">
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="text-gray-500">Loading configuration...</span>
|
<span class="text-gray-500">Loading configuration...</span>
|
||||||
|
|
@ -3092,11 +3117,17 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</AgentStepSection>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* PMG Nodes Tab */}
|
{/* PMG Nodes Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pmg'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pmg'}>
|
||||||
<div class="space-y-4">
|
<AgentStepSection
|
||||||
|
step="Step 2"
|
||||||
|
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()}>
|
<Show when={!initialLoadComplete()}>
|
||||||
<div class="flex items-center justify-center py-8">
|
<div class="flex items-center justify-center py-8">
|
||||||
<span class="text-gray-500">Loading configuration...</span>
|
<span class="text-gray-500">Loading configuration...</span>
|
||||||
|
|
@ -3492,11 +3523,18 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</AgentStepSection>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Docker Tab */}
|
{/* Docker Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'docker'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'docker'}>
|
||||||
<DockerAgents />
|
<AgentStepSection
|
||||||
|
step="Step 2"
|
||||||
|
title="Deploy the Docker agent"
|
||||||
|
description="Mint a scoped reporting token and copy the install commands for the Docker host you are onboarding."
|
||||||
|
>
|
||||||
|
<DockerAgents />
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Podman Tab */}
|
{/* Podman Tab */}
|
||||||
|
|
@ -3517,7 +3555,13 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
{/* Host Agents */}
|
{/* Host Agents */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'host'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'host'}>
|
||||||
<HostAgents />
|
<AgentStepSection
|
||||||
|
step="Step 2"
|
||||||
|
title="Install the Pulse host agent"
|
||||||
|
description="Pick the operating system, generate a scoped token, and copy the tailored commands for Linux, macOS, or Windows."
|
||||||
|
>
|
||||||
|
<HostAgents />
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* System General Tab */}
|
{/* System General Tab */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue