Skip to content

Commit 5edc55a

Browse files
committed
✨ feat(ui): queue-aware container update state with sequence labels
Replace the naive all-Updating display with proper queue tracking: - GroupUpdateSequenceEntry tracks position/total per container - Queue head renders as "Updating 1 of N", tail as "Queued 2 of N" - isContainerUpdateInProgress defers to queue head position - isContainerUpdateQueued recognizes backend queued + local queue - clearPendingActionState prunes queue/sequence on completion - Detail panels (side + full-page) now show Queued status and disable actions for queued containers
1 parent 18890d5 commit 5edc55a

File tree

9 files changed

+396
-65
lines changed

9 files changed

+396
-65
lines changed

ui/src/components/containers/ContainerFullPageDetail.vue

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const {
1919
confirmDelete,
2020
actionInProgress,
2121
isContainerUpdateInProgress,
22+
isContainerUpdateQueued,
23+
getContainerUpdateSequenceLabel,
2224
error,
2325
registryColorBg,
2426
registryColorText,
@@ -28,23 +30,47 @@ const {
2830
activeDetailTab,
2931
} = useContainersViewTemplateContext();
3032
33+
function isActionQueued(container: { id?: unknown; name?: unknown }) {
34+
return isContainerUpdateQueued(container);
35+
}
36+
3137
function isActionInProgress(container: { id?: unknown; name?: unknown }) {
3238
return (
3339
hasTrackedContainerAction(actionInProgress.value, container) ||
3440
isContainerUpdateInProgress(container)
3541
);
3642
}
3743
44+
function isActionBlocked(container: { id?: unknown; name?: unknown }) {
45+
return isActionInProgress(container) || isActionQueued(container);
46+
}
47+
48+
function formatUpdateStateLabel(
49+
container: { id?: unknown; name?: unknown },
50+
baseLabel: 'Updating' | 'Queued',
51+
) {
52+
const sequence = getContainerUpdateSequenceLabel(container);
53+
return sequence ? `${baseLabel} ${sequence}` : baseLabel;
54+
}
55+
3856
function getStatusLabel(container: { id?: unknown; name?: unknown; status?: string }) {
39-
return isActionInProgress(container) ? 'Updating' : (container.status ?? 'unknown');
57+
if (isActionInProgress(container)) {
58+
return formatUpdateStateLabel(container, 'Updating');
59+
}
60+
if (isActionQueued(container)) {
61+
return formatUpdateStateLabel(container, 'Queued');
62+
}
63+
return container.status ?? 'unknown';
4064
}
4165
4266
function getStatusTone(container: { id?: unknown; name?: unknown; status?: string }) {
43-
return isActionInProgress(container)
44-
? 'warning'
45-
: container.status === 'running'
46-
? 'success'
47-
: 'danger';
67+
if (isActionInProgress(container)) {
68+
return 'warning';
69+
}
70+
if (isActionQueued(container)) {
71+
return 'neutral';
72+
}
73+
return container.status === 'running' ? 'success' : 'danger';
4874
}
4975
</script>
5076

@@ -65,7 +91,7 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
6591
</AppButton>
6692
<div class="flex items-center gap-3 min-w-0">
6793
<StatusDot
68-
:status="isActionInProgress(selectedContainer) ? 'warning' : selectedContainer.status === 'running' ? 'running' : 'stopped'"
94+
:status="isActionBlocked(selectedContainer) ? 'warning' : selectedContainer.status === 'running' ? 'running' : 'stopped'"
6995
:pulse="isActionInProgress(selectedContainer)"
7096
v-tooltip.top="getStatusLabel(selectedContainer)"
7197
size="lg" />
@@ -85,6 +111,11 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
85111
name="spinner"
86112
:size="12"
87113
class="mr-1 dd-spin" />
114+
<AppIcon
115+
v-else-if="isActionQueued(selectedContainer)"
116+
name="clock"
117+
:size="12"
118+
class="mr-1" />
88119
{{ getStatusLabel(selectedContainer) }}
89120
</AppBadge>
90121
<AppBadge
@@ -120,9 +151,9 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
120151
<AppButton size="none" variant="plain" weight="none"
121152
v-if="selectedContainer.status === 'running'"
122153
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-semibold transition-colors"
123-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
154+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
124155
:style="{ backgroundColor: 'var(--dd-danger-muted)', color: 'var(--dd-danger)', border: '1px solid var(--dd-danger)' }"
125-
:disabled="isActionInProgress(selectedContainer)"
156+
:disabled="isActionBlocked(selectedContainer)"
126157
aria-label="Stop container"
127158
@click="confirmStop(selectedContainer)">
128159
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'stop'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />
@@ -131,27 +162,27 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
131162
<AppButton size="none" variant="plain" weight="none"
132163
v-else
133164
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-semibold transition-colors"
134-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
165+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
135166
:style="{ backgroundColor: 'var(--dd-success-muted)', color: 'var(--dd-success)', border: '1px solid var(--dd-success)' }"
136-
:disabled="isActionInProgress(selectedContainer)"
167+
:disabled="isActionBlocked(selectedContainer)"
137168
aria-label="Start container"
138169
@click="startContainer(selectedContainer)">
139170
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'play'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />
140171
Start
141172
</AppButton>
142173
<AppButton size="none" variant="plain" weight="none"
143174
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-semibold transition-colors"
144-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : 'dd-text-muted hover:dd-text'"
145-
:disabled="isActionInProgress(selectedContainer)"
175+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : 'dd-text-muted hover:dd-text'"
176+
:disabled="isActionBlocked(selectedContainer)"
146177
aria-label="Restart container"
147178
@click="confirmRestart(selectedContainer)">
148179
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'restart'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />
149180
Restart
150181
</AppButton>
151182
<AppButton size="none" variant="plain" weight="none"
152183
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-semibold transition-colors"
153-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : 'dd-text-muted hover:dd-text'"
154-
:disabled="isActionInProgress(selectedContainer)"
184+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : 'dd-text-muted hover:dd-text'"
185+
:disabled="isActionBlocked(selectedContainer)"
155186
aria-label="Scan container"
156187
@click="scanContainer(selectedContainer)">
157188
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'security'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />
@@ -160,9 +191,9 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
160191
<AppButton size="none" variant="plain" weight="none"
161192
v-if="selectedContainer.newTag && selectedContainer.bouncer === 'blocked'"
162193
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-bold transition-colors"
163-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
194+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
164195
:style="{ backgroundColor: 'var(--dd-danger-muted)', color: 'var(--dd-danger)', border: '1px solid var(--dd-danger)' }"
165-
:disabled="isActionInProgress(selectedContainer)"
196+
:disabled="isActionBlocked(selectedContainer)"
166197
aria-label="Update blocked by security scan"
167198
@click="confirmForceUpdate(selectedContainer)">
168199
<AppIcon name="lock" :size="12" />
@@ -171,19 +202,19 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
171202
<AppButton size="none" variant="plain" weight="none"
172203
v-else-if="selectedContainer.newTag"
173204
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-bold transition-colors"
174-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
205+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
175206
:style="{ backgroundColor: 'var(--dd-success-muted)', color: 'var(--dd-success)', border: '1px solid var(--dd-success)' }"
176-
:disabled="isActionInProgress(selectedContainer)"
207+
:disabled="isActionBlocked(selectedContainer)"
177208
aria-label="Update container"
178209
@click="confirmUpdate(selectedContainer)">
179210
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'cloud-download'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />
180211
Update
181212
</AppButton>
182213
<AppButton size="none" variant="plain" weight="none"
183214
class="flex items-center gap-1.5 px-3 py-1.5 dd-rounded text-2xs-plus font-semibold transition-colors"
184-
:class="isActionInProgress(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
215+
:class="isActionBlocked(selectedContainer) ? 'opacity-50 cursor-not-allowed' : ''"
185216
:style="{ backgroundColor: 'var(--dd-danger-muted)', color: 'var(--dd-danger)', border: '1px solid var(--dd-danger)' }"
186-
:disabled="isActionInProgress(selectedContainer)"
217+
:disabled="isActionBlocked(selectedContainer)"
187218
aria-label="Delete container"
188219
@click="confirmDelete(selectedContainer)">
189220
<AppIcon :name="isActionInProgress(selectedContainer) ? 'spinner' : 'trash'" :size="12" :class="isActionInProgress(selectedContainer) ? 'dd-spin' : ''" />

ui/src/components/containers/ContainerSideDetail.vue

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const {
1818
activeDetailTab,
1919
actionInProgress,
2020
isContainerUpdateInProgress,
21+
isContainerUpdateQueued,
22+
getContainerUpdateSequenceLabel,
2123
confirmStop,
2224
startContainer,
2325
confirmRestart,
@@ -27,23 +29,47 @@ const {
2729
confirmDelete,
2830
} = useContainersViewTemplateContext();
2931
32+
function isActionQueued(container: { id?: unknown; name?: unknown }) {
33+
return isContainerUpdateQueued(container);
34+
}
35+
3036
function isActionInProgress(container: { id?: unknown; name?: unknown }) {
3137
return (
3238
hasTrackedContainerAction(actionInProgress.value, container) ||
3339
isContainerUpdateInProgress(container)
3440
);
3541
}
3642
43+
function isActionBlocked(container: { id?: unknown; name?: unknown }) {
44+
return isActionInProgress(container) || isActionQueued(container);
45+
}
46+
47+
function formatUpdateStateLabel(
48+
container: { id?: unknown; name?: unknown },
49+
baseLabel: 'Updating' | 'Queued',
50+
) {
51+
const sequence = getContainerUpdateSequenceLabel(container);
52+
return sequence ? `${baseLabel} ${sequence}` : baseLabel;
53+
}
54+
3755
function getStatusLabel(container: { id?: unknown; name?: unknown; status?: string }) {
38-
return isActionInProgress(container) ? 'Updating' : (container.status ?? 'unknown');
56+
if (isActionInProgress(container)) {
57+
return formatUpdateStateLabel(container, 'Updating');
58+
}
59+
if (isActionQueued(container)) {
60+
return formatUpdateStateLabel(container, 'Queued');
61+
}
62+
return container.status ?? 'unknown';
3963
}
4064
4165
function getStatusTone(container: { id?: unknown; name?: unknown; status?: string }) {
42-
return isActionInProgress(container)
43-
? 'warning'
44-
: container.status === 'running'
45-
? 'success'
46-
: 'danger';
66+
if (isActionInProgress(container)) {
67+
return 'warning';
68+
}
69+
if (isActionQueued(container)) {
70+
return 'neutral';
71+
}
72+
return container.status === 'running' ? 'success' : 'danger';
4773
}
4874
</script>
4975

@@ -66,60 +92,60 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
6692
icon="stop"
6793
size="xs"
6894
variant="danger"
69-
:disabled="isActionInProgress(selectedContainer)"
95+
:disabled="isActionBlocked(selectedContainer)"
7096
tooltip="Stop"
7197
@click="confirmStop(selectedContainer)" />
7298
<AppIconButton
7399
v-else
74100
icon="play"
75101
size="xs"
76102
variant="success"
77-
:disabled="isActionInProgress(selectedContainer)"
103+
:disabled="isActionBlocked(selectedContainer)"
78104
tooltip="Start"
79105
@click="startContainer(selectedContainer)" />
80106
<AppIconButton
81107
icon="restart"
82108
size="xs"
83109
variant="muted"
84-
:disabled="isActionInProgress(selectedContainer)"
110+
:disabled="isActionBlocked(selectedContainer)"
85111
tooltip="Restart"
86112
@click="confirmRestart(selectedContainer)" />
87113
<AppIconButton
88114
icon="security"
89115
size="xs"
90116
variant="secondary"
91-
:disabled="isActionInProgress(selectedContainer)"
117+
:disabled="isActionBlocked(selectedContainer)"
92118
tooltip="Scan"
93119
@click="scanContainer(selectedContainer)" />
94120
<AppIconButton
95121
v-if="selectedContainer.newTag && selectedContainer.bouncer === 'blocked'"
96122
icon="lock"
97123
size="xs"
98124
variant="danger"
99-
:disabled="isActionInProgress(selectedContainer)"
125+
:disabled="isActionBlocked(selectedContainer)"
100126
tooltip="Blocked — Force Update"
101127
@click="confirmForceUpdate(selectedContainer)" />
102128
<AppIconButton
103129
v-else-if="selectedContainer.newTag"
104130
icon="cloud-download"
105131
size="xs"
106132
variant="success"
107-
:disabled="isActionInProgress(selectedContainer)"
133+
:disabled="isActionBlocked(selectedContainer)"
108134
tooltip="Update"
109135
@click="confirmUpdate(selectedContainer)" />
110136
<AppIconButton
111137
icon="trash"
112138
size="xs"
113139
variant="danger"
114-
:disabled="isActionInProgress(selectedContainer)"
140+
:disabled="isActionBlocked(selectedContainer)"
115141
tooltip="Delete"
116142
@click="confirmDelete(selectedContainer)" />
117143
</div>
118144
</template>
119145
<template #header>
120146
<div class="flex items-center gap-2 min-w-0">
121147
<StatusDot
122-
:status="isActionInProgress(selectedContainer) ? 'warning' : selectedContainer.status === 'running' ? 'running' : 'stopped'"
148+
:status="isActionBlocked(selectedContainer) ? 'warning' : selectedContainer.status === 'running' ? 'running' : 'stopped'"
123149
:pulse="isActionInProgress(selectedContainer)"
124150
v-tooltip.top="getStatusLabel(selectedContainer)"
125151
size="lg" />
@@ -140,6 +166,11 @@ function getStatusTone(container: { id?: unknown; name?: unknown; status?: strin
140166
name="spinner"
141167
:size="12"
142168
class="mr-1 dd-spin" />
169+
<AppIcon
170+
v-else-if="isActionQueued(selectedContainer)"
171+
name="clock"
172+
:size="12"
173+
class="mr-1" />
143174
{{ getStatusLabel(selectedContainer) }}
144175
</AppBadge>
145176
<AppBadge tone="neutral" size="xs">

ui/src/components/containers/ContainersGroupedViews.vue

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
containerActionsDisabledReason,
2121
isContainerUpdateInProgress,
2222
isContainerUpdateQueued,
23+
getContainerUpdateSequenceLabel,
2324
updateAllInGroup,
2425
tt,
2526
containerViewMode,
@@ -71,12 +72,20 @@ function isContainerQueued(container: { id?: unknown; name?: unknown }) {
7172
return isContainerUpdateQueued(container);
7273
}
7374
75+
function formatContainerUpdateLabel(
76+
container: { id?: unknown; name?: unknown },
77+
baseLabel: 'Updating' | 'Queued',
78+
) {
79+
const sequence = getContainerUpdateSequenceLabel(container);
80+
return sequence ? `${baseLabel} ${sequence}` : baseLabel;
81+
}
82+
7483
function getContainerStatusLabel(container: { id?: unknown; name?: unknown; status?: string }) {
7584
if (isContainerUpdating(container)) {
76-
return 'Updating';
85+
return formatContainerUpdateLabel(container, 'Updating');
7786
}
7887
if (isContainerQueued(container)) {
79-
return 'Queued';
88+
return formatContainerUpdateLabel(container, 'Queued');
8089
}
8190
return container.status ?? 'unknown';
8291
}
@@ -173,8 +182,8 @@ function getContainerStatusIconStyle(container: { id?: unknown; name?: unknown;
173182
@row-click="selectContainer($event)">
174183
<!-- Container icon (own column) -->
175184
<template #cell-icon="{ row: c }">
176-
<AppIcon v-if="isContainerUpdating(c)" name="spinner" :size="14" class="dd-spin dd-text-muted" v-tooltip.top="tt('Updating')" />
177-
<AppIcon v-else-if="isContainerQueued(c)" name="clock" :size="14" class="dd-text-muted" v-tooltip.top="tt('Queued')" />
185+
<AppIcon v-if="isContainerUpdating(c)" name="spinner" :size="14" class="dd-spin dd-text-muted" v-tooltip.top="tt(getContainerStatusLabel(c))" />
186+
<AppIcon v-else-if="isContainerQueued(c)" name="clock" :size="14" class="dd-text-muted" v-tooltip.top="tt(getContainerStatusLabel(c))" />
178187
<ContainerIcon v-else :icon="c.icon" :size="20" />
179188
</template>
180189

@@ -566,13 +575,13 @@ function getContainerStatusIconStyle(container: { id?: unknown; name?: unknown;
566575
class="mt-2 inline-flex items-center gap-1 text-2xs"
567576
style="color: var(--dd-warning);">
568577
<AppIcon name="spinner" :size="12" class="dd-spin shrink-0" />
569-
Updating
578+
{{ formatContainerUpdateLabel(c, 'Updating') }}
570579
</div>
571580
<div
572581
v-else-if="isContainerQueued(c)"
573582
class="mt-2 inline-flex items-center gap-1 text-2xs dd-text-muted">
574583
<AppIcon name="clock" :size="12" class="shrink-0" />
575-
Queued
584+
{{ formatContainerUpdateLabel(c, 'Queued') }}
576585
</div>
577586
<div v-if="c.suggestedTag || c.releaseNotes || c.releaseLink" class="flex items-center gap-2 flex-wrap mt-2">
578587
<SuggestedTagBadge :tag="c.suggestedTag" :current-tag="c.currentTag" />
@@ -652,13 +661,13 @@ function getContainerStatusIconStyle(container: { id?: unknown; name?: unknown;
652661
class="text-2xs mt-0.5 inline-flex items-center gap-1"
653662
style="color: var(--dd-warning);">
654663
<AppIcon name="spinner" :size="10" class="dd-spin shrink-0" />
655-
Updating
664+
{{ formatContainerUpdateLabel(c, 'Updating') }}
656665
</div>
657666
<div
658667
v-else-if="isContainerQueued(c)"
659668
class="text-2xs mt-0.5 inline-flex items-center gap-1 dd-text-muted">
660669
<AppIcon name="clock" :size="10" class="shrink-0" />
661-
Queued
670+
{{ formatContainerUpdateLabel(c, 'Queued') }}
662671
</div>
663672
<div
664673
v-else-if="!c.newTag && c.noUpdateReason"

ui/src/views/ContainersView.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ const {
261261
formatOperationStatus,
262262
formatRollbackReason,
263263
formatTimestamp,
264+
getContainerUpdateSequenceLabel,
264265
getContainerListPolicyState,
265266
getOperationStatusStyle,
266267
getTriggerKey,
@@ -1059,6 +1060,7 @@ provide(containersViewTemplateContextKey, {
10591060
containerActionsEnabled,
10601061
containerActionsDisabledReason,
10611062
actionInProgress,
1063+
getContainerUpdateSequenceLabel,
10621064
isContainerUpdateInProgress,
10631065
isContainerUpdateQueued,
10641066
updateAllInGroup,

0 commit comments

Comments
 (0)