Unify platform sub-page status toggles with overview compact pills

PlatformTableToolbar was rendering FilterButtonGroup with the default
variant (heavy bg-surface-alt box, border-heavy active state, fixed
blue active color) on every platform sub-page — Replication, Ceph,
Backups, Mail Gateway, Docker Hosts/Services, K8s Clusters/Nodes/
Deployments, TrueNAS Systems, vSphere Hosts. The Proxmox / Docker
overview WorkloadsFilter, by contrast, uses the compact variant with
per-option tone, so "Running" goes green, "Stopped" goes red, etc.

Switch the shared toolbar to compact and enrich the shared status
filter option arrays (PLATFORM_STATUS_FILTER_OPTIONS,
PLATFORM_HEALTH_FILTER_OPTIONS) with tone + leading status dot. Same
treatment for the bespoke filter arrays on Replication (Healthy /
Failed / Pending / Disabled), Ceph (Healthy / Warning / Critical),
and Backups archives + tasks (Protected / Verified / Unverified,
OK / Failed / Running). Backups archives and tasks also flip from
default to compact variant since they bypass PlatformTableToolbar.

Settings panel FilterButtonGroup usages stay default — different
context, intentionally heavier.
This commit is contained in:
rcourtman
2026-05-20 17:08:27 +01:00
parent f58a5f3290
commit 9cb698e736
4 changed files with 36 additions and 23 deletions
@@ -132,18 +132,20 @@ export function PlatformErrorState(props: {
// 'running' vs 'online', 'stopped' vs 'offline') collapse to one chip set.
export type PlatformResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
export const PLATFORM_STATUS_FILTER_OPTIONS: FilterOption<PlatformResourceStatusFilter>[] = [
{ value: 'all', label: 'All' },
{ value: 'online', label: 'Online' },
{ value: 'degraded', label: 'Degraded' },
{ value: 'offline', label: 'Offline' },
{ value: 'online', label: 'Online', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'degraded', label: 'Degraded', tone: 'warning', leading: statusDot('bg-amber-500') },
{ value: 'offline', label: 'Offline', tone: 'danger', leading: statusDot('bg-red-500') },
];
export const PLATFORM_HEALTH_FILTER_OPTIONS: FilterOption<PlatformResourceStatusFilter>[] = [
{ value: 'all', label: 'All' },
{ value: 'online', label: 'Healthy' },
{ value: 'degraded', label: 'Degraded' },
{ value: 'offline', label: 'Offline' },
{ value: 'online', label: 'Healthy', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'degraded', label: 'Degraded', tone: 'warning', leading: statusDot('bg-amber-500') },
{ value: 'offline', label: 'Offline', tone: 'danger', leading: statusDot('bg-red-500') },
];
const ONLINE_STATUSES = new Set<ResourceStatus>(['online', 'running']);
@@ -289,6 +291,7 @@ export function PlatformTableToolbar<T extends string | number>(props: {
/>
</div>
<FilterButtonGroup
variant="compact"
options={props.statusOptions}
value={props.status}
onChange={props.onStatusChange}
@@ -147,18 +147,20 @@ async function fetchPVEBackups(): Promise<PVEBackupsPayload> {
);
}
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
const ARCHIVE_STATUS_FILTERS: FilterOption<'all' | 'protected' | 'verified' | 'unverified'>[] = [
{ value: 'all', label: 'All' },
{ value: 'protected', label: 'Protected' },
{ value: 'verified', label: 'Verified' },
{ value: 'unverified', label: 'Unverified' },
{ value: 'protected', label: 'Protected', tone: 'info', leading: statusDot('bg-blue-500') },
{ value: 'verified', label: 'Verified', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'unverified', label: 'Unverified', tone: 'warning', leading: statusDot('bg-amber-500') },
];
const TASK_STATUS_FILTERS: FilterOption<'all' | 'ok' | 'failed' | 'running'>[] = [
{ value: 'all', label: 'All' },
{ value: 'ok', label: 'OK' },
{ value: 'failed', label: 'Failed' },
{ value: 'running', label: 'Running' },
{ value: 'ok', label: 'OK', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'failed', label: 'Failed', tone: 'danger', leading: statusDot('bg-red-500') },
{ value: 'running', label: 'Running', tone: 'info', leading: statusDot('bg-blue-500') },
];
export const ProxmoxBackupsTable: Component<{
@@ -400,6 +402,7 @@ export const ProxmoxBackupsTable: Component<{
</div>
<Show when={tab() === 'archives'}>
<FilterButtonGroup
variant="compact"
options={ARCHIVE_STATUS_FILTERS}
value={archiveFilter()}
onChange={setArchiveFilter}
@@ -407,6 +410,7 @@ export const ProxmoxBackupsTable: Component<{
</Show>
<Show when={tab() === 'tasks'}>
<FilterButtonGroup
variant="compact"
options={TASK_STATUS_FILTERS}
value={taskFilter()}
onChange={setTaskFilter}
@@ -9,6 +9,7 @@ import {
} from 'solid-js';
import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
import type { FilterOption } from '@/components/shared/FilterButtonGroup';
import { StatusDot } from '@/components/shared/StatusDot';
import type { StatusIndicatorVariant } from '@/utils/status';
import { TableCard } from '@/components/shared/TableCard';
@@ -46,12 +47,14 @@ import { ProxmoxCephClusterDrawer } from './ProxmoxCephClusterDrawer';
type CephStatusFilter = 'all' | 'healthy' | 'warning' | 'critical';
const STATUS_FILTER_OPTIONS = [
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
const STATUS_FILTER_OPTIONS: FilterOption<CephStatusFilter>[] = [
{ value: 'all', label: 'All' },
{ value: 'healthy', label: 'Healthy' },
{ value: 'warning', label: 'Warning' },
{ value: 'critical', label: 'Critical' },
] satisfies Array<{ value: CephStatusFilter; label: string }>;
{ value: 'healthy', label: 'Healthy', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'warning', label: 'Warning', tone: 'warning', leading: statusDot('bg-amber-500') },
{ value: 'critical', label: 'Critical', tone: 'danger', leading: statusDot('bg-red-500') },
];
function classify(resource: Resource): CephStatusFilter {
const raw = (resource.ceph?.healthStatus ?? resource.status ?? '').toUpperCase();
@@ -23,6 +23,7 @@ import {
} from '@/components/shared/Table';
import { apiFetch } from '@/utils/apiClient';
import { formatRelativeTime } from '@/utils/format';
import type { FilterOption } from '@/components/shared/FilterButtonGroup';
import {
PLATFORM_TABLE_BODY_CLASS,
PLATFORM_TABLE_CARD_CLASS,
@@ -42,13 +43,15 @@ import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api';
type ReplicationStatusFilter = 'all' | 'healthy' | 'failed' | 'pending' | 'disabled';
const STATUS_FILTER_OPTIONS = [
const statusDot = (className: string) => <span class={`h-2 w-2 rounded-full ${className}`} />;
const STATUS_FILTER_OPTIONS: FilterOption<ReplicationStatusFilter>[] = [
{ value: 'all', label: 'All' },
{ value: 'healthy', label: 'Healthy' },
{ value: 'failed', label: 'Failed' },
{ value: 'pending', label: 'Pending' },
{ value: 'disabled', label: 'Disabled' },
] satisfies Array<{ value: ReplicationStatusFilter; label: string }>;
{ value: 'healthy', label: 'Healthy', tone: 'success', leading: statusDot('bg-emerald-500') },
{ value: 'failed', label: 'Failed', tone: 'danger', leading: statusDot('bg-red-500') },
{ value: 'pending', label: 'Pending', tone: 'warning', leading: statusDot('bg-amber-500') },
{ value: 'disabled', label: 'Disabled', tone: 'muted', leading: statusDot('bg-slate-400') },
];
interface ReplicationStatusIndicator {
variant: StatusIndicatorVariant;