From fb37d931ae36e74df6a8c9fa4e03868be57d4abe Mon Sep 17 00:00:00 2001 From: Jarek Krochmalski Date: Sun, 13 Sep 2026 14:50:02 +0200 Subject: [PATCH] choose which stack operations show the full compose log --- src/lib/stores/settings.ts | 20 ++++- src/lib/utils/stack-log-operations.ts | 65 ++++++++++++++++ src/routes/api/settings/general/+server.ts | 36 +++++++-- src/routes/settings/general/GeneralTab.svelte | 35 ++++++++- src/routes/stacks/+page.svelte | 38 +++++++--- tests/stack-log-operations.test.ts | 75 +++++++++++++++++++ 6 files changed, 249 insertions(+), 20 deletions(-) create mode 100644 src/lib/utils/stack-log-operations.ts create mode 100644 tests/stack-log-operations.test.ts diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index 2c5391c..1173f86 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -7,6 +7,11 @@ import { parseTimestamp, type DateTimeFormatters } from '$lib/utils/date-format'; +import { + DEFAULT_STACK_LOG_OPERATIONS, + sanitizeStackLogOperations, + type StackLogOperation +} from '$lib/utils/stack-log-operations'; export type TimeFormat = '12h' | '24h'; export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY'; @@ -59,6 +64,7 @@ export interface AppSettings { // Scanner Advanced settings (#1219). Empty values = use auto-detection. defaultScannerNetworkMode: string; // '' | 'host' | 'bridge' | 'none' | defaultScannerDns: string[]; // ['1.1.1.1', '8.8.8.8']; empty = inherit + stackLogOperations: StackLogOperation[]; // stack ops that show the log popover (#1558) } const DEFAULT_SETTINGS: AppSettings = { @@ -102,6 +108,7 @@ const DEFAULT_SETTINGS: AppSettings = { protectScannerImages: true, defaultScannerNetworkMode: '', defaultScannerDns: [], + stackLogOperations: DEFAULT_STACK_LOG_OPERATIONS, defaultComposeTemplate: `version: "3.8" services: @@ -195,7 +202,8 @@ function createSettingsStore() { showWhatsNew: settings.showWhatsNew ?? DEFAULT_SETTINGS.showWhatsNew, protectScannerImages: settings.protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages, defaultScannerNetworkMode: settings.defaultScannerNetworkMode ?? DEFAULT_SETTINGS.defaultScannerNetworkMode, - defaultScannerDns: Array.isArray(settings.defaultScannerDns) ? settings.defaultScannerDns : DEFAULT_SETTINGS.defaultScannerDns + defaultScannerDns: Array.isArray(settings.defaultScannerDns) ? settings.defaultScannerDns : DEFAULT_SETTINGS.defaultScannerDns, + stackLogOperations: sanitizeStackLogOperations(settings.stackLogOperations) }); } } catch { @@ -257,7 +265,8 @@ function createSettingsStore() { showWhatsNew: updatedSettings.showWhatsNew ?? DEFAULT_SETTINGS.showWhatsNew, protectScannerImages: updatedSettings.protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages, defaultScannerNetworkMode: updatedSettings.defaultScannerNetworkMode ?? DEFAULT_SETTINGS.defaultScannerNetworkMode, - defaultScannerDns: Array.isArray(updatedSettings.defaultScannerDns) ? updatedSettings.defaultScannerDns : DEFAULT_SETTINGS.defaultScannerDns + defaultScannerDns: Array.isArray(updatedSettings.defaultScannerDns) ? updatedSettings.defaultScannerDns : DEFAULT_SETTINGS.defaultScannerDns, + stackLogOperations: sanitizeStackLogOperations(updatedSettings.stackLogOperations) }); } } catch (error) { @@ -291,6 +300,13 @@ function createSettingsStore() { return newSettings; }); }, + setStackLogOperations: (value: StackLogOperation[]) => { + const clean = sanitizeStackLogOperations(value); + update((current) => { + saveSettings({ stackLogOperations: clean }); + return { ...current, stackLogOperations: clean }; + }); + }, setShowStoppedContainers: (value: boolean) => { update((current) => { const newSettings = { ...current, showStoppedContainers: value }; diff --git a/src/lib/utils/stack-log-operations.ts b/src/lib/utils/stack-log-operations.ts new file mode 100644 index 0000000..8674689 --- /dev/null +++ b/src/lib/utils/stack-log-operations.ts @@ -0,0 +1,65 @@ +/** + * Which stack operations show the full compose-log popover (#1558). + * + * Some users find the log popover on every start/stop noisy and just want the + * buttons + a toast. This setting lists the operations for which the popover IS + * shown; operations not in the list run "quiet" (toast only), and their log is + * opened automatically only if the operation FAILS, so errors are never hidden. + * + * Stored as a JSON array of operation keys in the settings KV. Pure + shared so + * the default, the key set, and the "should show?" decision are one source of + * truth for the server, the store, and the stacks page. + */ +// Only operations that actually stream a compose log are listed. Remove is deliberately +// absent: it never opens the log popover (plain toast + error dialog), so a checkbox for it +// would control nothing. +export type StackLogOperation = 'start' | 'stop' | 'restart' | 'deploy' | 'down'; + +/** All operations, in display order, with a human label for the settings list. */ +export const STACK_LOG_OPERATIONS: { key: StackLogOperation; label: string }[] = [ + { key: 'start', label: 'Start' }, + { key: 'stop', label: 'Stop' }, + { key: 'restart', label: 'Restart / recreate' }, + { key: 'deploy', label: 'Deploy / redeploy' }, + { key: 'down', label: 'Down' } +]; + +const ALL_KEYS = STACK_LOG_OPERATIONS.map((o) => o.key); + +/** + * Default: show the log for everything EXCEPT the simple start/stop, matching the + * 1.0.47 behavior for the heavier operations while giving start/stop the quiet + * treatment the request asked for. + */ +export const DEFAULT_STACK_LOG_OPERATIONS: StackLogOperation[] = ['restart', 'deploy', 'down']; + +/** Keep only valid, de-duplicated operation keys (defends the KV value + API body). */ +export function sanitizeStackLogOperations(value: unknown): StackLogOperation[] { + if (!Array.isArray(value)) return [...DEFAULT_STACK_LOG_OPERATIONS]; + const seen = new Set(); + for (const v of value) { + if (typeof v === 'string' && (ALL_KEYS as string[]).includes(v)) seen.add(v as StackLogOperation); + } + return ALL_KEYS.filter((k) => seen.has(k)); // canonical order +} + +/** Whether the log popover should be shown for this operation. */ +export function shouldShowStackLog(ops: StackLogOperation[], op: StackLogOperation): boolean { + return ops.includes(op); +} + +/** + * Decode the stored KV value (a JSON array string) into the operation list. The + * unset-vs-empty distinction is load-bearing: a null/absent value means the user never + * touched the setting -> use the default; a stored "[]" means the user turned every op + * off -> honor it as "log nothing" (NOT the default). Malformed JSON falls back to the + * default rather than throwing. + */ +export function parseStackLogOperationsStorage(raw: string | null | undefined): StackLogOperation[] { + if (raw == null) return [...DEFAULT_STACK_LOG_OPERATIONS]; + try { + return sanitizeStackLogOperations(JSON.parse(raw)); + } catch { + return [...DEFAULT_STACK_LOG_OPERATIONS]; + } +} diff --git a/src/routes/api/settings/general/+server.ts b/src/routes/api/settings/general/+server.ts index 24d4408..ad61214 100644 --- a/src/routes/api/settings/general/+server.ts +++ b/src/routes/api/settings/general/+server.ts @@ -40,6 +40,7 @@ import { refreshSystemJobs } from '$lib/server/scheduler'; import { sendToEventSubprocess, sendToMetricsSubprocess } from '$lib/server/subprocess-manager'; import { DEFAULT_GRYPE_IMAGE, DEFAULT_TRIVY_IMAGE } from '$lib/server/scanner'; import { DEFAULT_HELPER_IMAGE } from '$lib/server/backups/restic'; +import { DEFAULT_STACK_LOG_OPERATIONS, sanitizeStackLogOperations, parseStackLogOperationsStorage, type StackLogOperation } from '$lib/utils/stack-log-operations'; // The real engine default (version-pinned, `-baseline`-aware). NOT a hardcoded // `:latest` — that would advertise a floating tag the backup engine never uses and, @@ -132,6 +133,8 @@ export interface GeneralSettings { // Scanner Advanced settings (#1219). Empty = use auto-detection. defaultScannerNetworkMode: string; defaultScannerDns: string[]; + // Stack operations that show the full compose-log popover (#1558). + stackLogOperations: StackLogOperation[]; } const DEFAULT_SETTINGS: Omit = { @@ -176,6 +179,7 @@ const DEFAULT_SETTINGS: Omit { editorIndentGuides, protectScannerImages, defaultScannerNetworkMode, - defaultScannerDnsRaw + defaultScannerDnsRaw, + stackLogOperationsRaw ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), @@ -347,7 +352,8 @@ export const GET: RequestHandler = async ({ cookies }) => { getSetting('editor_indent_guides'), getSetting('protect_scanner_images'), getSetting('default_scanner_network_mode'), - getSetting('default_scanner_dns') + getSetting('default_scanner_dns'), + getSetting('stack_log_operations') ]); const settings: GeneralSettings = { @@ -405,7 +411,8 @@ export const GET: RequestHandler = async ({ cookies }) => { editorIndentGuides: editorIndentGuides ?? DEFAULT_SETTINGS.editorIndentGuides, protectScannerImages: protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages, defaultScannerNetworkMode: defaultScannerNetworkMode ?? DEFAULT_SETTINGS.defaultScannerNetworkMode, - defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRaw) + defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRaw), + stackLogOperations: parseStackLogOperationsStorage(stackLogOperationsRaw) }; return json(settings); @@ -419,7 +426,7 @@ export const GET: RequestHandler = async ({ cookies }) => { * @openapi * summary: Update global general settings (all fields optional; only supplied keys are written) * description: A large flat settings bag - theme/fonts, scanner defaults, cleanup schedules, event/metrics collection, editor options (e.g. editorIndentGuides), and more. - * body: {animateIcons:boolean, editorIndentGuides:boolean, coloredActionButtons:boolean, lightTheme:string, darkTheme:string, defaultTimezone:string, logBufferSizeKb:integer, externalStackPaths:string, actionIconSize:string, compactPorts:boolean, confirmDestructive:boolean, dateFormat:string, defaultBackupImage:string, defaultComposeTemplate:string, defaultGrypeArgs:string, defaultGrypeImage:string, defaultScannerDns:array, defaultScannerNetworkMode:string, defaultTrivyArgs:string, defaultTrivyImage:string, deployLogReconcileCron:string, deployLogReconcileEnabled:boolean, downloadFormat:string, editorFont:string, eventCleanupCron:string, eventCleanupEnabled:boolean, eventCollectionMode:string, eventPollInterval:integer, eventRetentionDays:integer, font:string, fontSize:string, formatLogTimestamps:boolean, gridFontSize:string, highlightUpdates:boolean, honorProxyLabels:boolean, labelFilterMode:string, logMaxLines:integer, metricsCollectionInterval:integer, primaryStackLocation:string, protectScannerImages:boolean, scannerCleanupCron:string, scannerCleanupEnabled:boolean, scheduleCleanupCron:string, scheduleCleanupEnabled:boolean, scheduleRetentionDays:integer, showExposedPorts:boolean, showGitCommitHash:boolean, showImageChangelogLinks:boolean, showStoppedContainers:boolean, showWhatsNew:boolean, terminalFont:string, timeFormat:string, useSelfhstIcons:boolean} + * body: {animateIcons:boolean, editorIndentGuides:boolean, coloredActionButtons:boolean, lightTheme:string, darkTheme:string, defaultTimezone:string, logBufferSizeKb:integer, externalStackPaths:string, actionIconSize:string, compactPorts:boolean, confirmDestructive:boolean, dateFormat:string, defaultBackupImage:string, defaultComposeTemplate:string, defaultGrypeArgs:string, defaultGrypeImage:string, defaultScannerDns:array, defaultScannerNetworkMode:string, defaultTrivyArgs:string, defaultTrivyImage:string, deployLogReconcileCron:string, deployLogReconcileEnabled:boolean, downloadFormat:string, editorFont:string, eventCleanupCron:string, eventCleanupEnabled:boolean, eventCollectionMode:string, eventPollInterval:integer, eventRetentionDays:integer, font:string, fontSize:string, formatLogTimestamps:boolean, gridFontSize:string, highlightUpdates:boolean, honorProxyLabels:boolean, labelFilterMode:string, logMaxLines:integer, metricsCollectionInterval:integer, primaryStackLocation:string, protectScannerImages:boolean, scannerCleanupCron:string, scannerCleanupEnabled:boolean, scheduleCleanupCron:string, scheduleCleanupEnabled:boolean, scheduleRetentionDays:integer, showExposedPorts:boolean, showGitCommitHash:boolean, showImageChangelogLinks:boolean, showStoppedContainers:boolean, showWhatsNew:boolean, terminalFont:string, timeFormat:string, useSelfhstIcons:boolean, stackLogOperations:array} * resp-403: Permission denied (needs settings:edit) * resp-500: Failed to save settings */ @@ -431,7 +438,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { try { const body = await request.json(); - const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, deployLogReconcileCron, deployLogReconcileEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, showGitCommitHash, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, defaultBackupImage, honorProxyLabels, showImageChangelogLinks, useSelfhstIcons, animateIcons, editorIndentGuides, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns } = body; + const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, deployLogReconcileCron, deployLogReconcileEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, showGitCommitHash, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, defaultBackupImage, honorProxyLabels, showImageChangelogLinks, useSelfhstIcons, animateIcons, editorIndentGuides, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns, stackLogOperations } = body; if (confirmDestructive !== undefined) { await setSetting('confirm_destructive', confirmDestructive); @@ -626,6 +633,10 @@ export const POST: RequestHandler = async ({ request, cookies }) => { .slice(0, 10); // sane upper bound; Docker accepts more but no one needs it await setSetting('default_scanner_dns', JSON.stringify(cleaned)); } + if (stackLogOperations !== undefined && Array.isArray(stackLogOperations)) { + // Store the sanitized list verbatim; an empty array is a valid "log nothing" choice. + await setSetting('stack_log_operations', JSON.stringify(sanitizeStackLogOperations(stackLogOperations))); + } // Fetch all settings in parallel for the response const [ @@ -647,6 +658,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupEnabledVal, scannerCleanupCronVal, scannerCleanupEnabledVal, + deployLogReconcileCronVal, + deployLogReconcileEnabledVal, logBufferSizeKbVal, logMaxLinesVal, defaultTimezoneVal, @@ -679,7 +692,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { editorIndentGuidesVal, protectScannerImagesVal, defaultScannerNetworkModeVal, - defaultScannerDnsRawVal + defaultScannerDnsRawVal, + stackLogOperationsRawVal ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), @@ -699,6 +713,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { getEventCleanupEnabled(), getScannerCleanupCron(), getScannerCleanupEnabled(), + getDeployLogReconcileCron(), + getDeployLogReconcileEnabled(), getSetting('log_buffer_size_kb'), getSetting('log_max_lines'), getDefaultTimezone(), @@ -731,7 +747,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { getSetting('editor_indent_guides'), getSetting('protect_scanner_images'), getSetting('default_scanner_network_mode'), - getSetting('default_scanner_dns') + getSetting('default_scanner_dns'), + getSetting('stack_log_operations') ]); const settings: GeneralSettings = { @@ -753,6 +770,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { eventCleanupEnabled: eventCleanupEnabledVal, scannerCleanupCron: scannerCleanupCronVal, scannerCleanupEnabled: scannerCleanupEnabledVal, + deployLogReconcileCron: deployLogReconcileCronVal, + deployLogReconcileEnabled: deployLogReconcileEnabledVal, logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb, logMaxLines: (typeof logMaxLinesVal === 'number' && logMaxLinesVal > 0) ? Math.min(2000, Math.max(100, logMaxLinesVal)) @@ -787,7 +806,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => { animateIcons: animateIconsVal ?? DEFAULT_SETTINGS.animateIcons, editorIndentGuides: editorIndentGuidesVal ?? DEFAULT_SETTINGS.editorIndentGuides, defaultScannerNetworkMode: defaultScannerNetworkModeVal ?? DEFAULT_SETTINGS.defaultScannerNetworkMode, - defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRawVal) + defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRawVal), + stackLogOperations: parseStackLogOperationsStorage(stackLogOperationsRawVal) }; return json(settings); diff --git a/src/routes/settings/general/GeneralTab.svelte b/src/routes/settings/general/GeneralTab.svelte index 3658ab1..b55bae5 100644 --- a/src/routes/settings/general/GeneralTab.svelte +++ b/src/routes/settings/general/GeneralTab.svelte @@ -8,7 +8,8 @@ import { TogglePill, ToggleSwitch } from '$lib/components/ui/toggle-pill'; import CronEditor from '$lib/components/cron-editor.svelte'; import TimezoneSelector from '$lib/components/TimezoneSelector.svelte'; - import { Eye, Bell, Database, Calendar, ShieldCheck, FileText, AlertTriangle, HelpCircle, Globe, Activity, Clock, Info, Save, RotateCcw, LayoutDashboard, Tags, Archive, ChevronRight, ChevronDown, Compass } from 'lucide-svelte'; + import { Eye, Bell, Database, Calendar, ShieldCheck, FileText, AlertTriangle, HelpCircle, Globe, Activity, Clock, Info, Save, RotateCcw, LayoutDashboard, Tags, Archive, ChevronRight, ChevronDown, Compass, Layers } from 'lucide-svelte'; + import { STACK_LOG_OPERATIONS, type StackLogOperation } from '$lib/utils/stack-log-operations'; import CodeEditor from '$lib/components/CodeEditor.svelte'; import { appSettings, type DateFormat, type DownloadFormat, type EventCollectionMode, type LabelFilterMode } from '$lib/stores/settings'; import { canAccess, authStore } from '$lib/stores/auth'; @@ -42,6 +43,14 @@ let defaultTrivyImage = $derived($appSettings.defaultTrivyImage); let defaultScannerNetworkMode = $derived($appSettings.defaultScannerNetworkMode); let defaultScannerDns = $derived($appSettings.defaultScannerDns); + let stackLogOperations = $derived($appSettings.stackLogOperations); + + function toggleStackLogOperation(op: StackLogOperation, show: boolean) { + const next = show + ? [...stackLogOperations, op] + : stackLogOperations.filter((o) => o !== op); + appSettings.setStackLogOperations(next); + } let showAdvancedScannerSettings = $state(false); let defaultComposeTemplate = $derived($appSettings.defaultComposeTemplate); let labelFilterMode = $derived($appSettings.labelFilterMode); @@ -781,6 +790,30 @@ services: + + + + + Stack operation logs + +

