feat: migrate UI to @cameleer/design-system, add backend endpoints
Backend: - Add agent_events table (V5) and lifecycle event recording - Add route catalog endpoint (GET /routes/catalog) - Add route metrics endpoint (GET /routes/metrics) - Add agent events endpoint (GET /agents/events-log) - Enrich AgentInstanceResponse with tps, errorRate, activeRoutes, uptimeSeconds - Add TimescaleDB retention/compression policies (V6) Frontend: - Replace custom Mission Control UI with @cameleer/design-system components - Rebuild all pages: Dashboard, ExchangeDetail, RoutesMetrics, AgentHealth, AgentInstance, RBAC, AuditLog, OIDC, DatabaseAdmin, OpenSearchAdmin, Swagger - New LayoutShell with design system AppShell, Sidebar, TopBar, CommandPalette - Consume design system from Gitea npm registry (@cameleer/design-system@0.0.1) - Add .npmrc for scoped registry, update Dockerfile with REGISTRY_TOKEN arg CI: - Pass REGISTRY_TOKEN build-arg to UI Docker build step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,23 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
|
||||
// ─── Types ───
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface RoleSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
system: boolean;
|
||||
source: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface GroupSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
parentGroupId: string | null;
|
||||
}
|
||||
|
||||
export interface UserSummary {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
@@ -33,17 +32,6 @@ export interface UserDetail {
|
||||
effectiveGroups: GroupSummary[];
|
||||
}
|
||||
|
||||
export interface GroupDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
parentGroupId: string | null;
|
||||
createdAt: string;
|
||||
directRoles: RoleSummary[];
|
||||
effectiveRoles: RoleSummary[];
|
||||
members: UserSummary[];
|
||||
childGroups: GroupSummary[];
|
||||
}
|
||||
|
||||
export interface RoleDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -56,6 +44,53 @@ export interface RoleDetail {
|
||||
effectivePrincipals: UserSummary[];
|
||||
}
|
||||
|
||||
export interface GroupDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
parentGroupId: string | null;
|
||||
createdAt: string;
|
||||
directRoles: RoleSummary[];
|
||||
effectiveRoles: RoleSummary[];
|
||||
members: UserSummary[];
|
||||
childGroups: GroupSummary[];
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface CreateRoleRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoleRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export interface CreateGroupRequest {
|
||||
name: string;
|
||||
parentGroupId?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateGroupRequest {
|
||||
name: string;
|
||||
parentGroupId?: string | null;
|
||||
}
|
||||
|
||||
// ── Stats Hook ───────────────────────────────────────────────────────
|
||||
|
||||
export interface RbacStats {
|
||||
userCount: number;
|
||||
activeUserCount: number;
|
||||
@@ -64,53 +99,6 @@ export interface RbacStats {
|
||||
roleCount: number;
|
||||
}
|
||||
|
||||
// ─── Query hooks ───
|
||||
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'users'],
|
||||
queryFn: () => adminFetch<UserDetail[]>('/users'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUser(userId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'users', userId],
|
||||
queryFn: () => adminFetch<UserDetail>(`/users/${encodeURIComponent(userId!)}`),
|
||||
enabled: !!userId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroups() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'groups'],
|
||||
queryFn: () => adminFetch<GroupDetail[]>('/groups'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGroup(groupId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'groups', groupId],
|
||||
queryFn: () => adminFetch<GroupDetail>(`/groups/${groupId}`),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRoles() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'roles'],
|
||||
queryFn: () => adminFetch<RoleDetail[]>('/roles'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRole(roleId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'roles', roleId],
|
||||
queryFn: () => adminFetch<RoleDetail>(`/roles/${roleId}`),
|
||||
enabled: !!roleId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRbacStats() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'rbac', 'stats'],
|
||||
@@ -118,162 +106,69 @@ export function useRbacStats() {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Mutation hooks ───
|
||||
// ── User Query Hooks ───────────────────────────────────────────────────
|
||||
|
||||
export function useAssignRoleToUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}/roles/${roleId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'users'],
|
||||
queryFn: () => adminFetch<UserDetail[]>('/users'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveRoleFromUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}/roles/${roleId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
export function useUser(userId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'users', userId],
|
||||
queryFn: () => adminFetch<UserDetail>(`/users/${userId}`),
|
||||
enabled: !!userId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddUserToGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}/groups/${groupId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
// ── Role Query Hooks ───────────────────────────────────────────────────
|
||||
|
||||
export function useRoles() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'roles'],
|
||||
queryFn: () => adminFetch<RoleDetail[]>('/roles'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveUserFromGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}/groups/${groupId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
export function useRole(roleId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'roles', roleId],
|
||||
queryFn: () => adminFetch<RoleDetail>(`/roles/${roleId}`),
|
||||
enabled: !!roleId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string; parentGroupId?: string }) =>
|
||||
adminFetch<{ id: string }>('/groups', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
// ── Group Query Hooks ──────────────────────────────────────────────────
|
||||
|
||||
export function useGroups() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'groups'],
|
||||
queryFn: () => adminFetch<GroupDetail[]>('/groups'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: string; name?: string; parentGroupId?: string | null }) =>
|
||||
adminFetch(`/groups/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
export function useGroup(groupId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'groups', groupId],
|
||||
queryFn: () => adminFetch<GroupDetail>(`/groups/${groupId}`),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
adminFetch(`/groups/${id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignRoleToGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
||||
adminFetch(`/groups/${groupId}/roles/${roleId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveRoleFromGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
||||
adminFetch(`/groups/${groupId}/roles/${roleId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name: string; description?: string; scope?: string }) =>
|
||||
adminFetch<{ id: string }>('/roles', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: string; name?: string; description?: string; scope?: string }) =>
|
||||
adminFetch(`/roles/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
adminFetch(`/roles/${id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
// ── User Mutation Hooks ────────────────────────────────────────────────
|
||||
|
||||
export function useCreateUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { username: string; displayName?: string; email?: string; password?: string }) =>
|
||||
mutationFn: (req: CreateUserRequest) =>
|
||||
adminFetch<UserDetail>('/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -281,13 +176,13 @@ export function useCreateUser() {
|
||||
export function useUpdateUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, ...data }: { userId: string; displayName?: string; email?: string }) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}`, {
|
||||
mutationFn: ({ userId, ...req }: UpdateUserRequest & { userId: string }) =>
|
||||
adminFetch<void>(`/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -296,9 +191,163 @@ export function useDeleteUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (userId: string) =>
|
||||
adminFetch(`/users/${encodeURIComponent(userId)}`, { method: 'DELETE' }),
|
||||
adminFetch<void>(`/users/${userId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'rbac'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignRoleToUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
||||
adminFetch<void>(`/users/${userId}/roles/${roleId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveRoleFromUser() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, roleId }: { userId: string; roleId: string }) =>
|
||||
adminFetch<void>(`/users/${userId}/roles/${roleId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddUserToGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
||||
adminFetch<void>(`/users/${userId}/groups/${groupId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveUserFromGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, groupId }: { userId: string; groupId: string }) =>
|
||||
adminFetch<void>(`/users/${userId}/groups/${groupId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Role Mutation Hooks ────────────────────────────────────────────────
|
||||
|
||||
export function useCreateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (req: CreateRoleRequest) =>
|
||||
adminFetch<{ id: string }>('/roles', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...req }: UpdateRoleRequest & { id: string }) =>
|
||||
adminFetch<void>(`/roles/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteRole() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
adminFetch<void>(`/roles/${id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Group Mutation Hooks ───────────────────────────────────────────────
|
||||
|
||||
export function useCreateGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (req: CreateGroupRequest) =>
|
||||
adminFetch<{ id: string }>('/groups', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, ...req }: UpdateGroupRequest & { id: string }) =>
|
||||
adminFetch<void>(`/groups/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
adminFetch<void>(`/groups/${id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssignRoleToGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
||||
adminFetch<void>(`/groups/${groupId}/roles/${roleId}`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveRoleFromGroup() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ groupId, roleId }: { groupId: string; roleId: string }) =>
|
||||
adminFetch<void>(`/groups/${groupId}/roles/${roleId}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'groups'] });
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'roles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user