feat(volume-browser): phase 2 - file browser UI

- Browse route: /projects/{id}/volumes/{volId}/browse
- Directory listing with file icons, sizes, dates
- Breadcrumb navigation, click-to-navigate directories
- Download entire volume or folder as ZIP
- Upload files via file picker
- i18n EN/RU for all browser strings
This commit is contained in:
2026-04-01 23:04:30 +03:00
parent 4a0f223d61
commit 6b54a72ec9
6 changed files with 357 additions and 1 deletions
+62 -1
View File
@@ -21,7 +21,8 @@ import type {
StandaloneProxy,
ValidationResult,
Volume,
VolumeScopeInfo
VolumeScopeInfo,
BrowseResult
} from './types';
// ── Helpers ─────────────────────────────────────────────────────────
@@ -352,6 +353,66 @@ export function deleteVolume(
return del<{ deleted: string }>(`/api/projects/${projectId}/volumes/${volId}`);
}
export function browseVolume(
projectId: string,
volId: string,
params?: { path?: string; stage?: string; tag?: string }
): Promise<BrowseResult> {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const qs = query.toString();
return get<BrowseResult>(`/api/projects/${projectId}/volumes/${volId}/browse${qs ? `?${qs}` : ''}`);
}
export function volumeDownloadUrl(
projectId: string,
volId: string,
params?: { path?: string; stage?: string; tag?: string }
): string {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
if (token) query.set('token', token);
const qs = query.toString();
return `/api/projects/${projectId}/volumes/${volId}/download${qs ? `?${qs}` : ''}`;
}
export async function uploadToVolume(
projectId: string,
volId: string,
files: FileList,
params?: { path?: string; stage?: string; tag?: string }
): Promise<{ uploaded: string[]; count: number }> {
const query = new URLSearchParams();
if (params?.path) query.set('path', params.path);
if (params?.stage) query.set('stage', params.stage);
if (params?.tag) query.set('tag', params.tag);
const qs = query.toString();
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
const token = typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`/api/projects/${projectId}/volumes/${volId}/upload${qs ? `?${qs}` : ''}`, {
method: 'POST',
headers,
body: formData,
});
const envelope = await res.json();
if (!envelope.success) throw new Error(envelope.error ?? 'Upload failed');
return envelope.data;
}
// ── Event Log ───────────────────────────────────────────────────────
export function fetchEventLog(params?: {
+16
View File
@@ -144,6 +144,22 @@
"updateFailed": "Failed to update volume",
"deleteFailed": "Failed to delete volume"
},
"volumeBrowser": {
"title": "Volume Browser",
"loadFailed": "Failed to load directory",
"empty": "This directory is empty.",
"name": "Name",
"size": "Size",
"modified": "Modified",
"downloadAll": "Download volume as ZIP",
"downloadFolder": "Download folder as ZIP",
"upload": "Upload files",
"uploaded": "Uploaded",
"files": "file(s)",
"uploadFailed": "Failed to upload files",
"browse": "Browse",
"download": "Download"
},
"quickDeploy": {
"title": "Quick Deploy",
"description": "Deploy a container image with zero configuration. Paste an image URL, review the defaults, and deploy.",
+16
View File
@@ -144,6 +144,22 @@
"updateFailed": "Не удалось обновить том",
"deleteFailed": "Не удалось удалить том"
},
"volumeBrowser": {
"title": "Обзор тома",
"loadFailed": "Не удалось загрузить каталог",
"empty": "Этот каталог пуст.",
"name": "Имя",
"size": "Размер",
"modified": "Изменён",
"downloadAll": "Скачать том как ZIP",
"downloadFolder": "Скачать папку как ZIP",
"upload": "Загрузить файлы",
"uploaded": "Загружено",
"files": "файл(ов)",
"uploadFailed": "Не удалось загрузить файлы",
"browse": "Обзор",
"download": "Скачать"
},
"quickDeploy": {
"title": "Быстрый деплой",
"description": "Разверните образ контейнера без настройки. Вставьте URL образа, проверьте параметры и разверните.",
+15
View File
@@ -185,6 +185,21 @@ export interface VolumeScopeInfo {
path_example: string;
}
/** A file or directory entry in a volume listing. */
export interface FileEntry {
name: string;
is_dir: boolean;
size: number;
mod_time: string;
}
/** Response from the volume browse endpoint. */
export interface BrowseResult {
path: string;
root: string;
entries: FileEntry[];
}
/** Docker daemon health check result. */
export interface DockerHealth {
connected: boolean;