Refine Proxmox navigation and Docker onboarding

This commit is contained in:
rcourtman
2025-10-05 21:05:46 +00:00
parent 5693560ddd
commit a0bb56acf3
8 changed files with 332 additions and 89 deletions
+78 -79
View File
@@ -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 dont plan to monitor container hosts. Well 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 wont 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;