feat(dashboard): group tree, detail page, and members tab
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Group[]>('/api/v1/admin/groups'),
|
||||
get: (idOrSlug: string) =>
|
||||
adminRequest<GroupDetail>(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`),
|
||||
create: (input: CreateGroupInput) =>
|
||||
adminRequest<Group>('/api/v1/admin/groups', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
}),
|
||||
update: (idOrSlug: string, input: PatchGroupInput) =>
|
||||
adminRequest<Group>(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(input)
|
||||
}),
|
||||
reparent: (idOrSlug: string, parent: string | null) =>
|
||||
adminRequest<Group>(
|
||||
`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/reparent`,
|
||||
{ method: 'POST', body: JSON.stringify({ parent }) }
|
||||
),
|
||||
delete: (idOrSlug: string) =>
|
||||
adminRequest<null>(`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
members: {
|
||||
list: (idOrSlug: string) =>
|
||||
adminRequest<GroupMember[]>(
|
||||
`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members`
|
||||
),
|
||||
grant: (idOrSlug: string, input: GrantAppMemberInput) =>
|
||||
adminRequest<GroupMember>(
|
||||
`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members`,
|
||||
{ method: 'POST', body: JSON.stringify(input) }
|
||||
),
|
||||
update: (idOrSlug: string, userId: string, role: AppRole) =>
|
||||
adminRequest<GroupMember>(
|
||||
`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members/${userId}`,
|
||||
{ method: 'PATCH', body: JSON.stringify({ role }) }
|
||||
),
|
||||
remove: (idOrSlug: string, userId: string) =>
|
||||
adminRequest<null>(
|
||||
`/api/v1/admin/groups/${encodeURIComponent(idOrSlug)}/members/${userId}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
domains: {
|
||||
listForApp: (idOrSlug: string) =>
|
||||
adminRequest<AppDomain[]>(
|
||||
|
||||
Reference in New Issue
Block a user