git stack source badge with forge-aware icons and commit hash
This commit is contained in:
9 files changed
+280
-76
No files matched your search
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
// Generic git-repository glyph (non-GitHub/GitLab forge). The official Git
|
||||
// logo (mdi-git) - distinct from lucide's GitBranch, which reads as a branch.
|
||||
// currentColor so it inherits text color; `class` mirrors the lucide icon API.
|
||||
let { class: className = '' }: { class?: string } = $props();
|
||||
</script>
|
||||
|
||||
<svg class={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M2.6,10.59L8.38,4.8L10.07,6.5C9.83,7.35 10.22,8.28 11,8.73V14.27C10.4,14.61 10,15.26 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.26 13.6,14.61 13,14.27V9.41L15.07,11.5C15,11.65 15,11.82 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10C16.82,10 16.65,10 16.5,10.07L13.93,7.5C14.19,6.57 13.71,5.55 12.78,5.16C12.35,5 11.9,4.96 11.5,5.07L9.8,3.38L10.59,2.6C11.37,1.81 12.63,1.81 13.41,2.6L21.4,10.59C22.19,11.37 22.19,12.63 21.4,13.41L13.41,21.4C12.63,22.19 11.37,22.19 10.59,21.4L2.6,13.41C1.81,12.63 1.81,11.37 2.6,10.59Z" />
|
||||
</svg>
|
||||
@@ -17,6 +17,7 @@ export interface AppSettings {
|
||||
confirmDestructive: boolean;
|
||||
showStoppedContainers: boolean;
|
||||
highlightUpdates: boolean;
|
||||
showGitCommitHash: boolean;
|
||||
timeFormat: TimeFormat;
|
||||
dateFormat: DateFormat;
|
||||
downloadFormat: DownloadFormat;
|
||||
@@ -59,6 +60,7 @@ const DEFAULT_SETTINGS: AppSettings = {
|
||||
confirmDestructive: true,
|
||||
showStoppedContainers: true,
|
||||
highlightUpdates: true,
|
||||
showGitCommitHash: false,
|
||||
timeFormat: '24h',
|
||||
dateFormat: 'DD.MM.YYYY',
|
||||
downloadFormat: 'tar',
|
||||
@@ -147,6 +149,7 @@ function createSettingsStore() {
|
||||
confirmDestructive: settings.confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive,
|
||||
showStoppedContainers: settings.showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers,
|
||||
highlightUpdates: settings.highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates,
|
||||
showGitCommitHash: settings.showGitCommitHash ?? DEFAULT_SETTINGS.showGitCommitHash,
|
||||
timeFormat: settings.timeFormat ?? DEFAULT_SETTINGS.timeFormat,
|
||||
dateFormat: settings.dateFormat ?? DEFAULT_SETTINGS.dateFormat,
|
||||
downloadFormat: settings.downloadFormat ?? DEFAULT_SETTINGS.downloadFormat,
|
||||
@@ -205,6 +208,7 @@ function createSettingsStore() {
|
||||
confirmDestructive: updatedSettings.confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive,
|
||||
showStoppedContainers: updatedSettings.showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers,
|
||||
highlightUpdates: updatedSettings.highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates,
|
||||
showGitCommitHash: updatedSettings.showGitCommitHash ?? DEFAULT_SETTINGS.showGitCommitHash,
|
||||
timeFormat: updatedSettings.timeFormat ?? DEFAULT_SETTINGS.timeFormat,
|
||||
dateFormat: updatedSettings.dateFormat ?? DEFAULT_SETTINGS.dateFormat,
|
||||
downloadFormat: updatedSettings.downloadFormat ?? DEFAULT_SETTINGS.downloadFormat,
|
||||
@@ -287,6 +291,13 @@ function createSettingsStore() {
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setShowGitCommitHash: (value: boolean) => {
|
||||
update((current) => {
|
||||
const newSettings = { ...current, showGitCommitHash: value };
|
||||
saveSettings({ showGitCommitHash: value });
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setTimeFormat: (value: TimeFormat) => {
|
||||
update((current) => {
|
||||
const newSettings = { ...current, timeFormat: value };
|
||||
@@ -626,6 +637,15 @@ export function formatDate(date: Date | string | number): string {
|
||||
return formatDatePart(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact relative time like "just now", "5m ago", "2d ago", "3mo ago".
|
||||
* For display ONLY, alongside the absolute date - sort still uses the raw
|
||||
* timestamp, never this string.
|
||||
*/
|
||||
// Re-exported from the import-light utils/format module (so it stays unit-testable
|
||||
// without pulling $app/environment). Kept exported here for existing callers.
|
||||
export { formatRelativeTime } from '$lib/utils/format';
|
||||
|
||||
/**
|
||||
* Get the current time format setting (for components that need it).
|
||||
*/
|
||||
@@ -640,6 +660,14 @@ export function getDateFormat(): DateFormat {
|
||||
return cachedDateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global default timezone setting (for components that format a time and have
|
||||
* no more-specific timezone to use). Falls back to UTC via DEFAULT_SETTINGS.
|
||||
*/
|
||||
export function getDefaultTimezone(): string {
|
||||
return cachedDefaultTimezone;
|
||||
}
|
||||
|
||||
// Regex matching ISO 8601 timestamps at the start of log lines (after optional container prefix)
|
||||
// Matches: 2026-01-12T07:47:44.449821093Z or 2026-01-12T07:47:44Z
|
||||
const ISO_TIMESTAMP_RE = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.\d+)?Z/g;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Github, Gitlab } from 'lucide-svelte';
|
||||
import type { Component } from 'svelte';
|
||||
import GitGenericIcon from '$lib/components/icons/GitGenericIcon.svelte';
|
||||
|
||||
/**
|
||||
* Pick a forge icon from a repo URL: GitHub and GitLab get their own mark,
|
||||
* everything else gets the generic Git logo (mdi-git, not lucide's GitBranch
|
||||
* which reads as a branch).
|
||||
*/
|
||||
export function forgeIcon(url: string | null | undefined): Component {
|
||||
const u = (url || '').toLowerCase();
|
||||
if (u.includes('github.com')) return Github as unknown as Component;
|
||||
if (u.includes('gitlab.')) return Gitlab as unknown as Component;
|
||||
return GitGenericIcon as unknown as Component;
|
||||
}
|
||||
@@ -88,6 +88,8 @@ export interface GeneralSettings {
|
||||
compactPorts: boolean;
|
||||
// Show exposed (internal) ports
|
||||
showExposedPorts: boolean;
|
||||
// Show the deployed git commit hash in the stack source badge
|
||||
showGitCommitHash: boolean;
|
||||
// Log timestamp formatting
|
||||
formatLogTimestamps: boolean;
|
||||
// External stack paths
|
||||
@@ -141,6 +143,7 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
|
||||
metricsCollectionInterval: 30000,
|
||||
compactPorts: false,
|
||||
showExposedPorts: false,
|
||||
showGitCommitHash: false,
|
||||
formatLogTimestamps: false,
|
||||
lightTheme: 'default',
|
||||
darkTheme: 'default',
|
||||
@@ -253,6 +256,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
editorFont,
|
||||
compactPorts,
|
||||
showExposedPorts,
|
||||
showGitCommitHash,
|
||||
formatLogTimestamps,
|
||||
externalStackPaths,
|
||||
primaryStackLocation,
|
||||
@@ -302,6 +306,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getSetting('show_exposed_ports'),
|
||||
getSetting('show_git_commit_hash'),
|
||||
getSetting('format_log_timestamps'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation(),
|
||||
@@ -354,6 +359,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont,
|
||||
editorFont: editorFont ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPorts ?? DEFAULT_SETTINGS.compactPorts,
|
||||
showGitCommitHash: showGitCommitHash ?? DEFAULT_SETTINGS.showGitCommitHash,
|
||||
showExposedPorts: showExposedPorts ?? DEFAULT_SETTINGS.showExposedPorts,
|
||||
formatLogTimestamps: formatLogTimestamps ?? DEFAULT_SETTINGS.formatLogTimestamps,
|
||||
externalStackPaths,
|
||||
@@ -387,7 +393,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, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, defaultBackupImage, honorProxyLabels, showImageChangelogLinks, animateIcons, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns } = body;
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, 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, animateIcons, protectScannerImages, showWhatsNew, defaultScannerNetworkMode, defaultScannerDns } = body;
|
||||
|
||||
if (confirmDestructive !== undefined) {
|
||||
await setSetting('confirm_destructive', confirmDestructive);
|
||||
@@ -498,6 +504,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (editorFont !== undefined && VALID_EDITOR_FONTS.includes(editorFont)) {
|
||||
await setSetting('theme_editor_font', editorFont);
|
||||
}
|
||||
if (showGitCommitHash !== undefined) {
|
||||
await setSetting('show_git_commit_hash', showGitCommitHash);
|
||||
}
|
||||
if (compactPorts !== undefined) {
|
||||
await setSetting('compact_ports', compactPorts);
|
||||
}
|
||||
@@ -601,6 +610,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
editorFontVal,
|
||||
compactPortsVal,
|
||||
showExposedPortsVal,
|
||||
showGitCommitHashVal,
|
||||
formatLogTimestampsVal,
|
||||
externalStackPathsVal,
|
||||
primaryStackLocationVal,
|
||||
@@ -650,6 +660,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getSetting('show_exposed_ports'),
|
||||
getSetting('show_git_commit_hash'),
|
||||
getSetting('format_log_timestamps'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation(),
|
||||
@@ -703,6 +714,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
editorFont: editorFontVal ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPortsVal ?? DEFAULT_SETTINGS.compactPorts,
|
||||
showExposedPorts: showExposedPortsVal ?? DEFAULT_SETTINGS.showExposedPorts,
|
||||
showGitCommitHash: showGitCommitHashVal ?? DEFAULT_SETTINGS.showGitCommitHash,
|
||||
formatLogTimestamps: formatLogTimestampsVal ?? DEFAULT_SETTINGS.formatLogTimestamps,
|
||||
externalStackPaths: externalStackPathsVal,
|
||||
primaryStackLocation: primaryStackLocationVal,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
let highlightUpdates = $derived($appSettings.highlightUpdates);
|
||||
let compactPorts = $derived($appSettings.compactPorts);
|
||||
let showExposedPorts = $derived($appSettings.showExposedPorts);
|
||||
let showGitCommitHash = $derived($appSettings.showGitCommitHash);
|
||||
let honorProxyLabels = $derived($appSettings.honorProxyLabels);
|
||||
let showImageChangelogLinks = $derived($appSettings.showImageChangelogLinks);
|
||||
let showWhatsNew = $derived($appSettings.showWhatsNew);
|
||||
@@ -411,6 +412,28 @@ services:
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Display internal container ports in the container list grid</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<Label>Show git commit hash</Label>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top" class="w-96 max-w-[90vw]">
|
||||
<p>Shows the deployed short commit hash on the Git badge in the stacks list source column, with the full hash, repo URL and branch in a tooltip.</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<TogglePill
|
||||
checked={showGitCommitHash}
|
||||
onchange={(checked) => {
|
||||
appSettings.setShowGitCommitHash(checked);
|
||||
toast.success(checked ? 'Showing git commit hash on stack badges' : 'Git commit hash hidden');
|
||||
}}
|
||||
disabled={!$canAccess('settings', 'edit')}
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">Display the deployed commit hash on the Git source badge in the stacks list</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<Label>Honor Traefik/Pangolin labels</Label>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Plus, Trash2, Pencil, GitBranch, FolderGit2, Plug, CheckCircle, XCircle, Loader2, Github, Lock, Globe } from 'lucide-svelte';
|
||||
import { Plus, Trash2, Pencil, GitBranch, FolderGit2, Plug, CheckCircle, XCircle, Loader2, Lock, Globe } from 'lucide-svelte';
|
||||
import { forgeIcon } from '$lib/utils/git-forge';
|
||||
import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
|
||||
import { canAccess } from '$lib/stores/auth';
|
||||
import GitRepositoryModal from './GitRepositoryModal.svelte';
|
||||
@@ -160,13 +161,10 @@
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
{#each repositories as repo (repo.id)}
|
||||
{@const ForgeIcon = forgeIcon(repo.url)}
|
||||
<div class="flex items-center justify-between py-2 px-3 rounded-md border bg-card hover:bg-muted/50 transition-colors">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
{#if repo.url.includes('github.com')}
|
||||
<Github class="w-4 h-4 shrink-0 text-muted-foreground" />
|
||||
{:else}
|
||||
<FolderGit2 class="w-4 h-4 shrink-0 text-muted-foreground" />
|
||||
{/if}
|
||||
<ForgeIcon class="w-4 h-4 shrink-0 text-muted-foreground" />
|
||||
<span class="font-medium text-sm truncate">{repo.name}</span>
|
||||
<span class="text-xs text-muted-foreground truncate hidden sm:inline">{repo.url}</span>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import BatchOperationModal from '$lib/components/BatchOperationModal.svelte';
|
||||
import type { ComposeStackInfo, ContainerStats } from '$lib/types';
|
||||
import StackModal from './StackModal.svelte';
|
||||
import GitSourceBadge from './GitSourceBadge.svelte';
|
||||
import GitStackModal from './GitStackModal.svelte';
|
||||
import ImportStackModal from './ImportStackModal.svelte';
|
||||
import GitDeployProgressPopover from './GitDeployProgressPopover.svelte';
|
||||
@@ -70,6 +71,7 @@
|
||||
let showImportModal = $state(false);
|
||||
let editingStackName = $state('');
|
||||
let stackModalReadonly = $state(false);
|
||||
let stackModalGitInfo = $state<{ commit?: string; url?: string; branch?: string } | null>(null);
|
||||
let editingGitStack = $state<any>(null);
|
||||
let envId = $state<number | null>(null);
|
||||
|
||||
@@ -492,7 +494,15 @@
|
||||
let confirmRemoveContainerId = $state<string | null>(null);
|
||||
|
||||
// Operation error state (for stack and container operations)
|
||||
let operationError = $state<{ id: string; title: string; message: string } | null>(null);
|
||||
let operationError = $state<{ id: string; message: string } | null>(null);
|
||||
|
||||
// Inline operation-error auto-dismiss timers, cleared on destroy.
|
||||
let pendingTimeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
function clearErrorAfterDelay(id: string) {
|
||||
pendingTimeouts.push(setTimeout(() => {
|
||||
if (operationError?.id === id) operationError = null;
|
||||
}, 5000));
|
||||
}
|
||||
|
||||
// Error dialog state (for showing detailed errors)
|
||||
let errorDialogData = $state<{ title: string; message: string } | null>(null);
|
||||
@@ -1084,6 +1094,12 @@
|
||||
function viewGitStack(name: string) {
|
||||
editingStackName = name;
|
||||
stackModalReadonly = true;
|
||||
const src = getStackSource(name);
|
||||
stackModalGitInfo = {
|
||||
commit: src?.gitStack?.lastCommit || undefined,
|
||||
url: src?.repository?.url || undefined,
|
||||
branch: src?.repository?.branch || undefined
|
||||
};
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
@@ -1369,6 +1385,10 @@
|
||||
|
||||
// Cleanup on component destroy
|
||||
onDestroy(() => {
|
||||
// Clear pending inline-error dismiss timers
|
||||
pendingTimeouts.forEach(clearTimeout);
|
||||
pendingTimeouts = [];
|
||||
|
||||
// Clear polling intervals
|
||||
if (stacksInterval) {
|
||||
clearInterval(stacksInterval);
|
||||
@@ -1684,7 +1704,7 @@
|
||||
{stackEnvVarCounts[stack.name]}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="whitespace-nowrap">
|
||||
<Tooltip.Content>
|
||||
{stackEnvVarCounts[stack.name]} environment variable{stackEnvVarCounts[stack.name] !== 1 ? 's' : ''} configured
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
@@ -1737,7 +1757,7 @@
|
||||
<span class="text-2xs font-medium text-amber-500 leading-none">{stack.updateCount}</span>
|
||||
{/if}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="whitespace-nowrap">
|
||||
<Tooltip.Content>
|
||||
External stack - update possible for individual containers only.
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
@@ -1746,42 +1766,19 @@
|
||||
</span>
|
||||
{:else if column.id === 'source'}
|
||||
{#if source.sourceType === 'git'}
|
||||
{#if source.gitStack?.lastCommit}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span
|
||||
class="inline-flex items-center justify-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 shadow-sm min-w-[5.5rem]"
|
||||
>
|
||||
<GitBranch class="w-3 h-3" />
|
||||
<span>Git</span>
|
||||
<span class="font-mono text-[10px] opacity-75">{source.gitStack.lastCommit}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="whitespace-nowrap">
|
||||
<code class="text-xs">{source.gitStack.lastCommit}</code>
|
||||
{#if source.repository}
|
||||
<br />
|
||||
<span class="text-xs">{source.repository.url} ({source.repository.branch})</span>
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex items-center justify-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 shadow-sm min-w-[5.5rem]"
|
||||
title={source.repository ? `${source.repository.url} (${source.repository.branch})` : 'Deployed from Git repository'}
|
||||
>
|
||||
<GitBranch class="w-3 h-3" />
|
||||
Git
|
||||
</span>
|
||||
{/if}
|
||||
<GitSourceBadge {source} />
|
||||
{:else if source.sourceType === 'internal'}
|
||||
<span
|
||||
class="inline-flex items-center justify-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 shadow-sm min-w-[5.5rem]"
|
||||
title="Managed by Dockhand"
|
||||
>
|
||||
<FileCode class="w-3 h-3" />
|
||||
Internal
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span
|
||||
class="inline-flex items-center justify-center gap-1 text-xs px-1.5 py-0.5 rounded-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 shadow-sm min-w-[5.5rem]"
|
||||
>
|
||||
<FileCode class="w-3 h-3" />
|
||||
Internal
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Managed by Dockhand</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
@@ -1792,7 +1789,7 @@
|
||||
Untracked
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="whitespace-nowrap">
|
||||
<Tooltip.Content>
|
||||
Compose file location unknown. Click the stack name or edit button to locate it.
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
@@ -1816,40 +1813,70 @@
|
||||
{:else if column.id === 'containers'}
|
||||
<div class="flex items-center gap-1">
|
||||
{#if getContainerStateCounts(stack).running}
|
||||
<span class="inline-flex items-center gap-0.5 text-emerald-600 dark:text-emerald-400" title="Running">
|
||||
<Play class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).running}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-emerald-600 dark:text-emerald-400">
|
||||
<Play class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).running}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Running</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if getContainerStateCounts(stack).exited}
|
||||
<span class="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400" title="Exited">
|
||||
<Square class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).exited}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-red-600 dark:text-red-400">
|
||||
<Square class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).exited}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Exited</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if getContainerStateCounts(stack).paused}
|
||||
<span class="inline-flex items-center gap-0.5 text-amber-600 dark:text-amber-400" title="Paused">
|
||||
<Pause class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).paused}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-amber-600 dark:text-amber-400">
|
||||
<Pause class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).paused}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Paused</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if getContainerStateCounts(stack).restarting}
|
||||
<span class="inline-flex items-center gap-0.5 text-blue-600 dark:text-blue-400" title="Restarting">
|
||||
<span class="w-3.5 h-3.5 flex items-center justify-center"><RefreshCw class="w-3.5 h-3.5 animate-spin" /></span>
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).restarting}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-blue-600 dark:text-blue-400">
|
||||
<span class="w-3.5 h-3.5 flex items-center justify-center"><RefreshCw class="w-3.5 h-3.5 animate-spin" /></span>
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).restarting}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Restarting</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if getContainerStateCounts(stack).created}
|
||||
<span class="inline-flex items-center gap-0.5 text-slate-500 dark:text-slate-400" title="Created">
|
||||
<CircleDashed class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).created}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-slate-500 dark:text-slate-400">
|
||||
<CircleDashed class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).created}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Created</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if getContainerStateCounts(stack).dead}
|
||||
<span class="inline-flex items-center gap-0.5 text-rose-700 dark:text-rose-400" title="Dead">
|
||||
<Skull class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).dead}</span>
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<span class="inline-flex items-center gap-0.5 text-rose-700 dark:text-rose-400">
|
||||
<Skull class="w-3.5 h-3.5" />
|
||||
<span class="text-xs font-medium">{getContainerStateCounts(stack).dead}</span>
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>Dead</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
{#if stack.containers.length === 0}
|
||||
<span class="text-xs text-muted-foreground">-</span>
|
||||
@@ -2606,10 +2633,12 @@
|
||||
mode="edit"
|
||||
stackName={editingStackName}
|
||||
readonly={stackModalReadonly}
|
||||
gitInfo={stackModalGitInfo}
|
||||
onClose={() => {
|
||||
showEditModal = false;
|
||||
editingStackName = '';
|
||||
stackModalReadonly = false;
|
||||
stackModalGitInfo = null;
|
||||
}}
|
||||
onSuccess={fetchStacks}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { GitBranch, GitCommitHorizontal } from 'lucide-svelte';
|
||||
import { forgeIcon } from '$lib/utils/git-forge';
|
||||
import { appSettings } from '$lib/stores/settings';
|
||||
|
||||
interface Props {
|
||||
source: { repository?: { url?: string; branch?: string } | null; gitStack?: { lastCommit?: string | null } | null };
|
||||
}
|
||||
let { source }: Props = $props();
|
||||
|
||||
const ForgeIcon = $derived(forgeIcon(source.repository?.url));
|
||||
const showHash = $derived(!!source.gitStack?.lastCommit && $appSettings.showGitCommitHash);
|
||||
// A tooltip is only worth showing when there's real git info in it.
|
||||
const hasGitInfo = $derived(!!source.gitStack?.lastCommit || !!source.repository);
|
||||
</script>
|
||||
|
||||
{#snippet badge()}
|
||||
<span
|
||||
class="inline-flex max-w-full items-center justify-center gap-1 overflow-hidden text-xs px-1.5 py-0.5 rounded-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 shadow-sm {showHash ? '' : 'min-w-[5.5rem]'}"
|
||||
>
|
||||
<ForgeIcon class="w-3 h-3 shrink-0" />
|
||||
<span class="shrink-0">Git</span>
|
||||
{#if showHash}
|
||||
<span class="font-mono text-[10px] opacity-75 truncate">{source.gitStack!.lastCommit}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#if hasGitInfo}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="block max-w-full overflow-hidden">
|
||||
{@render badge()}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content class="p-0">
|
||||
<div class="flex flex-col gap-1.5 px-3 py-2">
|
||||
{#if source.gitStack?.lastCommit}
|
||||
<div class="flex items-center gap-2">
|
||||
<GitCommitHorizontal class="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
<code class="text-xs">{source.gitStack.lastCommit}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{#if source.repository}
|
||||
{#if source.repository.branch}
|
||||
<div class="flex items-center gap-2">
|
||||
<GitBranch class="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="text-xs">{source.repository.branch}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<ForgeIcon class="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="text-xs">{source.repository.url}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
{@render badge()}
|
||||
{/if}
|
||||
@@ -7,7 +7,7 @@
|
||||
import CodeEditor, { type VariableMarker } from '$lib/components/CodeEditor.svelte';
|
||||
import StackEnvVarsPanel from '$lib/components/StackEnvVarsPanel.svelte';
|
||||
import { type EnvVar, type ValidationResult } from '$lib/components/StackEnvVarsEditor.svelte';
|
||||
import { Layers, Save, Play, Code, GitGraph, GitBranch, Loader2, AlertCircle, X, Sun, Moon, TriangleAlert, GripVertical, FolderOpen, Copy, Check, XCircle, MapPin, ArrowRight, ArrowDown, Info, Box, FolderSync, Archive } from 'lucide-svelte';
|
||||
import { Layers, Save, Play, Code, GitGraph, GitBranch, GitCommitHorizontal, Github, Loader2, AlertCircle, X, Sun, Moon, TriangleAlert, GripVertical, FolderOpen, Copy, Check, XCircle, MapPin, ArrowRight, ArrowDown, Info, Box, FolderSync, Archive } from 'lucide-svelte';
|
||||
import BackupPanel from '../containers/BackupPanel.svelte';
|
||||
import { volumesForStack, type VolumeInfo } from '$lib/utils/mounts';
|
||||
import { fetchBackupExecutions } from '$lib/utils/backup';
|
||||
@@ -27,10 +27,7 @@
|
||||
import { readJobResponse } from '$lib/utils/sse-fetch';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import ComposeGraphViewer from './ComposeGraphViewer.svelte';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
|
||||
|
||||
// Get sidebar state to adjust modal positioning
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// localStorage key for persisted split ratio
|
||||
const STORAGE_KEY_SPLIT = 'dockhand-stack-modal-split';
|
||||
@@ -42,11 +39,15 @@
|
||||
initialCompose?: string; // Pre-fill compose content (for library deploy)
|
||||
initialStackName?: string; // Pre-fill stack name (for library deploy)
|
||||
readonly?: boolean; // View compose content without allowing local changes
|
||||
gitInfo?: { commit?: string; url?: string; branch?: string } | null; // Git provenance for read-only git stacks
|
||||
onClose: () => void;
|
||||
onSuccess: () => void; // Called after create or save
|
||||
}
|
||||
|
||||
let { open = $bindable(), mode: propMode, stackName: propStackName = '', initialCompose, initialStackName, readonly = false, onClose, onSuccess }: Props = $props();
|
||||
let { open = $bindable(), mode: propMode, stackName: propStackName = '', initialCompose, initialStackName, readonly = false, gitInfo = null, onClose, onSuccess }: Props = $props();
|
||||
|
||||
let gitCommitCopied = $state<'ok' | 'error' | null>(null);
|
||||
let gitUrlCopied = $state<'ok' | 'error' | null>(null);
|
||||
|
||||
// Local effective state - can transition from create → edit after failed deploy
|
||||
let mode = $state(propMode);
|
||||
@@ -1388,7 +1389,7 @@
|
||||
}}
|
||||
>
|
||||
<Dialog.Content
|
||||
class="max-w-none h-[95vh] flex flex-col p-0 gap-0 shadow-xl border-zinc-200 dark:border-zinc-700 {sidebar.state === 'collapsed' ? 'w-[calc(100vw-6rem)] ml-[1.5rem]' : 'w-[calc(100vw-12rem)] ml-[4.5rem]'}"
|
||||
class="max-w-none w-[calc(100vw-4rem)] h-[95vh] flex flex-col p-0 gap-0 shadow-xl border-zinc-200 dark:border-zinc-700"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<Dialog.Header class="px-5 py-3 border-b border-zinc-200 dark:border-zinc-700 flex-shrink-0">
|
||||
@@ -1540,6 +1541,34 @@
|
||||
<span class="ml-4 flex shrink-0 items-center gap-1 rounded-full border border-purple-500/30 bg-purple-500/10 px-2 py-0.5 text-2xs font-medium text-purple-600 dark:text-purple-400" title="This is a Git-managed stack — Dockhand shows its compose read-only; edit it in the repository.">
|
||||
<GitBranch class="h-3 w-3" /> Git · read-only
|
||||
</span>
|
||||
{#if gitInfo && (gitInfo.commit || gitInfo.url || gitInfo.branch)}
|
||||
<div class="ml-3 flex min-w-0 items-center gap-3 text-2xs text-muted-foreground">
|
||||
{#if gitInfo.commit}
|
||||
<span class="flex shrink-0 items-center gap-1">
|
||||
<GitCommitHorizontal class="h-3.5 w-3.5 shrink-0 opacity-70" />
|
||||
<code class="font-mono">{gitInfo.commit}</code>
|
||||
<button type="button" class="rounded p-0.5 hover:bg-muted transition-colors" title="Copy commit hash" onclick={() => copyText(gitInfo.commit ?? null, (v) => gitCommitCopied = v)}>
|
||||
{#if gitCommitCopied === 'ok'}<Check class="h-3 w-3 text-green-500" />{:else}<Copy class="h-3 w-3" />{/if}
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
{#if gitInfo.url}
|
||||
<span class="flex min-w-0 items-center gap-1">
|
||||
<Github class="h-3.5 w-3.5 shrink-0 opacity-70" />
|
||||
<span class="truncate">{gitInfo.url}</span>
|
||||
<button type="button" class="rounded p-0.5 hover:bg-muted transition-colors shrink-0" title="Copy repository URL" onclick={() => copyText(gitInfo.url ?? null, (v) => gitUrlCopied = v)}>
|
||||
{#if gitUrlCopied === 'ok'}<Check class="h-3 w-3 text-green-500" />{:else}<Copy class="h-3 w-3" />{/if}
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
{#if gitInfo.branch}
|
||||
<span class="flex shrink-0 items-center gap-1">
|
||||
<GitBranch class="h-3.5 w-3.5 shrink-0 opacity-70" />
|
||||
<span>{gitInfo.branch}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- Compose path -->
|
||||
<div class="flex-shrink-0 px-4 py-2" style="width: {splitRatio}%">
|
||||
|
||||
Reference in new issue
Block a user