From 87259391726cee3d4c3097ce2c06e62bb81066d1 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 24 Jun 2026 20:15:44 +0200 Subject: [PATCH] feat(dashboard): group tree, detail page, and members tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SvelteKit UI for Phase-2 groups, mirroring the apps/users patterns: - api.ts: Group/GroupDetail/GroupMember types + an api.groups client (list/get/create/update/reparent/delete + nested members CRUD); optional group selector wired into app create. - routes/groups: a collapsible tree overview (assembled from parent_id) with a New-group form, and a detail page with breadcrumb, subgroups/apps lists, a rename form (no slug field — slug is frozen), a reparent control, a delete button that surfaces the 409 RESTRICT message, and a Members tab reusing RoleChip/ConfirmModal/ActionMenu. - +layout.svelte: a Groups nav entry beside Apps. npm run check: 0 errors; npm run build: success. Co-Authored-By: Claude Opus 4.8 (1M context) --- dashboard/src/lib/api.ts | 89 ++ dashboard/src/routes/+layout.svelte | 1 + dashboard/src/routes/apps/+page.svelte | 27 +- dashboard/src/routes/groups/+page.svelte | 366 ++++++++ .../src/routes/groups/[slug]/+page.svelte | 789 ++++++++++++++++++ 5 files changed, 1270 insertions(+), 2 deletions(-) create mode 100644 dashboard/src/routes/groups/+page.svelte create mode 100644 dashboard/src/routes/groups/[slug]/+page.svelte diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index a8b111a..78a50a1 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -50,6 +50,47 @@ export interface App { export type AppRole = 'app_admin' | 'editor' | 'viewer'; +export interface Group { + id: string; + parent_id: string | null; + slug: string; + name: string; + description: string | null; + structure_version: number; + created_at: string; + updated_at: string; +} + +export interface GroupDetail extends Group { + /** Root → … → this node breadcrumb. */ + path: Group[]; + subgroups: Group[]; + apps: App[]; +} + +export interface GroupMember { + user_id: string; + username: string; + email: string | null; + instance_role: InstanceRole; + is_active: boolean; + role: AppRole; + created_at: string; +} + +export interface CreateGroupInput { + slug: string; + name: string; + description?: string | null; + /** Parent group slug or id; omit (or null) for a root group. */ + parent?: string | null; +} + +export interface PatchGroupInput { + name?: string; + description?: string | null; +} + export type DomainShape = 'exact' | 'wildcard' | 'parameterized'; export interface AppDomain { @@ -89,6 +130,8 @@ export interface CreateAppInput { name: string; description?: string | null; force_takeover?: boolean; + /** Parent group slug or id; omit for the root group. */ + group?: string | null; } export interface PatchAppInput { @@ -778,6 +821,52 @@ export const api = { ) }, + groups: { + list: () => adminRequest('/api/v1/admin/groups'), + get: (idOrSlug: string) => + adminRequest(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`), + create: (input: CreateGroupInput) => + adminRequest('/api/v1/admin/groups', { + method: 'POST', + body: JSON.stringify(input) + }), + update: (idOrSlug: string, input: PatchGroupInput) => + adminRequest(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`, { + method: 'PATCH', + body: JSON.stringify(input) + }), + reparent: (idOrSlug: string, parent: string | null) => + adminRequest( + `/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/reparent`, + { method: 'POST', body: JSON.stringify({ parent }) } + ), + delete: (idOrSlug: string) => + adminRequest(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`, { + method: 'DELETE' + }), + members: { + list: (idOrSlug: string) => + adminRequest( + `/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members` + ), + grant: (idOrSlug: string, input: GrantAppMemberInput) => + adminRequest( + `/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members`, + { method: 'POST', body: JSON.stringify(input) } + ), + update: (idOrSlug: string, userId: string, role: AppRole) => + adminRequest( + `/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members/${userId}`, + { method: 'PATCH', body: JSON.stringify({ role }) } + ), + remove: (idOrSlug: string, userId: string) => + adminRequest( + `/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members/${userId}`, + { method: 'DELETE' } + ) + } + }, + domains: { listForApp: (idOrSlug: string) => adminRequest( diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte index 7f1905f..1a1b2e3 100644 --- a/dashboard/src/routes/+layout.svelte +++ b/dashboard/src/routes/+layout.svelte @@ -55,6 +55,7 @@ PiCloud