ui: align tables and docker styling

This commit is contained in:
rcourtman 2025-10-28 18:51:52 +00:00
parent f2acdd59af
commit 52b0c65977
4 changed files with 130 additions and 103 deletions

View file

@ -874,7 +874,7 @@ function AppLayout(props: {
id: 'hosts' as const, id: 'hosts' as const,
label: 'Hosts', label: 'Hosts',
route: '/hosts', route: '/hosts',
settingsRoute: '/settings', settingsRoute: '/settings/host-agents',
tooltip: 'Monitor hosts with the host agent', tooltip: 'Monitor hosts with the host agent',
enabled: hasHosts() || !!seenPlatforms()['hosts'], enabled: hasHosts() || !!seenPlatforms()['hosts'],
live: hasHosts(), live: hasHosts(),

View file

@ -926,13 +926,13 @@ export function Dashboard(props: DashboardProps) {
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')} Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[56px] sm:w-[64px] lg:w-[72px] xl:w-[80px] 2xl:w-[90px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[44px] sm:w-[52px] lg:w-[60px] xl:w-[68px] 2xl:w-[80px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('vmid')} onClick={() => handleSort('vmid')}
> >
VMID {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')} VMID {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[76px] sm:w-[86px] lg:w-[96px] xl:w-[108px] 2xl:w-[128px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[60px] sm:w-[70px] lg:w-[80px] xl:w-[92px] 2xl:w-[112px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('uptime')} onClick={() => handleSort('uptime')}
> >
Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')} Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')}

View file

@ -560,13 +560,13 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* VMID */} {/* VMID */}
<td class="py-0.5 px-2 whitespace-nowrap w-[56px] sm:w-[64px] lg:w-[72px] xl:w-[80px] 2xl:w-[90px] text-sm text-gray-600 dark:text-gray-400 align-middle"> <td class="py-0.5 px-1.5 whitespace-nowrap w-[44px] sm:w-[52px] lg:w-[60px] xl:w-[68px] 2xl:w-[80px] text-sm text-gray-600 dark:text-gray-400 align-middle">
{props.guest.vmid} {props.guest.vmid}
</td> </td>
{/* Uptime */} {/* Uptime */}
<td <td
class={`py-0.5 px-2 w-[76px] sm:w-[86px] lg:w-[96px] xl:w-[108px] 2xl:w-[128px] text-sm whitespace-nowrap align-middle ${ class={`py-0.5 px-1.5 w-[60px] sm:w-[70px] lg:w-[80px] xl:w-[92px] 2xl:w-[112px] text-sm whitespace-nowrap align-middle ${
props.guest.uptime < 3600 ? 'text-orange-500' : 'text-gray-600 dark:text-gray-400' props.guest.uptime < 3600 ? 'text-orange-500' : 'text-gray-600 dark:text-gray-400'
}`} }`}
> >

View file

@ -32,7 +32,7 @@ const typeBadgeClass = (type: 'container' | 'service' | 'task' | 'unknown') => {
case 'container': case 'container':
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200'; return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200';
case 'service': case 'service':
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-200'; return 'bg-purple-50 text-purple-700 dark:bg-purple-900/30 dark:text-purple-200';
case 'task': case 'task':
return 'bg-slate-200 text-slate-600 dark:bg-slate-700 dark:text-slate-200'; return 'bg-slate-200 text-slate-600 dark:bg-slate-700 dark:text-slate-200';
default: default:
@ -286,17 +286,6 @@ const statusDotClass = (state: string) => {
} }
}; };
const hostStatusBadge = (host: DockerHost) => {
const status = toLower(host.status);
if (status === 'online') {
return 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300';
}
if (OFFLINE_HOST_STATUSES.has(status)) {
return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300';
}
return 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300';
};
const serviceHealthBadge = (service: DockerService) => { const serviceHealthBadge = (service: DockerService) => {
const desired = service.desiredTasks ?? 0; const desired = service.desiredTasks ?? 0;
const running = service.runningTasks ?? 0; const running = service.runningTasks ?? 0;
@ -341,24 +330,6 @@ const DockerHostGroupHeader: Component<{ host: DockerHost; colspan: number }> =
({props.host.hostname}) ({props.host.hostname})
</span> </span>
</Show> </Show>
<span class={`rounded px-2 py-0.5 text-[10px] font-medium ${hostStatusBadge(props.host)}`}>
{status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown'}
</span>
<Show when={props.host.dockerVersion}>
<span class="text-[10px] text-slate-500 dark:text-slate-400">
Docker {props.host.dockerVersion}
</span>
</Show>
<Show when={uptime}>
<span class="text-[10px] text-slate-500 dark:text-slate-400">
Uptime {formatUptime(uptime!)}
</span>
</Show>
<Show when={lastSeen}>
<span class="text-[10px] text-slate-500 dark:text-slate-400">
Last seen {formatRelativeTime(lastSeen!)}
</span>
</Show>
</div> </div>
</td> </td>
</tr> </tr>
@ -425,6 +396,12 @@ const DockerContainerRow: Component<{ row: Extract<DockerRow, { kind: 'container
return container.status || container.state || 'Unknown'; return container.status || container.state || 'Unknown';
}; };
const containerTitle = () => {
const primary = container.name || container.id || 'Container';
const identifier = container.id && container.name && container.id !== container.name ? container.id : '';
return identifier ? `${primary} \u2014 ${identifier}` : primary;
};
return ( return (
<> <>
<tr <tr
@ -434,22 +411,17 @@ const DockerContainerRow: Component<{ row: Extract<DockerRow, { kind: 'container
onClick={toggle} onClick={toggle}
aria-expanded={expanded()} aria-expanded={expanded()}
> >
<td class="pl-4 pr-2 py-2"> <td class="pl-4 pr-2 py-1">
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-1.5 min-w-0">
<span class={`h-2 w-2 rounded-full ${statusDotClass(state())}`} aria-hidden="true" /> <span class={`h-2 w-2 rounded-full ${statusDotClass(state())}`} aria-hidden="true" />
<div class="min-w-0 flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100"> <div class="flex flex-col sm:flex-row sm:items-center sm:gap-1.5 min-w-0 text-sm text-gray-900 dark:text-gray-100">
<span class="truncate font-semibold" title={container.name || container.id}> <span class="truncate font-semibold" title={containerTitle()}>
{container.name || container.id} {container.name || container.id}
</span> </span>
<Show when={container.id && container.name && container.id !== container.name}>
<span class="hidden sm:inline text-[10px] text-gray-500 dark:text-gray-400 truncate" title={container.id}>
({container.id})
</span>
</Show>
</div> </div>
</div> </div>
</td> </td>
<td class="px-2 py-2"> <td class="px-2 py-1">
<span <span
class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass( class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass(
'container', 'container',
@ -458,17 +430,17 @@ const DockerContainerRow: Component<{ row: Extract<DockerRow, { kind: 'container
Container Container
</span> </span>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300"> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300">
<span class="truncate" title={container.image}> <span class="truncate" title={container.image}>
{container.image || '—'} {container.image || '—'}
</span> </span>
</td> </td>
<td class="px-2 py-2 text-xs"> <td class="px-2 py-1 text-xs">
<span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${statusBadgeClass()}`}> <span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${statusBadgeClass()}`}>
{statusLabel()} {statusLabel()}
</span> </span>
</td> </td>
<td class="px-2 py-2"> <td class="px-2 py-1">
<Show <Show
when={container.cpuPercent && container.cpuPercent > 0} when={container.cpuPercent && container.cpuPercent > 0}
fallback={<span class="text-xs text-gray-400"></span>} fallback={<span class="text-xs text-gray-400"></span>}
@ -476,7 +448,7 @@ const DockerContainerRow: Component<{ row: Extract<DockerRow, { kind: 'container
<MetricBar value={cpuPercent()} label={formatPercent(cpuPercent())} type="cpu" /> <MetricBar value={cpuPercent()} label={formatPercent(cpuPercent())} type="cpu" />
</Show> </Show>
</td> </td>
<td class="px-2 py-2"> <td class="px-2 py-1">
<Show <Show
when={container.memoryUsageBytes && container.memoryUsageBytes > 0} when={container.memoryUsageBytes && container.memoryUsageBytes > 0}
fallback={<span class="text-xs text-gray-400"></span>} fallback={<span class="text-xs text-gray-400"></span>}
@ -489,11 +461,11 @@ const DockerContainerRow: Component<{ row: Extract<DockerRow, { kind: 'container
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300"> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300">
{restarts()} {restarts()}
<span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span> <span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300">{uptime()}</td> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300">{uptime()}</td>
</tr> </tr>
<Show when={expanded() && hasDrawerContent()}> <Show when={expanded() && hasDrawerContent()}>
@ -584,6 +556,12 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
const badge = serviceHealthBadge(service); const badge = serviceHealthBadge(service);
const updatedAt = ensureMs(service.updatedAt ?? service.createdAt); const updatedAt = ensureMs(service.updatedAt ?? service.createdAt);
const serviceTitle = () => {
const primary = service.name || service.id || 'Service';
const identifier = service.id && service.name && service.id !== service.name ? service.id : '';
return identifier ? `${primary} \u2014 ${identifier}` : primary;
};
return ( return (
<> <>
<tr <tr
@ -591,38 +569,31 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
hasTasks() ? 'cursor-pointer' : '' hasTasks() ? 'cursor-pointer' : ''
} ${ } ${
expanded() expanded()
? 'bg-purple-50/90 dark:bg-purple-900/25' ? 'bg-gray-50 dark:bg-gray-800/40'
: 'bg-purple-50/50 dark:bg-purple-900/10 hover:bg-purple-50 dark:hover:bg-purple-900/20' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}`} }`}
onClick={toggle} onClick={toggle}
aria-expanded={expanded()} aria-expanded={expanded()}
> >
<td class="pl-4 pr-2 py-2"> <td class="pl-4 pr-2 py-1">
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-1.5 min-w-0">
<span <span
class={`h-2 w-2 rounded-full ${statusDotClass(badge.label.toLowerCase())}`} class={`h-2 w-2 rounded-full ${statusDotClass(badge.label.toLowerCase())}`}
aria-hidden="true" aria-hidden="true"
/> />
<div class="min-w-0 flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100"> <div class="flex flex-col sm:flex-row sm:items-center sm:gap-1.5 min-w-0 text-sm text-gray-900 dark:text-gray-100">
<span class="truncate font-semibold" title={service.name || service.id || 'Service'}> <span class="truncate font-semibold" title={serviceTitle()}>
{service.name || service.id || 'Service'} {service.name || service.id || 'Service'}
</span> </span>
<span
class={`hidden sm:inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass(
'service',
)}`}
>
Service
</span>
</div> </div>
<Show when={service.stack}> <Show when={service.stack}>
<span class="ml-2 text-[10px] text-gray-500 dark:text-gray-400 truncate hidden md:inline" title={`Stack: ${service.stack}`}> <span class="text-[10px] text-gray-500 dark:text-gray-400 truncate" title={`Stack: ${service.stack}`}>
Stack: {service.stack} Stack: {service.stack}
</span> </span>
</Show> </Show>
</div> </div>
</td> </td>
<td class="px-2 py-2"> <td class="px-2 py-1">
<span <span
class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass( class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass(
'service', 'service',
@ -631,25 +602,25 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
Service Service
</span> </span>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300"> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300">
<span class="truncate" title={service.image}> <span class="truncate" title={service.image}>
{service.image || '—'} {service.image || '—'}
</span> </span>
</td> </td>
<td class="px-2 py-2 text-xs"> <td class="px-2 py-1 text-xs">
<span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${badge.class}`}> <span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${badge.class}`}>
{badge.label} {badge.label}
</span> </span>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-400 dark:text-gray-500"></td> <td class="px-2 py-1 text-xs text-gray-400 dark:text-gray-500"></td>
<td class="px-2 py-2 text-xs text-gray-400 dark:text-gray-500"></td> <td class="px-2 py-1 text-xs text-gray-400 dark:text-gray-500"></td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap"> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap">
<span class="font-semibold text-gray-900 dark:text-gray-100"> <span class="font-semibold text-gray-900 dark:text-gray-100">
{(service.runningTasks ?? 0)}/{service.desiredTasks ?? 0} {(service.runningTasks ?? 0)}/{service.desiredTasks ?? 0}
</span> </span>
<span class="ml-1 text-gray-500 dark:text-gray-400">tasks</span> <span class="ml-1 text-gray-500 dark:text-gray-400">tasks</span>
</td> </td>
<td class="px-2 py-2 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap"> <td class="px-2 py-1 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap">
<Show when={updatedAt} fallback="—"> <Show when={updatedAt} fallback="—">
{(timestamp) => ( {(timestamp) => (
<span title={new Date(timestamp()).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}> <span title={new Date(timestamp()).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}>
@ -663,26 +634,26 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
<Show when={expanded() && hasTasks()}> <Show when={expanded() && hasTasks()}>
<tr class="bg-gray-50 dark:bg-gray-900/60"> <tr class="bg-gray-50 dark:bg-gray-900/60">
<td colSpan={props.columns} class="px-4 py-3"> <td colSpan={props.columns} class="px-4 py-3">
<div class="space-y-2 border-l-4 border-purple-200 dark:border-purple-800 pl-3"> <div class="space-y-2 border-l-4 border-gray-200 dark:border-gray-700 pl-3">
<div class="flex items-center justify-between text-[11px] font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300"> <div class="flex items-center justify-between text-[11px] font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300">
<span>Tasks</span> <span>Tasks</span>
<span class="text-[10px] font-normal text-gray-500 dark:text-gray-400"> <span class="text-[10px] font-normal text-gray-500 dark:text-gray-400">
{tasks.length} {tasks.length === 1 ? 'entry' : 'entries'} {tasks.length} {tasks.length === 1 ? 'entry' : 'entries'}
</span> </span>
</div> </div>
<div class="overflow-x-auto border border-purple-200 dark:border-purple-800/60 rounded-lg bg-white dark:bg-gray-900/60"> <div class="overflow-x-auto border border-gray-200 dark:border-gray-700/60 rounded-lg bg-white dark:bg-gray-900/60">
<table class="min-w-full divide-y divide-purple-100 dark:divide-purple-900/40 text-xs"> <table class="min-w-full divide-y divide-gray-100 dark:divide-gray-800/60 text-xs">
<thead class="bg-purple-50 dark:bg-purple-900/30 text-[10px] uppercase tracking-wide text-purple-700 dark:text-purple-200"> <thead class="bg-gray-100 dark:bg-gray-900/40 text-[10px] uppercase tracking-wide text-gray-600 dark:text-gray-200">
<tr> <tr>
<th class="py-1 pr-2 text-left font-medium">Task</th> <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">Node</th>
<th class="py-1 px-2 text-left font-medium">State</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">CPU</th>
<th class="py-1 px-2 text-left font-medium">Memory</th> <th class="py-1 px-2 text-left font-medium">Memory</th>
<th class="py-1 px-2 text-left font-medium">Updated</th> <th class="py-1 px-2 text-left font-medium">Updated</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-purple-100/60 dark:divide-purple-900/40"> <tbody class="divide-y divide-gray-100 dark:divide-gray-800/50">
<For each={tasks}> <For each={tasks}>
{(task) => { {(task) => {
const container = findContainerForTask(host.containers || [], task); const container = findContainerForTask(host.containers || [], task);
@ -695,6 +666,16 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
if (task.slot !== undefined) return `slot-${task.slot}`; if (task.slot !== undefined) return `slot-${task.slot}`;
return task.id ?? 'Task'; return task.id ?? 'Task';
}; };
const taskTitle = () => {
const label = taskLabel();
if (task.containerId && task.containerId !== label) {
return `${label} \u2014 ${task.containerId}`;
}
if (task.id && task.id !== label) {
return `${label} \u2014 ${task.id}`;
}
return label;
};
const state = toLower(task.currentState ?? task.desiredState ?? 'unknown'); const state = toLower(task.currentState ?? task.desiredState ?? 'unknown');
const stateClass = () => { const stateClass = () => {
if (state === 'running') { if (state === 'running') {
@ -706,10 +687,10 @@ const DockerServiceRow: Component<{ row: Extract<DockerRow, { kind: 'service' }>
return 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300'; return 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300';
}; };
return ( return (
<tr class="hover:bg-purple-50/60 dark:hover:bg-purple-900/20"> <tr class="hover:bg-gray-100 dark:hover:bg-gray-800/40">
<td class="py-1 pr-2"> <td class="py-1 pr-2">
<div class="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100"> <div class="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100">
<span class="truncate font-medium" title={taskLabel()}> <span class="truncate font-medium" title={taskTitle()}>
{taskLabel()} {taskLabel()}
</span> </span>
<span <span
@ -786,21 +767,32 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
return; return;
} }
if (selectedHostId && host.id !== selectedHostId) { if (selectedHostId && host.id !== selectedHostId) {
return; return;
} }
const hostRows: DockerRow[] = []; const containerRows: Array<Extract<DockerRow, { kind: 'container' }>> = [];
const serviceRows: Array<Extract<DockerRow, { kind: 'service' }>> = [];
const containers = host.containers || []; const containers = host.containers || [];
const services = host.services || []; const services = host.services || [];
const tasks = host.tasks || []; const tasks = host.tasks || [];
const serviceNames = new Set<string>();
const serviceIds = new Set<string>();
services.forEach((service) => {
if (service.name) serviceNames.add(service.name.toLowerCase());
if (service.id) serviceIds.add(service.id.toLowerCase());
});
const serviceOwnedContainers = new Set<string>();
containers.forEach((container) => { containers.forEach((container) => {
if (!containerMatchesStateFilter(filter, container)) return; if (!containerMatchesStateFilter(filter, container)) return;
const matchesSearch = searchTokens.every((token) => containerMatchesToken(token, host, container)); const matchesSearch = searchTokens.every((token) => containerMatchesToken(token, host, container));
if (!matchesSearch) return; if (!matchesSearch) return;
hostRows.push({ containerRows.push({
kind: 'container', kind: 'container',
id: container.id || `${host.id}-container-${container.name}`, id: container.id || `${host.id}-container-${container.name}`,
host, host,
@ -823,7 +815,12 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
return false; return false;
}); });
hostRows.push({ associatedTasks.forEach((task) => {
if (task.containerId) serviceOwnedContainers.add(task.containerId.toLowerCase());
if (task.containerName) serviceOwnedContainers.add(task.containerName.toLowerCase());
});
serviceRows.push({
kind: 'service', kind: 'service',
id: service.id || `${host.id}-service-${service.name}`, id: service.id || `${host.id}-service-${service.name}`,
host, host,
@ -832,18 +829,48 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
}); });
}); });
if (hostRows.length > 0) { if (serviceRows.length > 0) {
hostRows.sort((a, b) => { serviceRows.sort((a, b) => {
const nameA = const nameA = a.service.name || a.service.id || '';
a.kind === 'container' const nameB = b.service.name || b.service.id || '';
? a.container.name || a.container.id || ''
: a.service.name || a.service.id || '';
const nameB =
b.kind === 'container'
? b.container.name || b.container.id || ''
: b.service.name || b.service.id || '';
return nameA.localeCompare(nameB); return nameA.localeCompare(nameB);
}); });
}
if (containerRows.length > 0) {
containerRows.sort((a, b) => {
const nameA = a.container.name || a.container.id || '';
const nameB = b.container.name || b.container.id || '';
return nameA.localeCompare(nameB);
});
const filtered = containerRows.filter((row) => {
const idKey = (row.container.id || '').toLowerCase();
const nameKey = (row.container.name || '').toLowerCase();
const shortNameKey = nameKey.split('.')[0];
const labelServiceName =
row.container.labels?.['com.docker.swarm.service.name']?.toLowerCase() ?? '';
const labelServiceID =
row.container.labels?.['com.docker.swarm.service.id']?.toLowerCase() ?? '';
const belongsToService =
(idKey && serviceOwnedContainers.has(idKey)) ||
(nameKey && serviceOwnedContainers.has(nameKey)) ||
(shortNameKey && serviceOwnedContainers.has(shortNameKey)) ||
(labelServiceName && serviceNames.has(labelServiceName)) ||
(labelServiceID && serviceIds.has(labelServiceID)) ||
(serviceNames.size > 0 && nameKey && [...serviceNames].some((svc) => nameKey.startsWith(`${svc}.`)));
return !belongsToService;
});
containerRows.length = 0;
containerRows.push(...filtered);
}
const hostRows = [...serviceRows, ...containerRows];
if (hostRows.length > 0) {
groups.push({ host, rows: hostRows }); groups.push({ host, rows: hostRows });
} }
}); });
@ -908,13 +935,13 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
<table class="w-full min-w-[900px] table-fixed border-collapse"> <table class="w-full min-w-[900px] table-fixed border-collapse">
<thead> <thead>
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600"> <tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
<th class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[22%]"> <th class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[26%]">
Resource Resource
</th> </th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%]"> <th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%]">
Type Type
</th> </th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[22%]"> <th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[18%]">
Image / Stack Image / Stack
</th> </th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%]"> <th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%]">