From ca2579216fb3a9378096b5f3468d5c860f4decfb Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Thu, 25 Sep 2025 14:51:11 -0400 Subject: [PATCH] click overview row to open details --- .../Docker/DockerContainerManagement.vue | 199 ++++++++++-------- .../DockerContainerOverview.standalone.vue | 6 +- .../Docker/DockerContainersTable.vue | 124 ++++++++--- 3 files changed, 210 insertions(+), 119 deletions(-) diff --git a/web/src/components/Docker/DockerContainerManagement.vue b/web/src/components/Docker/DockerContainerManagement.vue index 2b32c42b9..43c004185 100644 --- a/web/src/components/Docker/DockerContainerManagement.vue +++ b/web/src/components/Docker/DockerContainerManagement.vue @@ -1,8 +1,9 @@ diff --git a/web/src/components/Docker/DockerContainerOverview.standalone.vue b/web/src/components/Docker/DockerContainerOverview.standalone.vue index f99fa1c7b..31ca876ab 100644 --- a/web/src/components/Docker/DockerContainerOverview.standalone.vue +++ b/web/src/components/Docker/DockerContainerOverview.standalone.vue @@ -21,10 +21,10 @@ const details = { diff --git a/web/src/components/Docker/DockerContainersTable.vue b/web/src/components/Docker/DockerContainersTable.vue index ea4685ae6..9cc568c02 100644 --- a/web/src/components/Docker/DockerContainersTable.vue +++ b/web/src/components/Docker/DockerContainersTable.vue @@ -197,7 +197,14 @@ function wrapCell(row: { original: TreeRow; depth?: number }, child: VNode) { ? 'ring-2 ring-primary/40' : '' }`, - onClick: () => { + onClick: (e: MouseEvent) => { + const target = e.target as HTMLElement | null; + if ( + target && + target.closest('input,button,textarea,a,[role=checkbox],[role=button],[data-stop-row-click]') + ) { + return; + } const r = row.original as TreeRow; emit('row:click', { id: r.id, type: r.type, name: r.name, containerId: r.containerId }); }, @@ -268,39 +275,58 @@ const columns = computed[]>(() => { const cols: TableColumn[] = [ { id: 'select', - header: ({ table }) => - props.compact - ? '' - : h(UCheckbox, { - modelValue: table.getIsSomePageRowsSelected() - ? 'indeterminate' - : table.getIsAllPageRowsSelected(), - 'onUpdate:modelValue': (value: boolean | 'indeterminate') => - table.toggleAllPageRowsSelected(!!value), - 'aria-label': 'Select all', - }), + header: () => { + if (props.compact) return ''; + const containers = flattenContainerRows(treeData.value); + const totalSelectable = containers.length; + const selectedIds = Object.entries(rowSelection.value) + .filter(([, selected]) => selected) + .map(([id]) => id); + const selectedSet = new Set(selectedIds); + const selectedCount = containers.reduce( + (count, row) => (selectedSet.has(row.id) ? count + 1 : count), + 0 + ); + const allSelected = totalSelectable > 0 && selectedCount === totalSelectable; + const someSelected = selectedCount > 0 && !allSelected; + return h(UCheckbox, { + modelValue: someSelected ? 'indeterminate' : allSelected, + 'onUpdate:modelValue': () => { + const target = someSelected || allSelected ? false : true; + const next: Record = {}; + if (target) { + for (const row of containers) next[row.id] = true; + } + rowSelection.value = next; + }, + 'aria-label': 'Select all', + }); + }, cell: ({ row }) => { switch ((row.original as TreeRow).type) { case 'container': return wrapCell( row, - h(UCheckbox, { - modelValue: row.getIsSelected(), - 'onUpdate:modelValue': (value: boolean | 'indeterminate') => { - const next = !!value; - row.toggleSelected(next); - const r = row.original as TreeRow; - emit('row:select', { - id: r.id, - type: r.type, - name: r.name, - containerId: r.containerId, - selected: next, - }); - }, - 'aria-label': 'Select row', - onClick: (e: Event) => e.stopPropagation(), - }) + h('span', { 'data-stop-row-click': 'true' }, [ + h(UCheckbox, { + modelValue: row.getIsSelected(), + 'onUpdate:modelValue': (value: boolean | 'indeterminate') => { + const next = !!value; + row.toggleSelected(next); + const r = row.original as TreeRow; + emit('row:select', { + id: r.id, + type: r.type, + name: r.name, + containerId: r.containerId, + selected: next, + }); + }, + 'aria-label': 'Select row', + role: 'checkbox', + onClick: (e: Event) => e.stopPropagation(), + }), + ]) ); case 'folder': return wrapCell( @@ -525,6 +551,13 @@ const emit = defineEmits<{ (e: 'update:selectedIds', value: string[]): void; }>(); +function arraysEqualAsSets(a: string[], b: string[]): boolean { + if (a.length !== b.length) return false; + const setA = new Set(a); + for (const id of b) if (!setA.has(id)) return false; + return true; +} + function flattenContainerRows(rows: TreeRow[]): TreeRow[] { const out: TreeRow[] = []; for (const r of rows) { @@ -534,11 +567,21 @@ function flattenContainerRows(rows: TreeRow[]): TreeRow[] { return out; } -// Sync external selectedIds into table rowSelection +// Sync external selectedIds into table rowSelection (driven only by prop changes) watch( - [() => props.selectedIds, treeData], - () => { - const target = new Set(props.selectedIds || []); + () => props.selectedIds, + (newVal) => { + if ( + arraysEqualAsSets( + newVal || [], + Object.entries(rowSelection.value) + .filter(([, s]) => s) + .map(([id]) => id) + ) + ) { + return; + } + const target = new Set(newVal || []); const next: Record = {}; for (const r of flattenContainerRows(treeData.value)) { next[r.id] = target.has(r.id); @@ -548,6 +591,20 @@ watch( { immediate: true } ); +// When tree changes, preserve existing selection for still-present rows +watch( + treeData, + () => { + const valid = new Set(flattenContainerRows(treeData.value).map((r) => r.id)); + const next: Record = {}; + for (const [id, selected] of Object.entries(rowSelection.value)) { + if (valid.has(id) && selected) next[id] = true; + } + rowSelection.value = next; + }, + { deep: false } +); + // Emit external selectedIds when selection changes watch( rowSelection, @@ -1368,6 +1425,7 @@ async function deleteFolderById(id: string) { :columns="columns" :get-row-id="(row: any) => row.id" :get-sub-rows="(row: any) => row.children" + :get-row-can-select="(row: any) => row.original.type === 'container'" :column-filters-options="{ filterFromLeafRows: true }" :loading="loading" :ui="{ td: 'p-0 empty:p-0', thead: compact ? 'hidden' : '', th: compact ? 'hidden' : '' }"