diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index ec68844..9f912e4 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -9,6 +9,7 @@ import type { Registry, RegistryImage, Settings, + Stage, StageEnv, Volume } from './types'; @@ -115,6 +116,20 @@ export function deleteProject(id: string): Promise<{ deleted: string }> { return del<{ deleted: string }>(`/api/projects/${id}`); } +// ── Stages ───────────────────────────────────────────────────────── + +export function createStage(projectId: string, data: Partial): Promise { + return post(`/api/projects/${projectId}/stages`, data); +} + +export function updateStage(projectId: string, stageId: string, data: Partial): Promise { + return put(`/api/projects/${projectId}/stages/${stageId}`, data); +} + +export function deleteStage(projectId: string, stageId: string): Promise { + return del(`/api/projects/${projectId}/stages/${stageId}`); +} + // ── Instances ─────────────────────────────────────────────────────── export function listInstances(projectId: string, stageId: string): Promise { diff --git a/web/src/routes/projects/[id]/+page.svelte b/web/src/routes/projects/[id]/+page.svelte index a691884..8d4df0a 100644 --- a/web/src/routes/projects/[id]/+page.svelte +++ b/web/src/routes/projects/[id]/+page.svelte @@ -7,7 +7,9 @@ import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import EmptyState from '$lib/components/EmptyState.svelte'; import Skeleton from '$lib/components/Skeleton.svelte'; - import { IconTrash, IconKey, IconHardDrive, IconDeploy, IconChevronRight, IconClock, IconTag, IconLoader } from '$lib/components/icons'; + import { IconTrash, IconKey, IconHardDrive, IconDeploy, IconChevronRight, IconClock, IconTag, IconLoader, IconPlus } from '$lib/components/icons'; + import FormField from '$lib/components/FormField.svelte'; + import { toasts } from '$lib/stores/toast'; import { t } from '$lib/i18n'; let project = $state(null); @@ -23,6 +25,45 @@ let deployError = $state(''); let availableTags = $state([]); + + // Add stage form + let showAddStage = $state(false); + let stageName = $state(''); + let stageTagPattern = $state('*'); + let stageAutoDeploy = $state(true); + let stageMaxInstances = $state('1'); + let addingStage = $state(false); + + async function handleAddStage() { + if (!stageName.trim()) return; + addingStage = true; + try { + await api.createStage(projectId, { + name: stageName.trim(), + tag_pattern: stageTagPattern.trim() || '*', + auto_deploy: stageAutoDeploy, + max_instances: parseInt(stageMaxInstances) || 1, + }); + toasts.success(`Stage "${stageName}" created`); + stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageMaxInstances = '1'; + showAddStage = false; + await loadProject(); + } catch (e) { + toasts.error(e instanceof Error ? e.message : 'Failed to create stage'); + } finally { + addingStage = false; + } + } + + async function handleDeleteStage(stageId: string, name: string) { + try { + await api.deleteStage(projectId, stageId); + toasts.success(`Stage "${name}" deleted`); + await loadProject(); + } catch (e) { + toasts.error(e instanceof Error ? e.message : 'Failed to delete stage'); + } + } let tagsLoading = $state(false); let showDeleteConfirm = $state(false); @@ -205,9 +246,45 @@
-

{$t('projectDetail.stages')}

+
+

{$t('projectDetail.stages')}

+ +
- {#if stages.length === 0} + {#if showAddStage} +
+
+ + + +
+ +
+
+
+ +
+
+ {/if} + + {#if stages.length === 0 && !showAddStage}
@@ -240,6 +317,14 @@ {$t('projectDetail.deployNewVersion')} +
diff --git a/web/src/routes/projects/[id]/env/+page.svelte b/web/src/routes/projects/[id]/env/+page.svelte index d86c3e4..1a128d8 100644 --- a/web/src/routes/projects/[id]/env/+page.svelte +++ b/web/src/routes/projects/[id]/env/+page.svelte @@ -34,7 +34,7 @@ error = ''; try { const detail = await api.getProject(projectId); - stages = detail.stages; + stages = detail.stages ?? []; try { projectEnv = JSON.parse(detail.project.env || '{}'); } catch {