feat(dashboard): Members tab on the app detail page
A new "Members" tab is rendered between Domains and Settings for callers whose `my_role` on the app is `app_admin` (owners always; explicit member-app_admins; admins do not see it — they're only implicit editors and can't manage memberships). The tab lets the caller: - See every explicit member of the app with username, email, instance- role chip, app-role chip, and joined date. Inactive users render greyed-out so admins know the row exists. - Pick a `member`-instance user from a dropdown and grant viewer / editor / app_admin access. The dropdown is populated from `/admin/admins` filtered to active members not already on the app. - Promote / demote / remove existing members via the shared `ActionMenu` kebab. Removal goes through `ConfirmModal`. Member-with-app_admin callers see a disabled add form with an explanatory message — they have authority to manage memberships but can't browse the user directory (gated on `InstanceManageUsers`), which is a known phase-3.5 caveat to revisit in a follow-up. Also extends `RoleChip` with an `appRole` prop and palette for app roles, and adds an `appMembers` namespace to api.ts mirroring the `domains` shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -296,6 +296,21 @@ export interface PatchAdminInput {
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export interface AppMemberDto {
|
||||
user_id: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
instance_role: InstanceRole;
|
||||
is_active: boolean;
|
||||
role: AppRole;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GrantAppMemberInput {
|
||||
user_id: string;
|
||||
role: AppRole;
|
||||
}
|
||||
|
||||
export interface ApiKeyDto {
|
||||
id: string;
|
||||
prefix: string;
|
||||
@@ -479,6 +494,28 @@ export const api = {
|
||||
)
|
||||
},
|
||||
|
||||
appMembers: {
|
||||
list: (idOrSlug: string) =>
|
||||
adminRequest<AppMemberDto[]>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members`
|
||||
),
|
||||
add: (idOrSlug: string, input: GrantAppMemberInput) =>
|
||||
adminRequest<AppMemberDto>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members`,
|
||||
{ method: 'POST', body: JSON.stringify(input) }
|
||||
),
|
||||
setRole: (idOrSlug: string, userId: string, role: AppRole) =>
|
||||
adminRequest<AppMemberDto>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members/${userId}`,
|
||||
{ method: 'PATCH', body: JSON.stringify({ role }) }
|
||||
),
|
||||
remove: (idOrSlug: string, userId: string) =>
|
||||
adminRequest<null>(
|
||||
`/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/members/${userId}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
},
|
||||
|
||||
execute: async (
|
||||
id: string,
|
||||
body: unknown,
|
||||
|
||||
Reference in New Issue
Block a user