ui: align tables and docker styling
This commit is contained in:
parent
f2acdd59af
commit
52b0c65977
4 changed files with 130 additions and 103 deletions
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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' ? '▲' : '▼')}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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%]">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue