diff --git a/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx b/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx index 90efa67548..a721cf3612 100644 --- a/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx +++ b/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx @@ -1,10 +1,12 @@ -import { For, Show } from 'solid-js'; +import { For, Show, createMemo } from 'solid-js'; import { DrawerDiskListCard, buildDrawerDiskListItems, type DrawerDiskListItem, } from '@/components/Workloads/DrawerDiskListCard'; +import { useResourceDetailDrawerDockerActionsState } from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState'; +import { areSystemSettingsLoaded, shouldHideDockerUpdateActions } from '@/stores/systemSettings'; import type { Resource } from '@/types/resource'; import { formatBytes, @@ -196,8 +198,24 @@ export function DockerHostDrawerOverview(props: DockerHostDrawerOverviewProps) { if (typeof count === 'number') { rows.push({ label: 'Containers', value: `${count}` }); } - if (typeof docker()?.updatesAvailableCount === 'number') { - rows.push({ label: 'Updates', value: `${docker()!.updatesAvailableCount}` }); + const updates = docker()?.updatesAvailableCount; + if (typeof updates === 'number') { + rows.push({ + label: 'Updates', + value: `${updates}`, + valueClass: updates > 0 ? 'text-sky-700 dark:text-sky-300' : undefined, + }); + } + const checkedAt = cleanText(docker()?.updatesLastCheckedAt); + if (checkedAt) { + const parsed = Date.parse(checkedAt); + if (Number.isFinite(parsed)) { + rows.push({ + label: 'Checked', + value: formatRelativeTime(parsed), + title: new Date(parsed).toLocaleString(), + }); + } } if (hasDockerSwarmEvidence(props.host)) { const role = cleanText(docker()?.swarm?.nodeRole); @@ -210,6 +228,30 @@ export function DockerHostDrawerOverview(props: DockerHostDrawerOverviewProps) { return rows; }; + type DockerHostCommandMeta = { + type?: string; + status?: string; + message?: string; + failureReason?: string; + }; + + const hostSourceId = createMemo(() => cleanText(docker()?.hostSourceId) || null); + const updatesAvailable = createMemo(() => docker()?.updatesAvailableCount ?? 0); + const hostCommand = createMemo( + () => docker()?.command as DockerHostCommandMeta | undefined, + ); + const hostCommandActive = createMemo(() => + ['queued', 'dispatched', 'acknowledged', 'in_progress'].includes( + cleanText(hostCommand()?.status).toLowerCase(), + ), + ); + const updateActions = useResourceDetailDrawerDockerActionsState({ + dockerHostSourceId: hostSourceId, + dockerUpdatesAvailable: updatesAvailable, + }); + const updateActionsLoading = () => !areSystemSettingsLoaded(); + const updateAllHidden = () => shouldHideDockerUpdateActions(); + const memoryRows = (): DockerOverviewRow[] => { const memory = memorySource(); if (!memory) return []; @@ -294,7 +336,89 @@ export function DockerHostDrawerOverview(props: DockerHostDrawerOverviewProps) {
- +
+

+ Containers +

+
+ + {(row) => ( +
+ {row.label} + + {row.value} + +
+ )} +
+ +
+ +
+ + {titleCase(cleanText(hostCommand()?.type).replace(/_/g, ' ') || 'Command')} + + + {titleCase(cleanText(hostCommand()?.status).replace(/_/g, ' ') || 'unknown')} + +
+
+ +
+ {updateActions.dockerActionError()} +
+
+ +
+ {updateActions.dockerActionNote()} +
+
+
+ + + + +
+
+
+
+
0} diff --git a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx index d3fa933595..33fc71395c 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx @@ -125,6 +125,32 @@ describe('DockerHostsTable', () => { expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute('type', 'button'); }); + it('surfaces container update actions in the host drawer', () => { + render(() => ( + , + }), + ]} + emptyIcon={} + emptyTitle="No Docker hosts" + emptyDescription="No hosts" + showToolbar={false} + /> + )); + + fireEvent.click(screen.getByText('docker-01').closest('tr')!); + + expect(screen.getByRole('button', { name: 'Check updates' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Update all (3)' })).toBeInTheDocument(); + }); + it('identifies the host system separately from the container runtime', () => { render(() => (