feat(workload): global Containers tab + frontend client
Adds the user-visible piece of the Workload refactor:
- web/src/lib/types.ts — Workload, Container, ContainerView,
App, WorkloadKind, ContainerState
- web/src/lib/api.ts — listWorkloads, getWorkload,
listWorkloadContainers, setWorkloadAppID,
listContainers (with filter),
CRUD for apps
- web/src/lib/i18n/{en,ru}.json — nav.containers
- web/src/routes/+layout.svelte — "Containers" nav item between Stacks
and Deploy, IconContainer
- web/src/routes/containers/+page.svelte — global Containers table:
* filter chips for kind (project/stack/site) and state
* client-side search across workload name / role / image /
subdomain / container ID prefix
* Workload column links to the kind-specific detail page,
resolved through a one-time /api/workloads call to map
workload_id → ref_id
* existing /containers/stale route untouched
The page renders against the live database now — boot backfill
populated workload rows from existing projects/stacks/sites,
the deployer dual-writes containers on every deploy, and the
30s reconciler keeps the index in sync with `docker ps`.
This commit is contained in:
+69
-1
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
ApiEnvelope,
|
||||
App,
|
||||
Container,
|
||||
ContainerStats,
|
||||
ContainerStatsSample,
|
||||
ContainerView,
|
||||
SystemStats,
|
||||
SystemStatsSample,
|
||||
TopContainerSample,
|
||||
@@ -30,7 +33,9 @@ import type {
|
||||
BrowseResult,
|
||||
DnsZone,
|
||||
DnsRecordView,
|
||||
BackupInfo
|
||||
BackupInfo,
|
||||
Workload,
|
||||
WorkloadKind
|
||||
} from './types';
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
@@ -1047,4 +1052,67 @@ export async function getStackLogs(
|
||||
return res.text();
|
||||
}
|
||||
|
||||
// ── Workloads ───────────────────────────────────────────────────────
|
||||
|
||||
export function listWorkloads(kind?: WorkloadKind, signal?: AbortSignal): Promise<Workload[]> {
|
||||
const path = kind ? `/api/workloads?kind=${encodeURIComponent(kind)}` : '/api/workloads';
|
||||
return get<Workload[]>(path, signal);
|
||||
}
|
||||
|
||||
export function getWorkload(id: string, signal?: AbortSignal): Promise<Workload> {
|
||||
return get<Workload>(`/api/workloads/${id}`, signal);
|
||||
}
|
||||
|
||||
export function listWorkloadContainers(id: string, signal?: AbortSignal): Promise<Container[]> {
|
||||
return get<Container[]>(`/api/workloads/${id}/containers`, signal);
|
||||
}
|
||||
|
||||
export function setWorkloadAppID(id: string, appID: string): Promise<Workload> {
|
||||
return request<Workload>(`/api/workloads/${id}/app`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ app_id: appID })
|
||||
});
|
||||
}
|
||||
|
||||
// ── Containers (global index) ───────────────────────────────────────
|
||||
|
||||
export interface ListContainersFilter {
|
||||
workload_id?: string;
|
||||
kind?: WorkloadKind;
|
||||
state?: string;
|
||||
app_id?: string;
|
||||
}
|
||||
|
||||
export function listContainers(filter: ListContainersFilter = {}, signal?: AbortSignal): Promise<ContainerView[]> {
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(filter)) {
|
||||
if (v) params.set(k, String(v));
|
||||
}
|
||||
const qs = params.toString();
|
||||
const path = qs ? `/api/containers?${qs}` : '/api/containers';
|
||||
return get<ContainerView[]>(path, signal);
|
||||
}
|
||||
|
||||
// ── Apps ────────────────────────────────────────────────────────────
|
||||
|
||||
export function listApps(signal?: AbortSignal): Promise<App[]> {
|
||||
return get<App[]>('/api/apps', signal);
|
||||
}
|
||||
|
||||
export function getApp(id: string, signal?: AbortSignal): Promise<App> {
|
||||
return get<App>(`/api/apps/${id}`, signal);
|
||||
}
|
||||
|
||||
export function createApp(data: { name: string; description?: string }): Promise<App> {
|
||||
return post<App>('/api/apps', data);
|
||||
}
|
||||
|
||||
export function updateApp(id: string, data: { name: string; description?: string }): Promise<App> {
|
||||
return put<App>(`/api/apps/${id}`, data);
|
||||
}
|
||||
|
||||
export function deleteApp(id: string): Promise<void> {
|
||||
return del<void>(`/api/apps/${id}`);
|
||||
}
|
||||
|
||||
export { ApiError };
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"logout": "Log out",
|
||||
"dns": "DNS Records",
|
||||
"sites": "Sites",
|
||||
"stacks": "Stacks"
|
||||
"stacks": "Stacks",
|
||||
"containers": "Containers"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"logout": "Выйти",
|
||||
"dns": "DNS-записи",
|
||||
"sites": "Сайты",
|
||||
"stacks": "Стеки"
|
||||
"stacks": "Стеки",
|
||||
"containers": "Контейнеры"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
|
||||
@@ -539,3 +539,74 @@ export interface SystemStatsSample {
|
||||
disk_total_bytes: number;
|
||||
}
|
||||
|
||||
// ── Workload / Container / App ────────────────────────────────────
|
||||
|
||||
export type WorkloadKind = 'project' | 'stack' | 'site';
|
||||
|
||||
/**
|
||||
* Workload is the unifying primitive over Project / Stack / StaticSite.
|
||||
* Read-only at this layer — mutations go through the kind-specific endpoints.
|
||||
*/
|
||||
export interface Workload {
|
||||
id: string;
|
||||
kind: WorkloadKind;
|
||||
ref_id: string;
|
||||
name: string;
|
||||
app_id: string;
|
||||
notification_url: string;
|
||||
webhook_require_signature: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type ContainerState =
|
||||
| 'running'
|
||||
| 'stopped'
|
||||
| 'failed'
|
||||
| 'removing'
|
||||
| 'missing'
|
||||
| 'starting'
|
||||
| 'created'
|
||||
| 'restarting'
|
||||
| 'paused'
|
||||
| string;
|
||||
|
||||
/** A row from the normalized containers index. */
|
||||
export interface Container {
|
||||
id: string;
|
||||
workload_id: string;
|
||||
workload_kind: WorkloadKind;
|
||||
role: string;
|
||||
container_id: string;
|
||||
image_ref: string;
|
||||
image_tag: string;
|
||||
host: string;
|
||||
state: ContainerState;
|
||||
port: number;
|
||||
subdomain: string;
|
||||
proxy_route_id: string;
|
||||
npm_proxy_id: number;
|
||||
last_seen_at: string;
|
||||
extra_json: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container row decorated with workload + app names by the backend so the
|
||||
* global Containers table can render without N+1 fetches.
|
||||
*/
|
||||
export interface ContainerView extends Container {
|
||||
workload_name: string;
|
||||
app_id?: string;
|
||||
app_name?: string;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user