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,19 +2106,29 @@ 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"
|
||||||
|
title="Choose a platform"
|
||||||
|
description="Pick the integration you want to configure. Cards switch the guidance shown in the next step."
|
||||||
|
>
|
||||||
|
<Card padding="lg" class="space-y-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>
|
<div>
|
||||||
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Step 1 · Choose a platform
|
{group.title}
|
||||||
</h3>
|
</h4>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<Show when={group.description}>
|
||||||
Select the integration you want to deploy or manage.
|
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{group.description}
|
||||||
</p>
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<For each={agentCards}>
|
<For each={group.agents}>
|
||||||
{(card) => {
|
{(card) => {
|
||||||
const isActive = () => selectedAgent() === card.id;
|
const isActive = () => selectedAgent() === card.id;
|
||||||
return (
|
return (
|
||||||
|
|
@ -2161,10 +2171,19 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</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'}>
|
||||||
|
<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">
|
<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">
|
||||||
|
|
@ -2682,10 +2701,16 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* PBS Nodes Tab */}
|
{/* PBS Nodes Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pbs'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pbs'}>
|
||||||
|
<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">
|
<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">
|
||||||
|
|
@ -3092,10 +3117,16 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* PMG Nodes Tab */}
|
{/* PMG Nodes Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pmg'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'pmg'}>
|
||||||
|
<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">
|
<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">
|
||||||
|
|
@ -3492,11 +3523,18 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Docker Tab */}
|
{/* Docker Tab */}
|
||||||
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'docker'}>
|
<Show when={activeTab() === 'agent-hub' && selectedAgent() === 'docker'}>
|
||||||
|
<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 />
|
<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'}>
|
||||||
|
<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 />
|
<HostAgents />
|
||||||
|
</AgentStepSection>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* System General Tab */}
|
{/* System General Tab */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue