This commit is contained in:
jarek committed 2026-04-26 08:01:32 +02:00
1 parent ccfda4c054
commit 7e3797cbfe
38 files changed
+1115 -274

No files matched your search

+2 -3
View File
@@ -12,6 +12,7 @@
import * as Select from '$lib/components/ui/select';
import * as Tooltip from '$lib/components/ui/tooltip';
import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
import { formatPorts, type PortMapping } from '$lib/utils/port-format';
import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { Badge } from '$lib/components/ui/badge';
@@ -1149,8 +1150,6 @@
}
}
import { formatPorts, type PortMapping } from '$lib/utils/port-format';
function extractHostFromUrl(urlString: string): string | null {
if (!urlString) return null;
@@ -1861,7 +1860,7 @@
{@const remainingCount = ports.length - 1}
<div class="flex {compactPorts ? 'flex-nowrap' : 'flex-wrap'} gap-1">
{#each displayPorts as port}
{@const url = !port.isRange && currentEnvDetails ? getPortUrl(port.publicPort) : null}
{@const url = currentEnvDetails ? getPortUrl(port.publicPort) : null}
{#if url}
<a
href={url}
+63 -5
View File
@@ -6,6 +6,7 @@
import { CircleArrowUp, Loader2, AlertCircle, CheckCircle2, XCircle, ChevronDown, ChevronRight, ExternalLink } from 'lucide-svelte';
import { appendEnvParam } from '$lib/stores/environment';
import { untrack } from 'svelte';
import type { VulnerabilityCriteria } from '$lib/server/db';
import type { StepType } from '$lib/utils/update-steps';
import { getStepLabel, getStepIcon, getStepColor } from '$lib/utils/update-steps';
@@ -78,11 +79,33 @@
let progress = $state<ContainerProgress[]>([]);
let progressListEl = $state<HTMLDivElement | null>(null);
let scrollTick = $state(0);
let userScrolledUp = $state(false);
let currentIndex = $state(0);
let totalCount = $state(0);
let summary = $state<{ total: number; success: number; failed: number; blocked: number } | null>(null);
let errorMessage = $state('');
let forceUpdating = $state<Set<string>>(new Set()); // Track containers being force-updated
let filterMode = $state<'updated' | 'failed'>('updated');
let filteredProgress = $derived(
!summary
? progress
: filterMode === 'failed'
? progress.filter(p => p.step === 'failed' || p.step === 'blocked')
: progress.filter(p => p.step === 'done' || p.success)
);
$effect(() => {
// Only track filterMode, not progress — avoid re-running on every SSE update
const mode = filterMode;
if (mode === 'updated') {
untrack(() => {
for (const item of progress) {
item.showLogs = false;
}
});
}
});
function formatPullLog(entry: PullLogEntry): string {
// Clarify potentially confusing Docker messages
@@ -241,6 +264,9 @@
} else if (data.type === 'complete') {
status = 'complete';
summary = data.summary;
for (const item of progress) {
item.showLogs = false;
}
onComplete({ success: successIds, failed: failedIds, blocked: blockedIds });
} else if (data.type === 'error') {
status = 'error';
@@ -266,6 +292,7 @@
currentIndex = 0;
summary = null;
errorMessage = '';
filterMode = 'updated';
}
function handleOpenChange(isOpen: boolean) {
@@ -384,10 +411,18 @@ const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2,
}
});
// Auto-scroll progress list to bottom on SSE data (not UI toggles)
// Track whether user has scrolled up to read earlier output
function handleProgressScroll() {
if (!progressListEl) return;
const { scrollTop, scrollHeight, clientHeight } = progressListEl;
// Consider "at bottom" if within 50px of the end
userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
}
// Auto-scroll progress list to bottom on SSE data, but only if user hasn't scrolled up
$effect(() => {
scrollTick;
if (progressListEl) {
if (progressListEl && !userScrolledUp) {
requestAnimationFrame(() => {
progressListEl?.scrollTo({ top: progressListEl.scrollHeight, behavior: 'smooth' });
});
@@ -438,10 +473,33 @@ const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2,
<Progress value={progressPercentage} class="h-2" />
</div>
<!-- Container list with status - scrollable area -->
<!-- Filter toggle + Container list with status - scrollable area -->
{#if progress.length > 0}
<div bind:this={progressListEl} class="border rounded-lg divide-y flex-1 min-h-0 overflow-auto">
{#each progress as item (item.containerId)}
{#if summary && (summary.failed > 0 || summary.blocked > 0) && summary.success > 0}
<div class="flex items-center gap-1 shrink-0">
<Button
variant={filterMode === 'updated' ? 'default' : 'outline'}
size="sm"
class="h-7 text-xs"
onclick={() => filterMode = 'updated'}
>
<CheckCircle2 class="w-3 h-3 mr-1" />
Updated ({summary.success})
</Button>
<Button
variant={filterMode === 'failed' ? 'destructive' : 'outline'}
size="sm"
class="h-7 text-xs"
onclick={() => filterMode = 'failed'}
>
<XCircle class="w-3 h-3 mr-1" />
Failed ({summary.failed + summary.blocked})
</Button>
</div>
{/if}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div bind:this={progressListEl} onscroll={handleProgressScroll} class="border rounded-lg divide-y flex-1 min-h-0 overflow-auto">
{#each filteredProgress as item (item.containerId)}
{@const StepIcon = getStepIcon(item.step)}
{@const isActive = item.step !== 'done' && item.step !== 'failed' && item.step !== 'blocked'}
{@const hasLogs = item.pullLogs.length > 0 || item.scanLogs.length > 0 || (item.vulnerabilities && item.vulnerabilities.length > 0)}