detect newer version tags for pinned images and show them as a badge with release notes

This commit is contained in:
jarek committed 2026-08-19 14:26:45 +02:00
1 parent 29795c074c
commit 99b1672be5
17 files changed
+529 -144

No files matched your search

+47 -38
View File
@@ -78,6 +78,7 @@
import FileBrowserModal from './FileBrowserModal.svelte';
import BatchUpdateModal from './BatchUpdateModal.svelte';
import CheckUpdatesButton from '$lib/components/CheckUpdatesButton.svelte';
import DismissUpdatesButton from '$lib/components/DismissUpdatesButton.svelte';
import BatchOperationModal from '$lib/components/BatchOperationModal.svelte';
import VersionUpdateBadge from '$lib/components/VersionUpdateBadge.svelte';
import VersionUpdateModal from '$lib/components/VersionUpdateModal.svelte';
@@ -131,6 +132,7 @@
// pseudo-status (#1063), which ANDs with actual Docker states.
const STATUS_FILTER_STORAGE_KEY = 'dockhand-containers-status-filter';
const UPDATE_AVAILABLE_FILTER_VALUE = 'update-available';
const NEWER_VERSION_FILTER_VALUE = 'newer-version';
let statusFilter = $state<string[]>([]);
// Status types with icons for filter and table
@@ -325,25 +327,39 @@
const containersWithFailedCheckSet = $derived(new Set($containerStore.failedUpdateIds));
const failedUpdateErrors = $derived($containerStore.failedUpdateErrors);
// Any update indicator on the page - digest updates, newer-version tags, or failed
// checks. Drives the compact "dismiss all" (×) button.
const hasUpdateIndicators = $derived(
$containerStore.pendingUpdateIds.length > 0 ||
hasNewerVersions ||
$containerStore.failedUpdateIds.length > 0
);
// Newer-version-tag (semver) suggestions from the last check, keyed by container ID.
const newerVersionsMap = $derived($containerStore.newerVersions);
// Filter dropdown entries: real statuses plus the synthetic
// "update-available" entry, only offered once we know about a pending
// update — picking it on an empty set would just empty the list (#1063).
const filterOptions = $derived(
containersWithUpdatesSet.size > 0
? [
...statusTypes,
{
value: UPDATE_AVAILABLE_FILTER_VALUE,
label: 'Update available',
icon: CircleArrowUp,
color: 'text-amber-500'
}
]
: statusTypes
);
const filterOptions = $derived([
...statusTypes,
...(containersWithUpdatesSet.size > 0
? [{
value: UPDATE_AVAILABLE_FILTER_VALUE,
label: 'Update available',
icon: CircleArrowUp,
color: 'text-amber-500'
}]
: []),
...(hasNewerVersions
? [{
value: NEWER_VERSION_FILTER_VALUE,
label: 'Newer version',
icon: Tag,
color: 'text-amber-500'
}]
: [])
]);
// Drop the 'update-available' filter when no pending updates remain —
// otherwise the user has no way to deselect it (dropdown hides the
@@ -355,6 +371,11 @@
) {
statusFilter = statusFilter.filter((v) => v !== UPDATE_AVAILABLE_FILTER_VALUE);
}
// Same guard for the newer-version filter: drop it once no container has a
// newer version, or the dropdown hides the entry and the list stays empty.
if (statusFilter.includes(NEWER_VERSION_FILTER_VALUE) && !hasNewerVersions) {
statusFilter = statusFilter.filter((v) => v !== NEWER_VERSION_FILTER_VALUE);
}
});
// Count of updatable containers (excluding system containers like Dockhand/Hawser)
@@ -745,14 +766,20 @@
// Filter by status. The synthetic 'update-available' value (#1063)
// is split off so it ANDs with real-state selections instead of
// being treated like another Docker state.
const stateValues = statusFilter.filter((v) => v !== UPDATE_AVAILABLE_FILTER_VALUE);
const stateValues = statusFilter.filter(
(v) => v !== UPDATE_AVAILABLE_FILTER_VALUE && v !== NEWER_VERSION_FILTER_VALUE
);
const updatesOnly = statusFilter.includes(UPDATE_AVAILABLE_FILTER_VALUE);
const newerVersionOnly = statusFilter.includes(NEWER_VERSION_FILTER_VALUE);
if (stateValues.length > 0) {
result = result.filter((c) => stateValues.includes(c.state.toLowerCase()));
}
if (updatesOnly) {
result = result.filter((c) => containersWithUpdatesSet.has(c.id));
}
if (newerVersionOnly) {
result = result.filter((c) => newerVersionsMap.has(c.id));
}
// Filter by search query
if (searchQuery.trim()) {
@@ -1445,34 +1472,16 @@
>
<CircleArrowUp class="w-3.5 h-3.5" />
Update all ({updatableContainersCount})
<button
type="button"
onclick={(e) => { e.stopPropagation(); dismissPendingUpdates(); }}
class="-mr-1 text-[12px] leading-none rounded-full hover:bg-destructive/20 hover:text-destructive transition-colors opacity-40 hover:opacity-100"
title="Dismiss all update indicators"
>×</button>
</Button>
{/snippet}
</ConfirmPopover>
{/if}
{#if hasNewerVersions}
<!-- Newer version tags. Shown whether or not digest updates also exist.
Only carries its own dismiss (×) when there's no "Update all" button
(whose × already clears both) - so we never show two dismiss controls. -->
<Button
size="sm"
variant="outline"
class="border-amber-500/40 text-amber-600 hover:bg-amber-500/10 hover:border-amber-500"
onclick={updatableContainersCount === 0 ? dismissPendingUpdates : undefined}
title={updatableContainersCount === 0 ? 'Dismiss version indicators' : 'Containers running a pinned tag with a newer version published'}
>
<Tag class="w-3.5 h-3.5" />
{$containerStore.newerVersions.size} newer version{$containerStore.newerVersions.size !== 1 ? 's' : ''}
{#if updatableContainersCount === 0}
<span class="ml-1 text-[12px] leading-none opacity-40">×</span>
{/if}
</Button>
{/if}
<DismissUpdatesButton
show={hasUpdateIndicators}
digestCount={updatableContainersCount}
newerVersionCount={$containerStore.newerVersions.size}
onDismiss={dismissPendingUpdates}
/>
{#if $canAccess('containers', 'remove')}
<ConfirmPopover
open={confirmPrune}