mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 03:33:53 +00:00
Refine Proxmox navigation and Docker onboarding
This commit is contained in:
+78
-79
@@ -10,7 +10,7 @@ import {
|
||||
runWithOwner,
|
||||
} from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { Router, Route, useNavigate, useLocation } from '@solidjs/router';
|
||||
import { Router, Route, Navigate, useNavigate, useLocation } from '@solidjs/router';
|
||||
import { getGlobalWebSocketStore } from './stores/websocket-global';
|
||||
import { Dashboard } from './components/Dashboard/Dashboard';
|
||||
import StorageComponent from './components/Storage/Storage';
|
||||
@@ -564,9 +564,16 @@ function App() {
|
||||
// Use Router with routes
|
||||
return (
|
||||
<Router root={RootLayout}>
|
||||
<Route path="/" component={() => <Dashboard vms={state().vms} containers={state().containers} nodes={state().nodes} />} />
|
||||
<Route path="/storage" component={StorageComponent} />
|
||||
<Route path="/backups" component={Backups} />
|
||||
<Route path="/" component={() => <Navigate href="/proxmox/overview" />} />
|
||||
<Route path="/proxmox" component={() => <Navigate href="/proxmox/overview" />} />
|
||||
<Route
|
||||
path="/proxmox/overview"
|
||||
component={() => <Dashboard vms={state().vms} containers={state().containers} nodes={state().nodes} />}
|
||||
/>
|
||||
<Route path="/proxmox/storage" component={StorageComponent} />
|
||||
<Route path="/proxmox/backups" component={Backups} />
|
||||
<Route path="/storage" component={() => <Navigate href="/proxmox/storage" />} />
|
||||
<Route path="/backups" component={() => <Navigate href="/proxmox/backups" />} />
|
||||
<Route path="/docker" component={() => <DockerHosts hosts={state().dockerHosts} />} />
|
||||
<Route path="/alerts/*" component={Alerts} />
|
||||
<Route path="/settings/*" component={Settings} />
|
||||
@@ -592,16 +599,56 @@ function AppLayout(props: {
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [dockerTabPreference, setDockerTabPreference] = createSignal(true);
|
||||
|
||||
const DOCKER_VISIBILITY_EVENT = 'pulse:docker-tab-visibility';
|
||||
const DOCKER_PREFERENCE_KEY = 'pulse-show-docker-tab';
|
||||
|
||||
const readDockerPreference = () => {
|
||||
if (typeof window === 'undefined') return true;
|
||||
const stored = window.localStorage.getItem(DOCKER_PREFERENCE_KEY);
|
||||
return stored !== 'false';
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setDockerTabPreference(readDockerPreference());
|
||||
|
||||
const refreshPreference = (value?: boolean) => {
|
||||
if (typeof value === 'boolean') {
|
||||
setDockerTabPreference(value);
|
||||
return;
|
||||
}
|
||||
setDockerTabPreference(readDockerPreference());
|
||||
};
|
||||
|
||||
const handler = (event: Event) => {
|
||||
if (event instanceof CustomEvent && typeof event.detail?.value === 'boolean') {
|
||||
refreshPreference(event.detail.value);
|
||||
} else {
|
||||
refreshPreference();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(DOCKER_VISIBILITY_EVENT, handler);
|
||||
return () => window.removeEventListener(DOCKER_VISIBILITY_EVENT, handler);
|
||||
});
|
||||
|
||||
// Determine active tab from current path
|
||||
const getActiveTab = () => {
|
||||
const path = location.pathname;
|
||||
if (path.startsWith('/storage')) return 'storage';
|
||||
if (path.startsWith('/backups')) return 'backups';
|
||||
if (path.startsWith('/proxmox')) return 'proxmox';
|
||||
if (path.startsWith('/docker')) return 'docker';
|
||||
if (path.startsWith('/alerts')) return 'alerts';
|
||||
if (path.startsWith('/settings')) return 'settings';
|
||||
return 'main';
|
||||
return 'proxmox';
|
||||
};
|
||||
|
||||
const shouldShowDockerTab = () => {
|
||||
const hosts = props.state().dockerHosts || [];
|
||||
if (hosts.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return dockerTabPreference();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -763,12 +810,13 @@ function AppLayout(props: {
|
||||
>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'main'
|
||||
getActiveTab() === 'proxmox'
|
||||
? 'active bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 -mb-px text-blue-600 dark:text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent'
|
||||
}`}
|
||||
onClick={() => navigate('/')}
|
||||
onClick={() => navigate('/proxmox/overview')}
|
||||
role="tab"
|
||||
title="Proxmox overview, storage, and backups"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
@@ -783,78 +831,29 @@ function AppLayout(props: {
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||
</svg>
|
||||
<span>Main</span>
|
||||
<span>Proxmox</span>
|
||||
</div>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'storage'
|
||||
? 'active bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 -mb-px text-blue-600 dark:text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent'
|
||||
}`}
|
||||
onClick={() => navigate('/storage')}
|
||||
role="tab"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
<Show when={shouldShowDockerTab()}>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'docker'
|
||||
? 'active bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 -mb-px text-blue-600 dark:text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent'
|
||||
}`}
|
||||
onClick={() => navigate('/docker')}
|
||||
role="tab"
|
||||
>
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
<span>Storage</span>
|
||||
</div>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'backups'
|
||||
? 'active bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 -mb-px text-blue-600 dark:text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent'
|
||||
}`}
|
||||
onClick={() => navigate('/backups')}
|
||||
role="tab"
|
||||
title="PVE backups, PBS backups, and VM/CT snapshots"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
<span>Backups</span>
|
||||
</div>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'docker'
|
||||
? 'active bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border-b-0 -mb-px text-blue-600 dark:text-blue-500'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent'
|
||||
}`}
|
||||
onClick={() => navigate('/docker')}
|
||||
role="tab"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338 0-.676.03-1.01.07-.458-1.515-1.877-2.352-3.173-2.352-1.604 0-2.832 1.125-3.254 1.828a.18.18 0 01-.142.084H1.101a.17.17 0 00-.17.171c0 1.047.134 3.528 1.82 5.416.819.915 2.096 2.055 4.563 2.434.766.117 1.582.176 2.427.176 1.066 0 2.14-.118 3.153-.35.88-.202 1.72-.5 2.497-.885.28-.14.53-.295.776-.458.986-.656 1.732-1.5 2.24-2.542.507.362 1.07.546 1.657.546.452 0 .908-.117 1.328-.346.922-.506 1.4-1.528 1.4-2.98 0-.156-.047-.31-.129-.438"/>
|
||||
</svg>
|
||||
<span>Docker</span>
|
||||
</div>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338 0-.676.03-1.01.07-.458-1.515-1.877-2.352-3.173-2.352-1.604 0-2.832 1.125-3.254 1.828a.18.18 0 01-.142.084H1.101a.17.17 0 00-.17.171c0 1.047.134 3.528 1.82 5.416.819.915 2.096 2.055 4.563 2.434.766.117 1.582.176 2.427.176 1.066 0 2.14-.118 3.153-.35.88-.202 1.72-.5 2.497-.885.28-.14.53-.295.776-.458.986-.656 1.732-1.5 2.24-2.542.507.362 1.07.546 1.657.546.452 0 .908-.117 1.328-.346.922-.506 1.4-1.528 1.4-2.98 0-.156-.047-.31-.129-.438"/>
|
||||
</svg>
|
||||
<span>Docker</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class={`tab px-2 sm:px-3 py-1.5 cursor-pointer text-xs sm:text-sm rounded-t flex items-center gap-1 sm:gap-1.5 transition-colors ${
|
||||
getActiveTab() === 'alerts'
|
||||
|
||||
@@ -3,12 +3,15 @@ import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { useWebSocket } from '@/App';
|
||||
import UnifiedBackups from './UnifiedBackups';
|
||||
import { ProxmoxSectionNav } from '@/components/Proxmox/ProxmoxSectionNav';
|
||||
|
||||
const Backups: Component = () => {
|
||||
const { state, connected } = useWebSocket();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="space-y-3">
|
||||
<ProxmoxSectionNav current="backups" />
|
||||
|
||||
{/* Loading State */}
|
||||
<Show when={connected() && !state.pveBackups && !state.pbs}>
|
||||
<Card padding="lg">
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { GuestMetadata } from '@/api/guestMetadata';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
|
||||
import { ProxmoxSectionNav } from '@/components/Proxmox/ProxmoxSectionNav';
|
||||
|
||||
interface DashboardProps {
|
||||
vms: VM[];
|
||||
@@ -514,7 +515,9 @@ export function Dashboard(props: DashboardProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="space-y-3">
|
||||
<ProxmoxSectionNav current="overview" />
|
||||
|
||||
{/* Unified Node Selector */}
|
||||
<UnifiedNodeSelector
|
||||
currentTab="dashboard"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { formatBytes, formatRelativeTime, formatUptime } from '@/utils/format';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { ScrollableTable } from '@/components/shared/ScrollableTable';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { CopyButton } from '@/components/shared/CopyButton';
|
||||
import { MetricBar } from '@/components/Dashboard/MetricBar';
|
||||
import { DockerFilter } from './DockerFilter';
|
||||
// import type { DockerHostSummary } from './DockerHostSummaryTable';
|
||||
@@ -750,17 +751,31 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
||||
title="No Docker hosts configured"
|
||||
description={
|
||||
<span>
|
||||
Deploy the Pulse Docker agent on your Docker hosts to collect container metrics. Review the{' '}
|
||||
Deploy the Pulse Docker agent on at least one Docker host to light up this tab. As soon as an agent reports in, container metrics appear automatically.
|
||||
</span>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<CopyButton
|
||||
text={`docker run -d \ \
|
||||
--name pulse-docker-agent \ \
|
||||
-e PULSE_URL="http://<pulse-server>:8080" \ \
|
||||
-e PULSE_TOKEN="<your-api-token>" \ \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \ \
|
||||
ghcr.io/rcourtman/pulse-docker-agent:latest`}
|
||||
class="w-full sm:w-auto"
|
||||
>
|
||||
Copy install command
|
||||
</CopyButton>
|
||||
<a
|
||||
href="https://github.com/rcourtman/Pulse/blob/main/docs/DOCKER_MONITORING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Docker monitoring guide
|
||||
</a>{' '}
|
||||
for setup instructions.
|
||||
</span>
|
||||
Read the Docker monitoring guide
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Component, JSX } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
|
||||
type ProxmoxSection = 'overview' | 'storage' | 'backups';
|
||||
|
||||
interface ProxmoxSectionNavProps {
|
||||
current: ProxmoxSection;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const sections: Array<{
|
||||
id: ProxmoxSection;
|
||||
label: string;
|
||||
path: string;
|
||||
icon: () => JSX.Element;
|
||||
}> = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
path: '/proxmox/overview',
|
||||
icon: () => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
label: 'Storage',
|
||||
path: '/proxmox/storage',
|
||||
icon: () => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'backups',
|
||||
label: 'Backups',
|
||||
path: '/proxmox/backups',
|
||||
icon: () => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const ProxmoxSectionNav: Component<ProxmoxSectionNavProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const baseClasses =
|
||||
'inline-flex items-center gap-1 px-2 sm:px-3 py-1 rounded-full border text-xs sm:text-sm transition-colors focus:outline-none focus-visible:ring focus-visible:ring-blue-400';
|
||||
|
||||
return (
|
||||
<div class={`flex flex-wrap items-center gap-1 sm:gap-2 ${props.class ?? ''}`} aria-label="Proxmox sections">
|
||||
{sections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isActive = section.id === props.current;
|
||||
const classes = isActive
|
||||
? `${baseClasses} bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 border-blue-200 dark:border-blue-700 shadow-sm`
|
||||
: `${baseClasses} border-transparent text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700/60`;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={classes}
|
||||
onClick={() => navigate(section.path)}
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
<Icon />
|
||||
<span>{section.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, createSignal, Show, For } from 'solid-js';
|
||||
import { Component, createEffect, createSignal, Show, For } from 'solid-js';
|
||||
import { useWebSocket } from '@/App';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
import { formatRelativeTime, formatAbsoluteTime } from '@/utils/format';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
|
||||
export const DockerAgents: Component = () => {
|
||||
const { state } = useWebSocket();
|
||||
@@ -10,6 +11,33 @@ export const DockerAgents: Component = () => {
|
||||
|
||||
const dockerHosts = () => state.dockerHosts || [];
|
||||
|
||||
const STORAGE_KEY = 'pulse-show-docker-tab';
|
||||
const readPreference = () => {
|
||||
if (typeof window === 'undefined') return true;
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
return stored !== 'false';
|
||||
};
|
||||
|
||||
const [showDockerTab, setShowDockerTab] = createSignal(readPreference());
|
||||
|
||||
const persistPreference = (value: boolean) => {
|
||||
setShowDockerTab(value);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, value ? 'true' : 'false');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('pulse:docker-tab-visibility', {
|
||||
detail: { value },
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (dockerHosts().length > 0 && !showDockerTab()) {
|
||||
persistPreference(true);
|
||||
}
|
||||
});
|
||||
|
||||
const pulseUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const protocol = window.location.protocol;
|
||||
@@ -95,6 +123,27 @@ WantedBy=multi-user.target`;
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Card class="space-y-3" padding="lg">
|
||||
<SectionHeader
|
||||
title="Docker tab visibility"
|
||||
description="Hide the Docker tab if you don’t plan to monitor container hosts. We’ll show it automatically once an agent reports in."
|
||||
size="sm"
|
||||
align="left"
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Show Docker tab in navigation
|
||||
</div>
|
||||
<Toggle
|
||||
checked={showDockerTab()}
|
||||
onChange={(event) => persistPreference((event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Preference is saved per browser. Hiding the tab won’t stop existing Docker hosts from reporting metrics.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Deployment Instructions */}
|
||||
<Show when={showInstructions()}>
|
||||
<Card class="space-y-4">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DiskList } from './DiskList';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
|
||||
import { ProxmoxSectionNav } from '@/components/Proxmox/ProxmoxSectionNav';
|
||||
|
||||
const Storage: Component = () => {
|
||||
const { state, connected, activeAlerts, initialDataReceived } = useWebSocket();
|
||||
@@ -496,7 +497,9 @@ const Storage: Component = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="space-y-3">
|
||||
<ProxmoxSectionNav current="storage" />
|
||||
|
||||
{/* Node Selector */}
|
||||
<UnifiedNodeSelector
|
||||
currentTab="storage"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createSignal, onCleanup, splitProps } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
|
||||
interface CopyButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
text: string;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function CopyButton(props: CopyButtonProps) {
|
||||
const [local, others] = splitProps(props, ['text', 'children', 'class', 'onClick']);
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
let resetTimeout: number | undefined;
|
||||
|
||||
const handleClick = async (event: MouseEvent) => {
|
||||
const handler = local.onClick as
|
||||
| ((event: MouseEvent) => void)
|
||||
| { handleEvent?: (event: MouseEvent) => void }
|
||||
| undefined;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
handler(event);
|
||||
} else if (handler && typeof handler.handleEvent === 'function') {
|
||||
handler.handleEvent(event);
|
||||
}
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(local.text);
|
||||
setCopied(true);
|
||||
window.clearTimeout(resetTimeout);
|
||||
resetTimeout = window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard', error);
|
||||
}
|
||||
};
|
||||
|
||||
onCleanup(() => {
|
||||
if (resetTimeout) {
|
||||
window.clearTimeout(resetTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...others}
|
||||
onClick={handleClick}
|
||||
class={`inline-flex items-center justify-center gap-2 rounded-md border border-transparent bg-blue-600 px-4 py-2 text-xs sm:text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-400 ${
|
||||
local.class ?? ''
|
||||
}`}
|
||||
>
|
||||
{copied() ? (
|
||||
<>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8 2a2 2 0 00-2 2v1H5a3 3 0 00-3 3v7a3 3 0 003 3h6a3 3 0 003-3v-1h1a2 2 0 002-2V7.414a2 2 0 00-.586-1.414l-3.414-3.414A2 2 0 0013.586 2H8zm4 4V3.414L15.586 7H13a1 1 0 01-1-1z" />
|
||||
<path d="M3 8a1 1 0 011-1h6a1 1 0 011 1v7a1 1 0 01-1 1H4a1 1 0 01-1-1V8z" />
|
||||
</svg>
|
||||
<span>{local.children}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyButton;
|
||||
Reference in New Issue
Block a user