This commit is contained in:
jarek committed 2026-01-02 15:29:56 +01:00
1 parent b89470e965
commit a1e07b1a10
11 files changed
+292 -72

No files matched your search

+4 -1
View File
@@ -214,7 +214,10 @@
variableMarkers?: VariableMarker[];
}
let { value = '', language = 'yaml', readonly = false, theme = 'dark', onchange, class: className = '', variableMarkers = [] }: Props = $props();
let { value = '', language = 'yaml', readonly = false, theme = 'dark', onchange, class: className = '', variableMarkers: variableMarkersProp = [] }: Props = $props();
// Keep markers reactive - destructured props with defaults lose reactivity
const variableMarkers = $derived(variableMarkersProp);
let container: HTMLDivElement;
let view: EditorView | null = null;
+6 -5
View File
@@ -17,6 +17,7 @@
placeholder?: { key: string; value: string };
infoText?: string;
existingSecretKeys?: Set<string>;
theme?: 'light' | 'dark';
class?: string;
onchange?: () => void;
}
@@ -31,6 +32,7 @@
placeholder = { key: 'VARIABLE_NAME', value: 'value' },
infoText,
existingSecretKeys = new Set<string>(),
theme = 'dark',
class: className = '',
onchange
}: Props = $props();
@@ -44,7 +46,6 @@
let confirmClearOpen = $state(false);
let contentAreaRef: HTMLDivElement;
let parseWarnings = $state<string[]>([]);
let editorTheme = $state<'light' | 'dark'>('dark');
let hasMergedOnLoad = $state(false);
// Count of secrets (for display in hint)
@@ -370,9 +371,9 @@
</div>
{:else if secretCount > 0}
<!-- Text view hint about secrets (only shown when secrets exist) -->
<div class="flex items-start gap-2 px-2 py-1.5 rounded bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50">
<ShieldAlert class="w-3.5 h-3.5 text-amber-500 shrink-0 mt-0.5" />
<div class="text-2xs text-amber-700 dark:text-amber-300">
<div class="flex items-start gap-2 px-2.5 py-2 rounded bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50">
<ShieldAlert class="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
<div class="text-xs text-amber-700 dark:text-amber-300">
<span class="font-medium">{secretCount} secret{secretCount === 1 ? '' : 's'} not shown.</span>
<span class="text-amber-600 dark:text-amber-400">Secrets are never written to disk and are injected via shell environment when the stack starts.</span>
</div>
@@ -429,7 +430,7 @@
<CodeEditor
value={rawContent}
language="dotenv"
theme={editorTheme}
theme={theme}
readonly={readonly}
onchange={handleTextChange}
class="h-full min-h-[200px] rounded-md overflow-hidden border border-zinc-200 dark:border-zinc-700"
@@ -0,0 +1,183 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { AlertCircle, Copy, Check, AlertTriangle, CheckCircle2, XCircle } from 'lucide-svelte';
interface Props {
open: boolean;
title: string;
message: string;
details?: string;
onClose: () => void;
}
let { open = $bindable(), title, message, details, onClose }: Props = $props();
let copied = $state(false);
interface ParsedOutput {
warnings: string[];
steps: { action: string; status: 'creating' | 'created' | 'starting' | 'started' | 'error' }[];
error: string | null;
raw: string;
parsed: boolean;
}
// Parse docker compose output into structured format
function parseDockerOutput(text: string): ParsedOutput {
const result: ParsedOutput = {
warnings: [],
steps: [],
error: null,
raw: text,
parsed: false
};
try {
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
for (const line of lines) {
// Parse time="..." level=warning msg="..."
const warningMatch = line.match(/time="[^"]*"\s+level=warning\s+msg="([^"]+)"/);
if (warningMatch) {
result.warnings.push(warningMatch[1]);
result.parsed = true;
continue;
}
// Parse container/network steps: "Network foo Creating" or "Container foo-1 Created"
const stepMatch = line.match(/^\s*(Network|Container|Volume)\s+(\S+)\s+(Creating|Created|Starting|Started|Stopping|Stopped|Removing|Removed)\s*$/i);
if (stepMatch) {
const [, type, name, status] = stepMatch;
const normalizedStatus = status.toLowerCase() as any;
result.steps.push({
action: `${type} ${name}`,
status: normalizedStatus
});
result.parsed = true;
continue;
}
// Parse error lines
if (line.startsWith('Error') || line.includes('error') || line.includes('failed')) {
result.error = result.error ? `${result.error}\n${line}` : line;
result.parsed = true;
continue;
}
}
// If we parsed something but have no clear error, check for remaining unparsed content
if (result.parsed && !result.error) {
const unparsed = lines.filter(line => {
if (line.match(/time="[^"]*"\s+level=warning/)) return false;
if (line.match(/^\s*(Network|Container|Volume)\s+\S+\s+(Creating|Created|Starting|Started|Stopping|Stopped|Removing|Removed)\s*$/i)) return false;
return true;
});
if (unparsed.length > 0) {
result.error = unparsed.join('\n');
}
}
} catch {
// Parsing failed, will show raw message
}
return result;
}
const parsed = $derived(parseDockerOutput(message));
async function copyError() {
const text = details ? `${message}\n\n${details}` : message;
await navigator.clipboard.writeText(text);
copied = true;
setTimeout(() => (copied = false), 2000);
}
function handleClose() {
open = false;
onClose();
}
</script>
<Dialog.Root bind:open onOpenChange={(o) => !o && handleClose()}>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-destructive">
<AlertCircle class="w-5 h-5" />
{title}
</Dialog.Title>
</Dialog.Header>
<div class="space-y-3 max-h-[60vh] overflow-y-auto">
{#if parsed.parsed}
<!-- Parsed docker compose output -->
{#if parsed.warnings.length > 0}
<div class="space-y-1">
{#each parsed.warnings as warning}
<div class="flex items-start gap-2 text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 px-2.5 py-1.5 rounded-md">
<AlertTriangle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
<span>{warning}</span>
</div>
{/each}
</div>
{/if}
{#if parsed.steps.length > 0}
<div class="bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md p-2.5 space-y-1">
{#each parsed.steps as step}
<div class="flex items-center gap-2 text-xs font-mono">
{#if step.status === 'created' || step.status === 'started' || step.status === 'removed' || step.status === 'stopped'}
<CheckCircle2 class="w-3.5 h-3.5 text-green-500" />
{:else if step.status === 'error'}
<XCircle class="w-3.5 h-3.5 text-red-500" />
{:else}
<div class="w-3.5 h-3.5 rounded-full border-2 border-zinc-400"></div>
{/if}
<span class="text-zinc-600 dark:text-zinc-300">{step.action}</span>
<span class="text-zinc-400 dark:text-zinc-500 capitalize">{step.status}</span>
</div>
{/each}
</div>
{/if}
{#if parsed.error}
<div class="bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md p-3 relative group">
<button
onclick={copyError}
class="absolute top-2 right-2 p-1 rounded text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-opacity"
title="Copy error"
>
{#if copied}
<Check class="w-3.5 h-3.5" />
{:else}
<Copy class="w-3.5 h-3.5" />
{/if}
</button>
<pre class="text-sm text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap break-words font-mono pr-6">{parsed.error}</pre>
</div>
{/if}
{:else}
<!-- Fallback to raw message -->
<div class="relative group">
<button
onclick={copyError}
class="absolute top-1 right-1 p-1 rounded text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Copy error"
>
{#if copied}
<Check class="w-3.5 h-3.5" />
{:else}
<Copy class="w-3.5 h-3.5" />
{/if}
</button>
<pre class="text-sm whitespace-pre-wrap font-sans pr-6">{message}</pre>
</div>
{/if}
{#if details}
<pre class="text-xs bg-zinc-100 dark:bg-zinc-800 p-3 rounded-md overflow-auto max-h-64 whitespace-pre-wrap break-all">{details}</pre>
{/if}
</div>
<Dialog.Footer class="flex gap-2 sm:justify-end">
<Button onclick={handleClose}>OK</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,3 @@
import ErrorDialog from './error-dialog.svelte';
export { ErrorDialog };