feat: volume scopes redesign — replace shared/isolated with 6 scopes

Replace confusing shared/isolated volume modes with explicit scopes:
- instance: per-deploy isolated directory
- stage: shared within a stage across deploys
- project: shared across all stages
- project_named: named group within a project
- named: global named volume across projects
- ephemeral: tmpfs in-memory mount

Includes schema migration (shared→project, isolated→instance),
backward-compatible deployer resolution, scope metadata API endpoint,
and redesigned volume editor UI with scope guide cards and hints.
This commit is contained in:
2026-03-31 23:22:43 +03:00
parent 1a8dfefa77
commit 8fb959f81f
12 changed files with 424 additions and 112 deletions
+8 -3
View File
@@ -20,7 +20,8 @@ import type {
StageEnv,
StandaloneProxy,
ValidationResult,
Volume
Volume,
VolumeScopeInfo
} from './types';
// ── Helpers ─────────────────────────────────────────────────────────
@@ -327,7 +328,7 @@ export function listVolumes(projectId: string): Promise<Volume[]> {
export function createVolume(
projectId: string,
data: { source: string; target: string; mode?: string }
data: { source: string; target: string; scope: string; name?: string; mode?: string }
): Promise<Volume> {
return post<Volume>(`/api/projects/${projectId}/volumes`, data);
}
@@ -335,11 +336,15 @@ export function createVolume(
export function updateVolume(
projectId: string,
volId: string,
data: { source?: string; target?: string; mode?: string }
data: { source?: string; target?: string; scope?: string; name?: string; mode?: string }
): Promise<Volume> {
return put<Volume>(`/api/projects/${projectId}/volumes/${volId}`, data);
}
export function listVolumeScopes(): Promise<VolumeScopeInfo[]> {
return get<VolumeScopeInfo[]>('/api/volumes/scopes');
}
export function deleteVolume(
projectId: string,
volId: string
+7 -6
View File
@@ -120,15 +120,16 @@
},
"volumeEditor": {
"title": "Volume Mounts",
"description": "Configure volume mounts for containers.",
"sharedDesc": "Shared mode uses the source path as-is for all instances.",
"isolatedDesc": "Isolated mode appends /{stage}-{tag}/ to the source, giving each instance its own directory.",
"description": "Configure volume mounts for containers. Choose a scope to control how volumes are shared between deploys.",
"sourceHost": "Source (Host)",
"targetContainer": "Target (Container)",
"mode": "Mode",
"scope": "Scope",
"nameColumn": "Name",
"namePlaceholder": "e.g. shared-db",
"requiresName": "requires name",
"noHostPath": "no host path",
"tmpfs": "tmpfs (in-memory)",
"actions": "Actions",
"shared": "Shared",
"isolated": "Isolated",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
+7 -6
View File
@@ -120,15 +120,16 @@
},
"volumeEditor": {
"title": "Тома",
"description": "Настройка монтирования томов для контейнеров.",
"sharedDesc": "Режим «Общий» использует путь источника как есть для всех экземпляров.",
"isolatedDesc": "Режим «Изолированный» добавляет /{stage}-{tag}/ к источнику, создавая свою директорию для каждого экземпляра.",
"description": "Настройка монтирования томов для контейнеров. Выберите область видимости для управления общим доступом между развёртываниями.",
"sourceHost": "Источник (хост)",
"targetContainer": "Цель (контейнер)",
"mode": "Режим",
"scope": "Область",
"nameColumn": "Имя",
"namePlaceholder": "напр. shared-db",
"requiresName": "требуется имя",
"noHostPath": "нет пути на хосте",
"tmpfs": "tmpfs (в памяти)",
"actions": "Действия",
"shared": "Общий",
"isolated": "Изолированный",
"edit": "Изменить",
"delete": "Удалить",
"save": "Сохранить",
+14 -1
View File
@@ -161,17 +161,30 @@ export interface EntityPickerItem {
disabledHint?: string;
}
/** Volume scope determines the sharing level. */
export type VolumeScope = 'instance' | 'stage' | 'project' | 'project_named' | 'named' | 'ephemeral';
/** Volume mount configuration for a project. */
export interface Volume {
id: string;
project_id: string;
source: string;
target: string;
mode: 'shared' | 'isolated';
mode?: string;
scope: VolumeScope;
name: string;
created_at: string;
updated_at: string;
}
/** Scope metadata returned by GET /api/volumes/scopes. */
export interface VolumeScopeInfo {
scope: VolumeScope;
description: string;
needs_name: boolean;
path_example: string;
}
/** Docker daemon health check result. */
export interface DockerHealth {
connected: boolean;