Allow viewing Git stack YAML
Open Git-sourced stack compose files in a read-only editor and graph while keeping repository settings separate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c718ac9a84
commit
9f09d60a83
4 files changed
+66
-33
No files matched your search
@@ -65,6 +65,7 @@
|
|||||||
let showGitModal = $state(false);
|
let showGitModal = $state(false);
|
||||||
let showImportModal = $state(false);
|
let showImportModal = $state(false);
|
||||||
let editingStackName = $state('');
|
let editingStackName = $state('');
|
||||||
|
let stackModalReadonly = $state(false);
|
||||||
let editingGitStack = $state<any>(null);
|
let editingGitStack = $state<any>(null);
|
||||||
let envId = $state<number | null>(null);
|
let envId = $state<number | null>(null);
|
||||||
|
|
||||||
@@ -1048,6 +1049,13 @@
|
|||||||
|
|
||||||
function editStack(name: string) {
|
function editStack(name: string) {
|
||||||
editingStackName = name;
|
editingStackName = name;
|
||||||
|
stackModalReadonly = false;
|
||||||
|
showEditModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewGitStack(name: string) {
|
||||||
|
editingStackName = name;
|
||||||
|
stackModalReadonly = true;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1593,19 +1601,17 @@
|
|||||||
{#if column.id === 'name'}
|
{#if column.id === 'name'}
|
||||||
{@const systemType = getStackSystemType(stack)}
|
{@const systemType = getStackSystemType(stack)}
|
||||||
<span class="flex items-center gap-1 min-w-0 w-full">
|
<span class="flex items-center gap-1 min-w-0 w-full">
|
||||||
{#if source.sourceType !== 'git'}
|
|
||||||
<!-- Internal stacks (including those needing file location) are clickable -->
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="font-medium text-xs hover:text-primary hover:underline cursor-pointer text-left truncate min-w-0"
|
class="font-medium text-xs hover:text-primary hover:underline cursor-pointer text-left truncate min-w-0"
|
||||||
onclick={(e) => { e.stopPropagation(); editStack(stack.name); }}
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (source.sourceType === 'git') viewGitStack(stack.name);
|
||||||
|
else editStack(stack.name);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{stack.name}
|
{stack.name}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
|
||||||
<!-- Git stacks open in GitStackModal instead -->
|
|
||||||
<span class="font-medium text-xs truncate min-w-0">{stack.name}</span>
|
|
||||||
{/if}
|
|
||||||
{#if systemType}
|
{#if systemType}
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger>
|
<Tooltip.Trigger>
|
||||||
@@ -2515,9 +2521,11 @@
|
|||||||
bind:open={showEditModal}
|
bind:open={showEditModal}
|
||||||
mode="edit"
|
mode="edit"
|
||||||
stackName={editingStackName}
|
stackName={editingStackName}
|
||||||
|
readonly={stackModalReadonly}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
showEditModal = false;
|
showEditModal = false;
|
||||||
editingStackName = '';
|
editingStackName = '';
|
||||||
|
stackModalReadonly = false;
|
||||||
}}
|
}}
|
||||||
onSuccess={fetchStacks}
|
onSuccess={fetchStacks}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,9 +19,10 @@
|
|||||||
composeContent: string;
|
composeContent: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
onContentChange?: (content: string) => void;
|
onContentChange?: (content: string) => void;
|
||||||
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { composeContent, class: className = '', onContentChange }: Props = $props();
|
let { composeContent, class: className = '', onContentChange, readonly = false }: Props = $props();
|
||||||
|
|
||||||
let containerEl: HTMLDivElement | null = $state(null);
|
let containerEl: HTMLDivElement | null = $state(null);
|
||||||
let cy: cytoscape.Core | null = null;
|
let cy: cytoscape.Core | null = null;
|
||||||
@@ -2103,6 +2104,7 @@
|
|||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="flex items-center justify-between px-2 py-1.5 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 min-h-[40px]">
|
<div class="flex items-center justify-between px-2 py-1.5 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 min-h-[40px]">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
{#if !readonly}
|
||||||
<!-- Add element dropdown -->
|
<!-- Add element dropdown -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Button
|
<Button
|
||||||
@@ -2198,6 +2200,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls -->
|
<!-- Controls -->
|
||||||
@@ -2333,7 +2336,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Details panel (overlay) -->
|
<!-- Details panel (overlay) -->
|
||||||
{#if selectedNode || selectedEdge}
|
{#if !readonly && (selectedNode || selectedEdge)}
|
||||||
<div class="absolute top-0 right-0 bottom-0 w-[420px] border-l border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/95 shadow-lg z-20 flex flex-col">
|
<div class="absolute top-0 right-0 bottom-0 w-[420px] border-l border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/95 shadow-lg z-20 flex flex-col">
|
||||||
<!-- Sticky header -->
|
<!-- Sticky header -->
|
||||||
{#if selectedNode}
|
{#if selectedNode}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
path: string | null;
|
path: string | null;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
onBrowse: () => void;
|
onBrowse?: () => void;
|
||||||
onChangeLocation?: () => void; // Optional: relocate entire folder
|
onChangeLocation?: () => void; // Optional: relocate entire folder
|
||||||
defaultText?: string;
|
defaultText?: string;
|
||||||
isSuggested?: boolean;
|
isSuggested?: boolean;
|
||||||
@@ -58,13 +58,15 @@
|
|||||||
>
|
>
|
||||||
{displayPath.truncated || defaultText}
|
{displayPath.truncated || defaultText}
|
||||||
</code>
|
</code>
|
||||||
<button
|
{#if onBrowse}
|
||||||
onclick={onBrowse}
|
<button
|
||||||
class="p-1 rounded transition-colors shrink-0 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
onclick={onBrowse}
|
||||||
title={`Browse for ${label.toLowerCase()}`}
|
class="p-1 rounded transition-colors shrink-0 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
||||||
>
|
title={`Browse for ${label.toLowerCase()}`}
|
||||||
<FolderOpen class="w-3.5 h-3.5" />
|
>
|
||||||
</button>
|
<FolderOpen class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if onChangeLocation}
|
{#if onChangeLocation}
|
||||||
<button
|
<button
|
||||||
onclick={onChangeLocation}
|
onclick={onChangeLocation}
|
||||||
|
|||||||
@@ -37,11 +37,12 @@
|
|||||||
stackName?: string; // Required for edit mode, optional for create
|
stackName?: string; // Required for edit mode, optional for create
|
||||||
initialCompose?: string; // Pre-fill compose content (for library deploy)
|
initialCompose?: string; // Pre-fill compose content (for library deploy)
|
||||||
initialStackName?: string; // Pre-fill stack name (for library deploy)
|
initialStackName?: string; // Pre-fill stack name (for library deploy)
|
||||||
|
readonly?: boolean; // View compose content without allowing local changes
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSuccess: () => void; // Called after create or save
|
onSuccess: () => void; // Called after create or save
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open = $bindable(), mode: propMode, stackName: propStackName = '', initialCompose, initialStackName, onClose, onSuccess }: Props = $props();
|
let { open = $bindable(), mode: propMode, stackName: propStackName = '', initialCompose, initialStackName, readonly = false, onClose, onSuccess }: Props = $props();
|
||||||
|
|
||||||
// Local effective state - can transition from create → edit after failed deploy
|
// Local effective state - can transition from create → edit after failed deploy
|
||||||
let mode = $state(propMode);
|
let mode = $state(propMode);
|
||||||
@@ -1363,6 +1364,8 @@
|
|||||||
<Dialog.Description class="text-xs text-zinc-500 dark:text-zinc-400">
|
<Dialog.Description class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
{#if mode === 'create'}
|
{#if mode === 'create'}
|
||||||
Create a new Docker Compose stack
|
Create a new Docker Compose stack
|
||||||
|
{:else if readonly}
|
||||||
|
View compose file and dependency graph
|
||||||
{:else}
|
{:else}
|
||||||
Edit compose file and environment variables
|
Edit compose file and environment variables
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1454,7 +1457,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- File location needed banner -->
|
<!-- File location needed banner -->
|
||||||
{#if mode === 'edit' && needsFileLocation}
|
{#if mode === 'edit' && needsFileLocation && !readonly}
|
||||||
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 bg-amber-50/50 dark:bg-amber-950/20">
|
<div class="px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<AlertCircle class="w-4 h-4 shrink-0 text-amber-500 mt-0.5" />
|
<AlertCircle class="w-4 h-4 shrink-0 text-amber-500 mt-0.5" />
|
||||||
@@ -1493,8 +1496,8 @@
|
|||||||
placeholder="/path/to/compose.yaml"
|
placeholder="/path/to/compose.yaml"
|
||||||
copied={composePathCopied}
|
copied={composePathCopied}
|
||||||
onCopy={() => copyText(workingComposePath, (v) => composePathCopied = v)}
|
onCopy={() => copyText(workingComposePath, (v) => composePathCopied = v)}
|
||||||
onBrowse={openComposeBrowser}
|
onBrowse={readonly ? undefined : openComposeBrowser}
|
||||||
onChangeLocation={mode === 'edit' && !needsFileLocation ? openChangeLocationBrowser : undefined}
|
onChangeLocation={mode === 'edit' && !needsFileLocation && !readonly ? openChangeLocationBrowser : undefined}
|
||||||
defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'}
|
defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'}
|
||||||
sourceHint={pathSourceHint}
|
sourceHint={pathSourceHint}
|
||||||
/>
|
/>
|
||||||
@@ -1510,8 +1513,8 @@
|
|||||||
placeholder="/path/to/.env (optional)"
|
placeholder="/path/to/.env (optional)"
|
||||||
copied={envPathCopied}
|
copied={envPathCopied}
|
||||||
onCopy={() => copyText(displayEnvPath, (v) => envPathCopied = v)}
|
onCopy={() => copyText(displayEnvPath, (v) => envPathCopied = v)}
|
||||||
onBrowse={openEnvBrowser}
|
onBrowse={readonly ? undefined : openEnvBrowser}
|
||||||
isEditable={true}
|
isEditable={!readonly}
|
||||||
isCustom={!!workingEnvPath}
|
isCustom={!!workingEnvPath}
|
||||||
defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'}
|
defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'}
|
||||||
isSuggested={isEnvPathSuggested}
|
isSuggested={isEnvPathSuggested}
|
||||||
@@ -1528,7 +1531,15 @@
|
|||||||
<div class="flex-shrink-0 flex flex-col min-w-0" style="width: {splitRatio}%">
|
<div class="flex-shrink-0 flex flex-col min-w-0" style="width: {splitRatio}%">
|
||||||
{#if open}
|
{#if open}
|
||||||
<div class="flex-1 p-3 min-h-0">
|
<div class="flex-1 p-3 min-h-0">
|
||||||
{#if needsFileLocation && !composeContent}
|
{#if readonly && needsFileLocation && !composeContent}
|
||||||
|
<div class="h-full rounded-md border border-dashed border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800/30 flex flex-col items-center justify-center text-center px-8">
|
||||||
|
<GitGraph class="w-12 h-12 text-zinc-300 dark:text-zinc-600 mb-4" />
|
||||||
|
<h3 class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">Compose file not available</h3>
|
||||||
|
<p class="text-xs text-zinc-500 dark:text-zinc-400 max-w-sm">
|
||||||
|
Deploy or sync this Git stack first so Dockhand has a local copy of its compose file.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if needsFileLocation && !composeContent}
|
||||||
<!-- Empty state for untracked stacks -->
|
<!-- Empty state for untracked stacks -->
|
||||||
<div class="h-full rounded-md border border-dashed border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800/30 flex flex-col items-center justify-center text-center px-8">
|
<div class="h-full rounded-md border border-dashed border-zinc-300 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800/30 flex flex-col items-center justify-center text-center px-8">
|
||||||
<FolderOpen class="w-12 h-12 text-zinc-300 dark:text-zinc-600 mb-4" />
|
<FolderOpen class="w-12 h-12 text-zinc-300 dark:text-zinc-600 mb-4" />
|
||||||
@@ -1578,8 +1589,9 @@
|
|||||||
bind:this={codeEditorRef}
|
bind:this={codeEditorRef}
|
||||||
value={composeContent}
|
value={composeContent}
|
||||||
language="yaml"
|
language="yaml"
|
||||||
|
{readonly}
|
||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
onchange={handleComposeChange}
|
onchange={readonly ? undefined : handleComposeChange}
|
||||||
variableMarkers={variableMarkers}
|
variableMarkers={variableMarkers}
|
||||||
class="flex-1 rounded-md overflow-hidden border border-zinc-200 dark:border-zinc-700"
|
class="flex-1 rounded-md overflow-hidden border border-zinc-200 dark:border-zinc-700"
|
||||||
/>
|
/>
|
||||||
@@ -1608,6 +1620,7 @@
|
|||||||
bind:rawContent={rawEnvContent}
|
bind:rawContent={rawEnvContent}
|
||||||
validation={envValidation}
|
validation={envValidation}
|
||||||
existingSecretKeys={mode === 'edit' ? existingSecretKeys : new Set()}
|
existingSecretKeys={mode === 'edit' ? existingSecretKeys : new Set()}
|
||||||
|
{readonly}
|
||||||
onchange={() => { markDirty(); debouncedValidate(); }}
|
onchange={() => { markDirty(); debouncedValidate(); }}
|
||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
infoText="These variables will be written to a .env file in the stack directory and passed to the compose command."
|
infoText="These variables will be written to a .env file in the stack directory and passed to the compose command."
|
||||||
@@ -1618,9 +1631,10 @@
|
|||||||
<!-- Graph tab: Full width -->
|
<!-- Graph tab: Full width -->
|
||||||
<ComposeGraphViewer
|
<ComposeGraphViewer
|
||||||
bind:this={graphViewerRef}
|
bind:this={graphViewerRef}
|
||||||
composeContent={composeContent || defaultCompose}
|
composeContent={composeContent || (mode === 'create' ? defaultCompose : '')}
|
||||||
class="h-full flex-1"
|
class="h-full flex-1"
|
||||||
onContentChange={handleGraphContentChange}
|
onContentChange={readonly ? undefined : handleGraphContentChange}
|
||||||
|
{readonly}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1630,7 +1644,9 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="px-5 py-2.5 border-t border-zinc-200 dark:border-zinc-700 flex items-center justify-between flex-shrink-0">
|
<div class="px-5 py-2.5 border-t border-zinc-200 dark:border-zinc-700 flex items-center justify-between flex-shrink-0">
|
||||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
{#if isDirty}
|
{#if readonly}
|
||||||
|
Read-only
|
||||||
|
{:else if isDirty}
|
||||||
<span class="text-amber-600 dark:text-amber-500">Unsaved changes</span>
|
<span class="text-amber-600 dark:text-amber-500">Unsaved changes</span>
|
||||||
{:else}
|
{:else}
|
||||||
No changes
|
No changes
|
||||||
@@ -1638,11 +1654,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button variant="outline" onclick={tryClose} disabled={saving}>
|
{#if readonly}
|
||||||
Cancel
|
<Button onclick={tryClose}>Close</Button>
|
||||||
</Button>
|
{:else}
|
||||||
|
<Button variant="outline" onclick={tryClose} disabled={saving}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if mode === 'create'}
|
{#if !readonly && mode === 'create'}
|
||||||
<!-- Create mode buttons -->
|
<!-- Create mode buttons -->
|
||||||
<Button variant="outline" onclick={() => handleCreate(false)} disabled={saving}>
|
<Button variant="outline" onclick={() => handleCreate(false)} disabled={saving}>
|
||||||
{#if saving}
|
{#if saving}
|
||||||
@@ -1662,7 +1682,7 @@
|
|||||||
Create & Start
|
Create & Start
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else if !readonly}
|
||||||
<!-- Edit mode buttons -->
|
<!-- Edit mode buttons -->
|
||||||
<Button variant="outline" class="w-24" onclick={() => handleSave(false)} disabled={saving || loading || (needsFileLocation && !workingComposePath.trim())}>
|
<Button variant="outline" class="w-24" onclick={() => handleSave(false)} disabled={saving || loading || (needsFileLocation && !workingComposePath.trim())}>
|
||||||
{#if saving && !savingWithRestart}
|
{#if saving && !savingWithRestart}
|
||||||
|
|||||||
Reference in new issue
Block a user