Choose which stack operations open the full compose-log popover. Unchecked operations run quietly with just a toast; their log still opens automatically if the operation fails.

+
+ +
+ {#each STACK_LOG_OPERATIONS as op} +
+ toggleStackLogOperation(op.key, checked)} + disabled={!$canAccess('settings', 'edit')} + /> + +
+ {/each} +
+
+
+ diff --git a/src/routes/stacks/+page.svelte b/src/routes/stacks/+page.svelte index 74e3f5e..c384b1f 100644 --- a/src/routes/stacks/+page.svelte +++ b/src/routes/stacks/+page.svelte @@ -22,6 +22,7 @@ import { extractPangolinUrls } from '$lib/utils/pangolin-urls'; import { extractCaddyUrls } from '$lib/utils/caddy-urls'; import { appSettings } from '$lib/stores/settings'; + import { shouldShowStackLog, type StackLogOperation } from '$lib/utils/stack-log-operations'; import ConfirmPopover from '$lib/components/ConfirmPopover.svelte'; import StackIcon from '$lib/components/StackIcon.svelte'; import ContainerIcon from '$lib/components/ContainerIcon.svelte'; @@ -538,12 +539,16 @@ let composeOutputStackName = $state(undefined); - function startComposeOutput(title: string, stackName?: string) { + // `op` gates the log popover per the user's Settings choice (#1558): the output is + // always buffered (so it's ready if needed), but the popover only opens for ops the + // user opted into. `op` omitted = always show (deploy/git paths that pass no op). + // finishComposeOutput force-opens it on failure, so a quiet op still surfaces errors. + function startComposeOutput(title: string, stackName?: string, op?: StackLogOperation) { composeOutputTitle = title; composeOutputStackName = stackName; composeOutputLines = []; composeOutputRunning = true; - composeOutputOpen = true; + composeOutputOpen = op === undefined || shouldShowStackLog($appSettings.stackLogOperations, op); composeOutputStartedAt = Date.now(); composeOutputOk = undefined; composeOutputMs = undefined; @@ -565,6 +570,9 @@ composeOutputOk = ok; composeOutputMs = Date.now() - composeOutputStartedAt; composeOutputExitCode = exitCode; + // A quiet op (popover suppressed by the setting) still surfaces its log on failure, + // so the user is never left with only a toast when something went wrong (#1558). + if (!ok) composeOutputOpen = true; if (composeOutputLines.length === 0 && output) { composeOutputLines = output.split('\n'); } @@ -1024,7 +1032,7 @@ async function startStack(name: string) { operationError = null; stackActionLoading = name; - startComposeOutput(`Starting ${name}`, name); + startComposeOutput(`Starting ${name}`, name, 'start'); try { const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/start`, envId), { method: 'POST' }); const data = await readJobResponse(response, appendComposeOutputLine); @@ -1053,7 +1061,7 @@ async function stopStack(name: string) { operationError = null; stackActionLoading = name; - startComposeOutput(`Stopping ${name}`, name); + startComposeOutput(`Stopping ${name}`, name, 'stop'); try { const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/stop`, envId), { method: 'POST' }); const data = await readJobResponse(response, appendComposeOutputLine); @@ -1082,7 +1090,7 @@ async function restartStack(name: string, mode: 'restart' | 'ordered' | 'recreate' = 'restart') { operationError = null; stackActionLoading = name; - startComposeOutput(mode === 'recreate' ? `Recreating ${name}` : `Restarting ${name}`, name); + startComposeOutput(mode === 'recreate' ? `Recreating ${name}` : `Restarting ${name}`, name, 'restart'); try { let url = appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/restart`, envId); if (mode === 'recreate' || mode === 'ordered') { @@ -1117,7 +1125,7 @@ async function redeployStack(name: string, options: { pull: boolean; build: boolean; forceRecreate: boolean }) { operationError = null; stackActionLoading = name; - startComposeOutput(`Redeploying ${name}`, name); + startComposeOutput(`Redeploying ${name}`, name, 'deploy'); // Record which redeploy options were chosen so the log shows them (they are not // otherwise visible once the popover closes). const chosen = [ @@ -1163,7 +1171,7 @@ operationError = null; stackActionLoading = name; stackDownLoading = name; - startComposeOutput(`Bringing down ${name}`, name); + startComposeOutput(`Bringing down ${name}`, name, 'down'); try { const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/down`, envId), { method: 'POST' }); const data = await readJobResponse(response, appendComposeOutputLine); @@ -1879,6 +1887,7 @@ redeployStack(stack.name, options)} > @@ -2253,8 +2263,11 @@ align="end" sideOffset={8} > -
- Restart stack {stack.name.length > 20 ? stack.name.slice(0, 20) + '...' : stack.name} +
+ + + Restart stack {stack.name} +