1.0.5
This commit is contained in:
1 parent
b89470e965
commit
a1e07b1a10
11 files changed
+292
-72
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user