ui: align docker filters with proxmox
This commit is contained in:
parent
52b0c65977
commit
995c04652c
3 changed files with 179 additions and 56 deletions
|
|
@ -9,6 +9,8 @@ interface DockerFilterProps {
|
|||
setSearch: (value: string) => void;
|
||||
groupingMode?: () => 'grouped' | 'flat';
|
||||
setGroupingMode?: (mode: 'grouped' | 'flat') => void;
|
||||
statusFilter?: () => 'all' | 'online' | 'degraded' | 'offline';
|
||||
setStatusFilter?: (value: 'all' | 'online' | 'degraded' | 'offline') => void;
|
||||
searchInputRef?: (el: HTMLInputElement) => void;
|
||||
onReset?: () => void;
|
||||
activeHostName?: string;
|
||||
|
|
@ -88,12 +90,14 @@ export const DockerFilter: Component<DockerFilterProps> = (props) => {
|
|||
() =>
|
||||
props.search().trim() !== '' ||
|
||||
(!!props.groupingMode && props.groupingMode() === 'flat') ||
|
||||
(!!props.statusFilter && props.statusFilter() !== 'all') ||
|
||||
Boolean(props.activeHostName),
|
||||
);
|
||||
|
||||
const handleReset = () => {
|
||||
props.setSearch('');
|
||||
props.setGroupingMode?.('grouped');
|
||||
props.setStatusFilter?.('all');
|
||||
props.onClearHost?.();
|
||||
props.onReset?.();
|
||||
closeHistory();
|
||||
|
|
@ -296,6 +300,73 @@ export const DockerFilter: Component<DockerFilterProps> = (props) => {
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Show when={props.statusFilter && props.setStatusFilter}>
|
||||
<div class="inline-flex rounded-lg bg-gray-100 p-0.5 dark:bg-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={props.statusFilter?.() === 'all'}
|
||||
onClick={() => props.setStatusFilter?.('all')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.statusFilter?.() === 'all'
|
||||
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Show all hosts"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={props.statusFilter?.() === 'online'}
|
||||
onClick={() =>
|
||||
props.setStatusFilter?.(props.statusFilter?.() === 'online' ? 'all' : 'online')
|
||||
}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.statusFilter?.() === 'online'
|
||||
? 'bg-white dark:bg-gray-800 text-green-600 dark:text-green-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Show online hosts only"
|
||||
>
|
||||
Online
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={props.statusFilter?.() === 'degraded'}
|
||||
onClick={() =>
|
||||
props.setStatusFilter?.(
|
||||
props.statusFilter?.() === 'degraded' ? 'all' : 'degraded',
|
||||
)
|
||||
}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.statusFilter?.() === 'degraded'
|
||||
? 'bg-white dark:bg-gray-800 text-amber-600 dark:text-amber-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Show degraded hosts only"
|
||||
>
|
||||
Degraded
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={props.statusFilter?.() === 'offline'}
|
||||
onClick={() =>
|
||||
props.setStatusFilter?.(
|
||||
props.statusFilter?.() === 'offline' ? 'all' : 'offline',
|
||||
)
|
||||
}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
|
||||
props.statusFilter?.() === 'offline'
|
||||
? 'bg-white dark:bg-gray-800 text-red-600 dark:text-red-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
title="Show offline hosts only"
|
||||
>
|
||||
Offline
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.groupingMode && props.setGroupingMode}>
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ import { useWebSocket } from '@/App';
|
|||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||
import { formatBytes, formatRelativeTime } from '@/utils/format';
|
||||
|
||||
const OFFLINE_HOST_STATUSES = new Set(['offline', 'error', 'unreachable', 'down', 'disconnected']);
|
||||
const DEGRADED_HOST_STATUSES = new Set([
|
||||
'degraded',
|
||||
'warning',
|
||||
'maintenance',
|
||||
'partial',
|
||||
'initializing',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
interface DockerHostsProps {
|
||||
hosts: DockerHost[];
|
||||
activeAlerts?: Record<string, unknown> | any;
|
||||
|
|
@ -40,6 +50,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
const [search, setSearch] = createSignal('');
|
||||
const debouncedSearch = useDebouncedValue(search, 250);
|
||||
const [selectedHostId, setSelectedHostId] = createSignal<string | null>(null);
|
||||
const [statusFilter, setStatusFilter] = createSignal<'all' | 'online' | 'degraded' | 'offline'>('all');
|
||||
|
||||
const clampPercent = (value: number | undefined | null) => {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) return 0;
|
||||
|
|
@ -143,10 +154,59 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
}
|
||||
});
|
||||
|
||||
const hostMatchesStatus = (host: DockerHost) => {
|
||||
const status = statusFilter();
|
||||
if (status === 'all') return true;
|
||||
const normalized = host.status?.toLowerCase() ?? '';
|
||||
if (status === 'online') return normalized === 'online';
|
||||
if (status === 'offline') return OFFLINE_HOST_STATUSES.has(normalized);
|
||||
if (status === 'degraded') {
|
||||
return DEGRADED_HOST_STATUSES.has(normalized) || normalized === 'degraded';
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const filteredHostSummaries = createMemo(() => {
|
||||
const summaries = hostSummaries();
|
||||
if (statusFilter() === 'all') return summaries;
|
||||
return summaries.filter((summary) => hostMatchesStatus(summary.host));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const hostId = selectedHostId();
|
||||
if (!hostId) return;
|
||||
if (!filteredHostSummaries().some((summary) => summary.host.id === hostId)) {
|
||||
setSelectedHostId(null);
|
||||
}
|
||||
});
|
||||
|
||||
const statsFilter = createMemo(() => {
|
||||
const status = statusFilter();
|
||||
if (status === 'all') return null;
|
||||
return { type: 'host-status', value: status };
|
||||
});
|
||||
|
||||
const handleHostSelect = (hostId: string) => {
|
||||
setSelectedHostId((current) => (current === hostId ? null : hostId));
|
||||
};
|
||||
|
||||
const renderFilter = () => (
|
||||
<DockerFilter
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
onReset={() => {
|
||||
setSearch('');
|
||||
setSelectedHostId(null);
|
||||
setStatusFilter('all');
|
||||
}}
|
||||
searchInputRef={(el) => {
|
||||
searchInputRef = el;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="space-y-0">
|
||||
<Show when={isLoading()}>
|
||||
|
|
@ -176,65 +236,57 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
|
|||
|
||||
<Show when={!isLoading()}>
|
||||
<Show
|
||||
when={sortedHosts().length === 0}
|
||||
when={sortedHosts().length > 0}
|
||||
fallback={
|
||||
<>
|
||||
<DockerFilter
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
onReset={() => {
|
||||
setSearch('');
|
||||
setSelectedHostId(null);
|
||||
}}
|
||||
searchInputRef={(el) => {
|
||||
searchInputRef = el;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Show when={hostSummaries().length > 0}>
|
||||
<DockerHostSummaryTable
|
||||
summaries={hostSummaries}
|
||||
selectedHostId={selectedHostId}
|
||||
onSelect={handleHostSelect}
|
||||
{renderFilter()}
|
||||
<Card padding="lg">
|
||||
<EmptyState
|
||||
icon={
|
||||
<svg class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
title="No Docker hosts configured"
|
||||
description="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."
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/settings/docker')}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
<span>Set up Docker agent</span>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<DockerUnifiedTable
|
||||
hosts={sortedHosts()}
|
||||
searchTerm={debouncedSearch()}
|
||||
selectedHostId={selectedHostId}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Card padding="lg">
|
||||
<EmptyState
|
||||
icon={
|
||||
<svg class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
title="No Docker hosts configured"
|
||||
description="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."
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/settings/docker')}
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
<span>Set up Docker agent</span>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
<Show when={hostSummaries().length > 0}>
|
||||
<DockerHostSummaryTable
|
||||
summaries={filteredHostSummaries}
|
||||
selectedHostId={selectedHostId}
|
||||
onSelect={handleHostSelect}
|
||||
/>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
{renderFilter()}
|
||||
|
||||
<DockerUnifiedTable
|
||||
hosts={sortedHosts()}
|
||||
searchTerm={debouncedSearch()}
|
||||
statsFilter={statsFilter()}
|
||||
selectedHostId={selectedHostId}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -648,8 +648,8 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
|
|||
<th class="py-1 pr-2 text-left font-medium">Task</th>
|
||||
<th class="py-1 px-2 text-left font-medium">Node</th>
|
||||
<th class="py-1 px-2 text-left font-medium">State</th>
|
||||
<th class="py-1 px-2 text-left font-medium">CPU</th>
|
||||
<th class="py-1 px-2 text-left font-medium">Memory</th>
|
||||
<th class="py-1 px-2 text-left font-medium w-[120px]">CPU</th>
|
||||
<th class="py-1 px-2 text-left font-medium w-[140px]">Memory</th>
|
||||
<th class="py-1 px-2 text-left font-medium">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -710,12 +710,12 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
|
|||
{task.currentState || task.desiredState || 'Unknown'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-1 px-2">
|
||||
<td class="py-1 px-2 w-[120px]">
|
||||
<Show when={cpu > 0} fallback={<span class="text-gray-400">—</span>}>
|
||||
<MetricBar value={Math.min(100, cpu)} label={formatPercent(cpu)} type="cpu" />
|
||||
</Show>
|
||||
</td>
|
||||
<td class="py-1 px-2">
|
||||
<td class="py-1 px-2 w-[140px]">
|
||||
<Show when={mem > 0} fallback={<span class="text-gray-400">—</span>}>
|
||||
<MetricBar value={Math.min(100, mem)} label={formatPercent(mem)} type="memory" />
|
||||
</Show>
|
||||
|
|
|
|||
Loading…
Reference in a new issue