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:
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AuditEvent {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
@@ -8,18 +10,18 @@ export interface AuditEvent {
|
||||
action: string;
|
||||
category: string;
|
||||
target: string;
|
||||
detail: Record<string, unknown>;
|
||||
detail: Record<string, unknown> | null;
|
||||
result: string;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
export interface AuditLogParams {
|
||||
from?: string;
|
||||
to?: string;
|
||||
username?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
sort?: string;
|
||||
order?: string;
|
||||
page?: number;
|
||||
@@ -34,21 +36,25 @@ export interface AuditLogResponse {
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export function useAuditLog(params: AuditLogParams) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.from) query.set('from', params.from);
|
||||
if (params.to) query.set('to', params.to);
|
||||
if (params.username) query.set('username', params.username);
|
||||
if (params.category) query.set('category', params.category);
|
||||
if (params.search) query.set('search', params.search);
|
||||
if (params.sort) query.set('sort', params.sort);
|
||||
if (params.order) query.set('order', params.order);
|
||||
if (params.page !== undefined) query.set('page', String(params.page));
|
||||
if (params.size !== undefined) query.set('size', String(params.size));
|
||||
const qs = query.toString();
|
||||
// ── Query Hooks ────────────────────────────────────────────────────────
|
||||
|
||||
export function useAuditLog(params: AuditLogParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'audit', params],
|
||||
queryFn: () => adminFetch<AuditLogResponse>(`/audit${qs ? `?${qs}` : ''}`),
|
||||
queryFn: () => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.username) qs.set('username', params.username);
|
||||
if (params.category) qs.set('category', params.category);
|
||||
if (params.search) qs.set('search', params.search);
|
||||
if (params.from) qs.set('from', params.from);
|
||||
if (params.to) qs.set('to', params.to);
|
||||
if (params.sort) qs.set('sort', params.sort);
|
||||
if (params.order) qs.set('order', params.order);
|
||||
if (params.page !== undefined) qs.set('page', String(params.page));
|
||||
if (params.size !== undefined) qs.set('size', String(params.size));
|
||||
const query = qs.toString();
|
||||
return adminFetch<AuditLogResponse>(`/audit${query ? `?${query}` : ''}`);
|
||||
},
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DatabaseStatus {
|
||||
connected: boolean;
|
||||
version: string;
|
||||
host: string;
|
||||
schema: string;
|
||||
version: string | null;
|
||||
host: string | null;
|
||||
schema: string | null;
|
||||
timescaleDb: boolean;
|
||||
}
|
||||
|
||||
export interface PoolStats {
|
||||
activeConnections: number;
|
||||
idleConnections: number;
|
||||
pendingThreads: number;
|
||||
maxPoolSize: number;
|
||||
maxWaitMs: number;
|
||||
threadsAwaitingConnection: number;
|
||||
connectionTimeout: number;
|
||||
maximumPoolSize: number;
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
@@ -33,18 +35,21 @@ export interface ActiveQuery {
|
||||
query: string;
|
||||
}
|
||||
|
||||
// ── Query Hooks ────────────────────────────────────────────────────────
|
||||
|
||||
export function useDatabaseStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'database', 'status'],
|
||||
queryFn: () => adminFetch<DatabaseStatus>('/database/status'),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDatabasePool() {
|
||||
export function useConnectionPool() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'database', 'pool'],
|
||||
queryFn: () => adminFetch<PoolStats>('/database/pool'),
|
||||
refetchInterval: 15000,
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,23 +57,27 @@ export function useDatabaseTables() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'database', 'tables'],
|
||||
queryFn: () => adminFetch<TableInfo[]>('/database/tables'),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDatabaseQueries() {
|
||||
export function useActiveQueries() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'database', 'queries'],
|
||||
queryFn: () => adminFetch<ActiveQuery[]>('/database/queries'),
|
||||
refetchInterval: 15000,
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Mutation Hooks ─────────────────────────────────────────────────────
|
||||
|
||||
export function useKillQuery() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (pid: number) => {
|
||||
await adminFetch<void>(`/database/queries/${pid}/kill`, { method: 'POST' });
|
||||
mutationFn: (pid: number) =>
|
||||
adminFetch<void>(`/database/queries/${pid}/kill`, { method: 'POST' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'database', 'queries'] });
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'database', 'queries'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface OpenSearchStatus {
|
||||
reachable: boolean;
|
||||
connected: boolean;
|
||||
clusterHealth: string;
|
||||
version: string;
|
||||
nodeCount: number;
|
||||
host: string;
|
||||
version: string | null;
|
||||
numberOfNodes: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PipelineStats {
|
||||
queueDepth: number;
|
||||
maxQueueSize: number;
|
||||
indexedCount: number;
|
||||
failedCount: number;
|
||||
indexedCount: number;
|
||||
debounceMs: number;
|
||||
indexingRate: number;
|
||||
lastIndexedAt: string | null;
|
||||
@@ -21,15 +23,15 @@ export interface PipelineStats {
|
||||
|
||||
export interface IndexInfo {
|
||||
name: string;
|
||||
health: string;
|
||||
docCount: number;
|
||||
size: string;
|
||||
sizeBytes: number;
|
||||
health: string;
|
||||
primaryShards: number;
|
||||
replicaShards: number;
|
||||
replicas: number;
|
||||
}
|
||||
|
||||
export interface IndicesPageResponse {
|
||||
export interface IndicesPage {
|
||||
indices: IndexInfo[];
|
||||
totalIndices: number;
|
||||
totalDocs: number;
|
||||
@@ -44,20 +46,17 @@ export interface PerformanceStats {
|
||||
requestCacheHitRate: number;
|
||||
searchLatencyMs: number;
|
||||
indexingLatencyMs: number;
|
||||
jvmHeapUsedBytes: number;
|
||||
jvmHeapMaxBytes: number;
|
||||
heapUsedBytes: number;
|
||||
heapMaxBytes: number;
|
||||
}
|
||||
|
||||
export interface IndicesParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
// ── Query Hooks ────────────────────────────────────────────────────────
|
||||
|
||||
export function useOpenSearchStatus() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'opensearch', 'status'],
|
||||
queryFn: () => adminFetch<OpenSearchStatus>('/opensearch/status'),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,42 +64,41 @@ export function usePipelineStats() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'opensearch', 'pipeline'],
|
||||
queryFn: () => adminFetch<PipelineStats>('/opensearch/pipeline'),
|
||||
refetchInterval: 15000,
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useIndices(params: IndicesParams) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.search) query.set('search', params.search);
|
||||
if (params.page !== undefined) query.set('page', String(params.page));
|
||||
if (params.size !== undefined) query.set('size', String(params.size));
|
||||
const qs = query.toString();
|
||||
|
||||
export function useOpenSearchIndices(page = 0, size = 20, search = '') {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'opensearch', 'indices', params],
|
||||
queryFn: () =>
|
||||
adminFetch<IndicesPageResponse>(
|
||||
`/opensearch/indices${qs ? `?${qs}` : ''}`,
|
||||
),
|
||||
queryKey: ['admin', 'opensearch', 'indices', page, size, search],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('size', String(size));
|
||||
if (search) params.set('search', search);
|
||||
return adminFetch<IndicesPage>(`/opensearch/indices?${params}`);
|
||||
},
|
||||
placeholderData: (prev) => prev,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePerformanceStats() {
|
||||
export function useOpenSearchPerformance() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'opensearch', 'performance'],
|
||||
queryFn: () => adminFetch<PerformanceStats>('/opensearch/performance'),
|
||||
refetchInterval: 15000,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Mutation Hooks ─────────────────────────────────────────────────────
|
||||
|
||||
export function useDeleteIndex() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (indexName: string) => {
|
||||
await adminFetch<void>(`/opensearch/indices/${encodeURIComponent(indexName)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
mutationFn: (indexName: string) =>
|
||||
adminFetch<void>(`/opensearch/indices/${indexName}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'opensearch', 'indices'] });
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'opensearch', 'indices'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { adminFetch } from './admin-api';
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DatabaseThresholds {
|
||||
connectionPoolWarning: number;
|
||||
connectionPoolCritical: number;
|
||||
@@ -24,6 +26,8 @@ export interface ThresholdConfig {
|
||||
opensearch: OpenSearchThresholds;
|
||||
}
|
||||
|
||||
// ── Query Hooks ────────────────────────────────────────────────────────
|
||||
|
||||
export function useThresholds() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'thresholds'],
|
||||
@@ -31,15 +35,18 @@ export function useThresholds() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveThresholds() {
|
||||
// ── Mutation Hooks ─────────────────────────────────────────────────────
|
||||
|
||||
export function useUpdateThresholds() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: ThresholdConfig) => {
|
||||
await adminFetch<ThresholdConfig>('/thresholds', {
|
||||
mutationFn: (config: ThresholdConfig) =>
|
||||
adminFetch<ThresholdConfig>('/thresholds', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['admin', 'thresholds'] });
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'thresholds'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../client';
|
||||
import { config } from '../../config';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
|
||||
export function useAgents(status?: string) {
|
||||
export function useAgents(status?: string, group?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['agents', status],
|
||||
queryKey: ['agents', status, group],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await api.GET('/agents', {
|
||||
params: { query: status ? { status } : {} },
|
||||
params: { query: { ...(status ? { status } : {}), ...(group ? { group } : {}) } },
|
||||
});
|
||||
if (error) throw new Error('Failed to load agents');
|
||||
return data!;
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAgentEvents(appId?: string, agentId?: string, limit = 50) {
|
||||
return useQuery({
|
||||
queryKey: ['agents', 'events', appId, agentId, limit],
|
||||
queryFn: async () => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const params = new URLSearchParams();
|
||||
if (appId) params.set('appId', appId);
|
||||
if (agentId) params.set('agentId', agentId);
|
||||
params.set('limit', String(limit));
|
||||
const res = await fetch(`${config.apiBaseUrl}/agents/events-log?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Cameleer-Protocol-Version': '1',
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to load agent events');
|
||||
return res.json();
|
||||
},
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
43
ui/src/api/queries/catalog.ts
Normal file
43
ui/src/api/queries/catalog.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { config } from '../../config';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
|
||||
export function useRouteCatalog() {
|
||||
return useQuery({
|
||||
queryKey: ['routes', 'catalog'],
|
||||
queryFn: async () => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const res = await fetch(`${config.apiBaseUrl}/routes/catalog`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Cameleer-Protocol-Version': '1',
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to load route catalog');
|
||||
return res.json();
|
||||
},
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRouteMetrics(from?: string, to?: string, appId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['routes', 'metrics', from, to, appId],
|
||||
queryFn: async () => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('from', from);
|
||||
if (to) params.set('to', to);
|
||||
if (appId) params.set('appId', appId);
|
||||
const res = await fetch(`${config.apiBaseUrl}/routes/metrics?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Cameleer-Protocol-Version': '1',
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to load route metrics');
|
||||
return res.json();
|
||||
},
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../client';
|
||||
import type { OidcAdminConfigRequest } from '../types';
|
||||
|
||||
export function useOidcConfig() {
|
||||
return useQuery({
|
||||
queryKey: ['admin', 'oidc'],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await api.GET('/admin/oidc');
|
||||
if (error) throw new Error('Failed to load OIDC config');
|
||||
return data!;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveOidcConfig() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (body: OidcAdminConfigRequest) => {
|
||||
const { data, error } = await api.PUT('/admin/oidc', { body });
|
||||
if (error) throw new Error('Failed to save OIDC config');
|
||||
return data!;
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'oidc'] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestOidcConnection() {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data, error } = await api.POST('/admin/oidc/test');
|
||||
if (error) throw new Error('OIDC test failed');
|
||||
return data!;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteOidcConfig() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { error } = await api.DELETE('/admin/oidc');
|
||||
if (error) throw new Error('Failed to delete OIDC config');
|
||||
},
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'oidc'] }),
|
||||
});
|
||||
}
|
||||
3503
ui/src/api/schema.d.ts
vendored
3503
ui/src/api/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -17,3 +17,8 @@ export type ErrorResponse = components['schemas']['ErrorResponse'];
|
||||
export type DiagramLayout = components['schemas']['DiagramLayout'];
|
||||
export type PositionedNode = components['schemas']['PositionedNode'];
|
||||
export type PositionedEdge = components['schemas']['PositionedEdge'];
|
||||
export type AppCatalogEntry = components['schemas']['AppCatalogEntry'];
|
||||
export type RouteSummary = components['schemas']['RouteSummary'];
|
||||
export type AgentSummary = components['schemas']['AgentSummary'];
|
||||
export type RouteMetrics = components['schemas']['RouteMetrics'];
|
||||
export type AgentEventResponse = components['schemas']['AgentEventResponse'];
|
||||
|
||||
Reference in New Issue
Block a user