diff --git a/docs/features/stack-management.mdx b/docs/features/stack-management.mdx index 2fbfa923..95b84090 100644 --- a/docs/features/stack-management.mdx +++ b/docs/features/stack-management.mdx @@ -6,7 +6,7 @@ description: Create, deploy, control, and remove Docker Compose stacks. A **stack** in Sencho is a Docker Compose project: a directory inside your `COMPOSE_DIR` that contains at least a `compose.yaml` (or `docker-compose.yml`) file. Sencho automatically discovers every subdirectory as a stack. - Create New Stack dialog + New stack dialog ## Creating a stack @@ -24,7 +24,7 @@ Sencho creates a new directory inside `COMPOSE_DIR` with a blank `compose.yaml` If you have a `docker run` command handy (from a README, a forum post, or your shell history), Sencho can turn it into a ready-to-deploy `compose.yaml` without hand translation. -Open the **Create New Stack** dialog and switch to the **From Docker Run** tab. +Open the **New stack** dialog and switch to the **From Docker Run** tab. Create stack dialog on the From Docker Run tab diff --git a/e2e/git-sources.spec.ts b/e2e/git-sources.spec.ts index c6a9a29d..cf26d2fd 100644 --- a/e2e/git-sources.spec.ts +++ b/e2e/git-sources.spec.ts @@ -210,7 +210,7 @@ async function deleteCreateFromGitStack(page: Page) { async function openCreateStackDialog(page: Page) { await page.getByRole('button', { name: 'Create Stack' }).click(); - await expect(page.getByRole('dialog').getByText('Create New Stack')).toBeVisible({ timeout: 5_000 }); + await expect(page.getByRole('dialog', { name: 'New stack' })).toBeVisible({ timeout: 5_000 }); } test.describe('Create stack from Git', () => { diff --git a/frontend/src/components/EditorLayout/CreateStackDialog.tsx b/frontend/src/components/EditorLayout/CreateStackDialog.tsx index 5ab200ee..f20237a4 100644 --- a/frontend/src/components/EditorLayout/CreateStackDialog.tsx +++ b/frontend/src/components/EditorLayout/CreateStackDialog.tsx @@ -1,15 +1,6 @@ -import { useState } from 'react'; -import { Plus, GitBranch, FileCode2, Loader2 } from 'lucide-react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '../ui/dialog'; -import { Tabs, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from '../ui/tabs'; -import { springs } from '@/lib/motion'; +import { useRef, useState, type KeyboardEvent } from 'react'; +import { Plus, GitBranch, FileCode2, Loader2, type LucideIcon } from 'lucide-react'; +import { Modal, ModalHeader, ModalBody, ModalFooter } from '../ui/modal'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; @@ -18,6 +9,7 @@ import { Checkbox } from '../ui/checkbox'; import { GitSourceFields, type ApplyMode } from '../stack/GitSourceFields'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; +import { cn } from '@/lib/utils'; export interface CreateStackDialogProps { open: boolean; @@ -26,8 +18,19 @@ export interface CreateStackDialogProps { onStacksChanged: () => void | Promise; } +type CreateMode = 'empty' | 'git' | 'docker-run'; + +const MODES: ReadonlyArray<{ id: CreateMode; label: string; icon: LucideIcon }> = [ + { id: 'empty', label: 'Empty', icon: Plus }, + { id: 'git', label: 'From Git', icon: GitBranch }, + { id: 'docker-run', label: 'From Docker Run', icon: FileCode2 }, +]; + +const tabId = (m: CreateMode) => `create-stack-tab-${m}`; +const panelId = (m: CreateMode) => `create-stack-panel-${m}`; + export function CreateStackDialog({ open, onOpenChange, onStackCreated, onStacksChanged }: CreateStackDialogProps) { - const [createMode, setCreateMode] = useState<'empty' | 'git' | 'docker-run'>('empty'); + const [createMode, setCreateMode] = useState('empty'); const [newStackName, setNewStackName] = useState(''); const [dockerRunInput, setDockerRunInput] = useState(''); const [convertedYaml, setConvertedYaml] = useState(null); @@ -64,7 +67,6 @@ export function CreateStackDialog({ open, onOpenChange, onStackCreated, onStacks const handleCreateStack = async () => { if (!newStackName.trim()) return; - // Send stackName directly (no .yml extension - backend creates directory) const stackName = newStackName.trim(); try { const response = await apiFetch('/stacks', { @@ -263,8 +265,11 @@ export function CreateStackDialog({ open, onOpenChange, onStackCreated, onStacks } }; + const busy = creatingFromGit || creatingFromDockerRun; + return ( - { onOpenChange(o); @@ -275,106 +280,99 @@ export function CreateStackDialog({ open, onOpenChange, onStackCreated, onStacks } }} > - - - Create New Stack - - Create a new stack: empty, cloned from a Git repository, or converted from a docker run command. - - + + -
- setCreateMode(v as 'empty' | 'git' | 'docker-run')}> - - - - - - Empty - - - - - - From Git - - - - - - From Docker Run - - - - - -
- - {createMode === 'empty' && ( - <> -
+ {createMode === 'empty' && ( +
+ +
setNewStackName(e.target.value)} + autoFocus />
- - - - - )} - {createMode === 'git' && ( - <> - -
-
- - setNewStackName(e.target.value)} - disabled={creatingFromGit} - /> -
+ + onOpenChange(false)}> + Cancel + + } + primary={ + + } + /> +
+ )} - + + +
+ + setNewStackName(e.target.value)} disabled={creatingFromGit} - repoUrl={gitRepoUrl} - branch={gitBranch} - composePath={gitComposePath} - syncEnv={gitSyncEnv} - authType={gitAuthType} - token={gitToken} - hasStoredToken={false} - applyMode={gitApplyMode} - onRepoUrlChange={setGitRepoUrl} - onBranchChange={setGitBranch} - onComposePathChange={setGitComposePath} - onSyncEnvChange={setGitSyncEnv} - onAuthTypeChange={setGitAuthType} - onTokenChange={setGitToken} - onApplyModeChange={setGitApplyMode} /> - -
- setGitDeployNow(c === true)} - disabled={creatingFromGit} - /> - -
-
- + + + +
+ setGitDeployNow(c === true)} + disabled={creatingFromGit} + /> + +
+
+ + onOpenChange(false)} disabled={creatingFromGit}> + Cancel + + } + primary={ - - - )} - {createMode === 'docker-run' && ( - <> - -
-
- - setNewStackName(e.target.value)} - disabled={creatingFromDockerRun} - /> -
-
- -