@syncular/console 0.0.6-159 → 0.0.6-168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,10 @@
2
2
  * React Query hooks for Console API
3
3
  */
4
4
 
5
- import { unwrap } from '@syncular/transport-http';
6
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
+ import type { ConnectionConfig } from '../lib/api';
7
7
  import type {
8
+ ApiKeyType,
8
9
  ConsoleApiKey,
9
10
  ConsoleApiKeyBulkRevokeResponse,
10
11
  ConsoleBlobListResponse,
@@ -25,57 +26,89 @@ import type {
25
26
  TimeseriesRange,
26
27
  TimeseriesStatsResponse,
27
28
  } from '../lib/types';
28
- import { useApiClient, useConnection } from './ConnectionContext';
29
+ import { useConnection } from './ConnectionContext';
29
30
  import { useInstanceContext } from './useInstanceContext';
30
31
 
32
+ type StatsParams = {
33
+ partitionId?: string;
34
+ instanceId?: string;
35
+ };
36
+ type TimeseriesParams = {
37
+ interval?: TimeseriesInterval;
38
+ range?: TimeseriesRange;
39
+ partitionId?: string;
40
+ instanceId?: string;
41
+ };
42
+ type LatencyParams = {
43
+ range?: TimeseriesRange;
44
+ partitionId?: string;
45
+ instanceId?: string;
46
+ };
47
+ type ListParams = {
48
+ limit?: number;
49
+ offset?: number;
50
+ partitionId?: string;
51
+ instanceId?: string;
52
+ };
53
+ type TimelineParams = ListParams & {
54
+ view?: 'all' | 'commits' | 'events';
55
+ eventType?: 'push' | 'pull';
56
+ actorId?: string;
57
+ clientId?: string;
58
+ requestId?: string;
59
+ traceId?: string;
60
+ table?: string;
61
+ outcome?: string;
62
+ search?: string;
63
+ from?: string;
64
+ to?: string;
65
+ };
66
+ type EntityLookupOptions = {
67
+ enabled?: boolean;
68
+ partitionId?: string;
69
+ instanceId?: string;
70
+ };
71
+ type RefetchableQueryOptions = {
72
+ refetchIntervalMs?: number;
73
+ enabled?: boolean;
74
+ };
75
+ type PrunePreviewOptions = {
76
+ enabled?: boolean;
77
+ instanceId?: string;
78
+ };
79
+ type OperationEventsParams = ListParams & {
80
+ operationType?: ConsoleOperationType;
81
+ };
82
+ type ApiKeysParams = {
83
+ limit?: number;
84
+ offset?: number;
85
+ type?: 'relay' | 'proxy' | 'admin';
86
+ status?: 'active' | 'revoked' | 'expiring';
87
+ expiresWithinDays?: number;
88
+ instanceId?: string;
89
+ };
90
+ type BlobsOptions = {
91
+ prefix?: string;
92
+ cursor?: string;
93
+ limit?: number;
94
+ refetchIntervalMs?: number;
95
+ };
96
+
31
97
  const queryKeys = {
32
- stats: (params?: { partitionId?: string; instanceId?: string }) =>
33
- ['console', 'stats', params] as const,
34
- timeseries: (params?: {
35
- interval?: TimeseriesInterval;
36
- range?: TimeseriesRange;
37
- partitionId?: string;
38
- instanceId?: string;
39
- }) => ['console', 'stats', 'timeseries', params] as const,
40
- latency: (params?: {
41
- range?: TimeseriesRange;
42
- partitionId?: string;
43
- instanceId?: string;
44
- }) => ['console', 'stats', 'latency', params] as const,
45
- commits: (params?: {
46
- limit?: number;
47
- offset?: number;
48
- partitionId?: string;
49
- instanceId?: string;
50
- }) => ['console', 'commits', params] as const,
98
+ stats: (params?: StatsParams) => ['console', 'stats', params] as const,
99
+ timeseries: (params?: TimeseriesParams) =>
100
+ ['console', 'stats', 'timeseries', params] as const,
101
+ latency: (params?: LatencyParams) =>
102
+ ['console', 'stats', 'latency', params] as const,
103
+ commits: (params?: ListParams) => ['console', 'commits', params] as const,
51
104
  commitDetail: (
52
105
  seq?: string | number,
53
106
  partitionId?: string,
54
107
  instanceId?: string
55
108
  ) => ['console', 'commit-detail', seq, partitionId, instanceId] as const,
56
- timeline: (params?: {
57
- limit?: number;
58
- offset?: number;
59
- partitionId?: string;
60
- instanceId?: string;
61
- view?: 'all' | 'commits' | 'events';
62
- eventType?: 'push' | 'pull';
63
- actorId?: string;
64
- clientId?: string;
65
- requestId?: string;
66
- traceId?: string;
67
- table?: string;
68
- outcome?: string;
69
- search?: string;
70
- from?: string;
71
- to?: string;
72
- }) => ['console', 'timeline', params] as const,
73
- clients: (params?: {
74
- limit?: number;
75
- offset?: number;
76
- partitionId?: string;
77
- instanceId?: string;
78
- }) => ['console', 'clients', params] as const,
109
+ timeline: (params?: TimelineParams) =>
110
+ ['console', 'timeline', params] as const,
111
+ clients: (params?: ListParams) => ['console', 'clients', params] as const,
79
112
  eventDetail: (
80
113
  id?: string | number,
81
114
  partitionId?: string,
@@ -90,21 +123,9 @@ const queryKeys = {
90
123
  ['console', 'handlers', instanceId] as const,
91
124
  prunePreview: (instanceId?: string) =>
92
125
  ['console', 'prune', 'preview', instanceId] as const,
93
- operations: (params?: {
94
- limit?: number;
95
- offset?: number;
96
- operationType?: ConsoleOperationType;
97
- partitionId?: string;
98
- instanceId?: string;
99
- }) => ['console', 'operations', params] as const,
100
- apiKeys: (params?: {
101
- limit?: number;
102
- offset?: number;
103
- type?: 'relay' | 'proxy' | 'admin';
104
- status?: 'active' | 'revoked' | 'expiring';
105
- expiresWithinDays?: number;
106
- instanceId?: string;
107
- }) => ['console', 'api-keys', params] as const,
126
+ operations: (params?: OperationEventsParams) =>
127
+ ['console', 'operations', params] as const,
128
+ apiKeys: (params?: ApiKeysParams) => ['console', 'api-keys', params] as const,
108
129
  storage: (params?: Record<string, unknown>) =>
109
130
  ['console', 'storage', params] as const,
110
131
  };
@@ -117,220 +138,305 @@ function resolveRefetchInterval(
117
138
  return refreshIntervalMs ?? defaultValueMs;
118
139
  }
119
140
 
120
- interface InstanceQueryFilter {
141
+ type ConsoleQueryValue = string | number | boolean | null | undefined;
142
+ type QueryKey = readonly unknown[];
143
+ type EvictClientRequest = {
144
+ clientId: string;
145
+ partitionId?: string;
121
146
  instanceId?: string;
122
- }
123
-
124
- function withInstanceQuery<T extends Record<string, unknown>>(
125
- query: T,
126
- instanceId: string | undefined
127
- ): T & InstanceQueryFilter {
128
- if (!instanceId) return query;
129
- return { ...query, instanceId };
130
- }
131
-
132
- function serializePathSegment(value: string | number): string {
133
- return encodeURIComponent(String(value));
147
+ };
148
+ type NotifyDataChangeRequest = {
149
+ tables: string[];
150
+ partitionId?: string;
151
+ instanceId?: string;
152
+ };
153
+ type CreateApiKeyRequest = {
154
+ name: string;
155
+ keyType: ApiKeyType;
156
+ scopeKeys?: string[];
157
+ actorId?: string;
158
+ expiresInDays?: number;
159
+ };
160
+ type ApiKeySecretResponse = { key: ConsoleApiKey; secretKey: string };
161
+
162
+ function buildQueryString(
163
+ query: Record<string, ConsoleQueryValue> | undefined
164
+ ): URLSearchParams {
165
+ const params = new URLSearchParams();
166
+ if (!query) return params;
167
+ for (const [key, value] of Object.entries(query)) {
168
+ if (value === undefined || value === null) continue;
169
+ params.set(key, String(value));
170
+ }
171
+ return params;
134
172
  }
135
173
 
136
174
  function buildConsoleUrl(
137
175
  serverUrl: string,
138
176
  path: string,
139
- queryString?: URLSearchParams
177
+ query?: Record<string, ConsoleQueryValue>
140
178
  ): string {
141
179
  const baseUrl = serverUrl.endsWith('/') ? serverUrl.slice(0, -1) : serverUrl;
180
+ const queryString = buildQueryString(query);
142
181
  const suffix = queryString?.toString();
143
182
  return `${baseUrl}${path}${suffix ? `?${suffix}` : ''}`;
144
183
  }
145
184
 
146
- export function useStats(
147
- options: {
148
- refetchIntervalMs?: number;
149
- partitionId?: string;
150
- instanceId?: string;
151
- } = {}
152
- ) {
153
- const client = useApiClient();
154
- const { instanceId: selectedInstanceId } = useInstanceContext();
155
- const instanceId = options.instanceId ?? selectedInstanceId;
156
- const query = withInstanceQuery(
157
- options.partitionId ? { partitionId: options.partitionId } : {},
158
- instanceId
185
+ function requireConnection(
186
+ connectionConfig: ConnectionConfig | null,
187
+ isConnected: boolean
188
+ ): ConnectionConfig {
189
+ if (!isConnected || !connectionConfig) throw new Error('Not connected');
190
+ return connectionConfig;
191
+ }
192
+
193
+ async function fetchConsoleJson<T>(args: {
194
+ connectionConfig: ConnectionConfig;
195
+ path: string;
196
+ query?: Record<string, ConsoleQueryValue>;
197
+ method?: 'GET' | 'POST' | 'DELETE';
198
+ body?: unknown;
199
+ errorMessage: string;
200
+ }): Promise<T> {
201
+ const headers: Record<string, string> = {
202
+ Authorization: `Bearer ${args.connectionConfig.token}`,
203
+ };
204
+ if (args.body !== undefined) {
205
+ headers['Content-Type'] = 'application/json';
206
+ }
207
+ const response = await fetch(
208
+ buildConsoleUrl(args.connectionConfig.serverUrl, args.path, args.query),
209
+ {
210
+ method: args.method ?? 'GET',
211
+ headers,
212
+ body: args.body === undefined ? undefined : JSON.stringify(args.body),
213
+ }
214
+ );
215
+ if (!response.ok) throw new Error(args.errorMessage);
216
+ return response.json();
217
+ }
218
+
219
+ async function fetchConsoleBlob(args: {
220
+ connectionConfig: ConnectionConfig;
221
+ path: string;
222
+ query?: Record<string, ConsoleQueryValue>;
223
+ errorMessage: string;
224
+ }): Promise<Blob> {
225
+ const response = await fetch(
226
+ buildConsoleUrl(args.connectionConfig.serverUrl, args.path, args.query),
227
+ {
228
+ method: 'GET',
229
+ headers: { Authorization: `Bearer ${args.connectionConfig.token}` },
230
+ }
159
231
  );
232
+ if (!response.ok) throw new Error(args.errorMessage);
233
+ return response.blob();
234
+ }
235
+
236
+ function useConsoleJsonQuery<T>(options: {
237
+ queryKey: QueryKey;
238
+ path: string;
239
+ query?: Record<string, ConsoleQueryValue>;
240
+ method?: 'GET' | 'POST' | 'DELETE';
241
+ errorMessage: string;
242
+ enabled?: boolean;
243
+ refetchInterval?: number | false;
244
+ }) {
245
+ const { config: connectionConfig, isConnected } = useConnection();
246
+
247
+ return useQuery<T>({
248
+ queryKey: options.queryKey,
249
+ queryFn: () =>
250
+ fetchConsoleJson<T>({
251
+ connectionConfig: requireConnection(connectionConfig, isConnected),
252
+ path: options.path,
253
+ query: options.query,
254
+ method: options.method,
255
+ errorMessage: options.errorMessage,
256
+ }),
257
+ enabled: (options.enabled ?? true) && isConnected && !!connectionConfig,
258
+ refetchInterval: options.refetchInterval,
259
+ });
260
+ }
261
+
262
+ function useConsoleEntityQuery<T>(options: {
263
+ queryKey: QueryKey;
264
+ id: string | number | undefined;
265
+ requiredMessage: string;
266
+ path: (id: string | number) => string;
267
+ query?: Record<string, ConsoleQueryValue>;
268
+ errorMessage: string;
269
+ enabled?: boolean;
270
+ }) {
271
+ const { config: connectionConfig, isConnected } = useConnection();
272
+
273
+ return useQuery<T>({
274
+ queryKey: options.queryKey,
275
+ queryFn: () => {
276
+ if (options.id === undefined) throw new Error(options.requiredMessage);
277
+ return fetchConsoleJson<T>({
278
+ connectionConfig: requireConnection(connectionConfig, isConnected),
279
+ path: options.path(options.id),
280
+ query: options.query,
281
+ errorMessage: options.errorMessage,
282
+ });
283
+ },
284
+ enabled:
285
+ (options.enabled ?? true) &&
286
+ options.id !== undefined &&
287
+ isConnected &&
288
+ !!connectionConfig,
289
+ });
290
+ }
291
+
292
+ function invalidateConsoleQueries(
293
+ queryClient: ReturnType<typeof useQueryClient>,
294
+ queryKeysToInvalidate: ReadonlyArray<QueryKey>
295
+ ): void {
296
+ for (const queryKey of queryKeysToInvalidate) {
297
+ queryClient.invalidateQueries({ queryKey });
298
+ }
299
+ }
160
300
 
161
- return useQuery<SyncStats>({
162
- queryKey: queryKeys.stats({
301
+ function useEffectiveInstanceId(instanceId?: string): string | undefined {
302
+ const { instanceId: selectedInstanceId } = useInstanceContext();
303
+ return instanceId ?? selectedInstanceId;
304
+ }
305
+
306
+ function useConsoleJsonMutation<TResult, TVariables>(options: {
307
+ mutationFn: (args: {
308
+ connectionConfig: ConnectionConfig;
309
+ variables: TVariables;
310
+ selectedInstanceId?: string;
311
+ }) => Promise<TResult>;
312
+ invalidateQueryKeys?: ReadonlyArray<QueryKey>;
313
+ }) {
314
+ const { config: connectionConfig, isConnected } = useConnection();
315
+ const { instanceId: selectedInstanceId } = useInstanceContext();
316
+ const queryClient = useQueryClient();
317
+
318
+ return useMutation<TResult, Error, TVariables>({
319
+ mutationFn: (variables) =>
320
+ options.mutationFn({
321
+ connectionConfig: requireConnection(connectionConfig, isConnected),
322
+ variables,
323
+ selectedInstanceId,
324
+ }),
325
+ onSuccess: () => {
326
+ if (!options.invalidateQueryKeys) return;
327
+ invalidateConsoleQueries(queryClient, options.invalidateQueryKeys);
328
+ },
329
+ });
330
+ }
331
+
332
+ export function useStats(
333
+ options: StatsParams & { refetchIntervalMs?: number } = {}
334
+ ) {
335
+ const instanceId = useEffectiveInstanceId(options.instanceId);
336
+ return useConsoleJsonQuery<SyncStats>({
337
+ queryKey: queryKeys.stats({ partitionId: options.partitionId, instanceId }),
338
+ path: '/console/stats',
339
+ query: {
163
340
  partitionId: options.partitionId,
164
341
  instanceId,
165
- }),
166
- queryFn: () => {
167
- if (!client) throw new Error('Not connected');
168
- return unwrap(client.GET('/console/stats', { params: { query } }));
169
342
  },
170
- enabled: !!client,
343
+ errorMessage: 'Failed to fetch stats',
171
344
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 5000),
172
345
  });
173
346
  }
174
347
 
175
348
  export function useTimeseriesStats(
176
- params: {
177
- interval?: TimeseriesInterval;
178
- range?: TimeseriesRange;
179
- partitionId?: string;
180
- instanceId?: string;
181
- } = {},
182
- options: { refetchIntervalMs?: number; enabled?: boolean } = {}
349
+ params: TimeseriesParams = {},
350
+ options: RefetchableQueryOptions = {}
183
351
  ) {
184
- const client = useApiClient();
185
- const { config: connectionConfig } = useConnection();
186
- const { instanceId: selectedInstanceId } = useInstanceContext();
187
- const instanceId = params.instanceId ?? selectedInstanceId;
352
+ const instanceId = useEffectiveInstanceId(params.instanceId);
188
353
 
189
- return useQuery<TimeseriesStatsResponse>({
354
+ return useConsoleJsonQuery<TimeseriesStatsResponse>({
190
355
  queryKey: queryKeys.timeseries({ ...params, instanceId }),
191
- queryFn: async () => {
192
- if (!client || !connectionConfig) throw new Error('Not connected');
193
- // Use fetch directly since this endpoint may not be in OpenAPI yet
194
- const queryString = new URLSearchParams();
195
- if (params.interval) queryString.set('interval', params.interval);
196
- if (params.range) queryString.set('range', params.range);
197
- if (params.partitionId)
198
- queryString.set('partitionId', params.partitionId);
199
- if (instanceId) queryString.set('instanceId', instanceId);
200
- const response = await fetch(
201
- `${connectionConfig.serverUrl}/console/stats/timeseries?${queryString}`,
202
- { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
203
- );
204
- if (!response.ok) throw new Error('Failed to fetch timeseries stats');
205
- return response.json();
356
+ path: '/console/stats/timeseries',
357
+ query: {
358
+ interval: params.interval,
359
+ range: params.range,
360
+ partitionId: params.partitionId,
361
+ instanceId,
206
362
  },
207
- enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
363
+ errorMessage: 'Failed to fetch timeseries stats',
364
+ enabled: options.enabled,
208
365
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
209
366
  });
210
367
  }
211
368
 
212
369
  export function useLatencyStats(
213
- params: {
214
- range?: TimeseriesRange;
215
- partitionId?: string;
216
- instanceId?: string;
217
- } = {},
218
- options: { refetchIntervalMs?: number; enabled?: boolean } = {}
370
+ params: LatencyParams = {},
371
+ options: RefetchableQueryOptions = {}
219
372
  ) {
220
- const client = useApiClient();
221
- const { config: connectionConfig } = useConnection();
222
- const { instanceId: selectedInstanceId } = useInstanceContext();
223
- const instanceId = params.instanceId ?? selectedInstanceId;
373
+ const instanceId = useEffectiveInstanceId(params.instanceId);
224
374
 
225
- return useQuery<LatencyStatsResponse>({
375
+ return useConsoleJsonQuery<LatencyStatsResponse>({
226
376
  queryKey: queryKeys.latency({ ...params, instanceId }),
227
- queryFn: async () => {
228
- if (!client || !connectionConfig) throw new Error('Not connected');
229
- // Use fetch directly since this endpoint may not be in OpenAPI yet
230
- const queryString = new URLSearchParams();
231
- if (params.range) queryString.set('range', params.range);
232
- if (params.partitionId)
233
- queryString.set('partitionId', params.partitionId);
234
- if (instanceId) queryString.set('instanceId', instanceId);
235
- const response = await fetch(
236
- `${connectionConfig.serverUrl}/console/stats/latency?${queryString}`,
237
- { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
238
- );
239
- if (!response.ok) throw new Error('Failed to fetch latency stats');
240
- return response.json();
377
+ path: '/console/stats/latency',
378
+ query: {
379
+ range: params.range,
380
+ partitionId: params.partitionId,
381
+ instanceId,
241
382
  },
242
- enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
383
+ errorMessage: 'Failed to fetch latency stats',
384
+ enabled: options.enabled,
243
385
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
244
386
  });
245
387
  }
246
388
 
247
389
  export function useCommits(
248
- params: {
249
- limit?: number;
250
- offset?: number;
251
- partitionId?: string;
252
- instanceId?: string;
253
- } = {},
254
- options: { refetchIntervalMs?: number; enabled?: boolean } = {}
390
+ params: ListParams = {},
391
+ options: RefetchableQueryOptions = {}
255
392
  ) {
256
- const client = useApiClient();
257
- const { instanceId: selectedInstanceId } = useInstanceContext();
258
- const instanceId = params.instanceId ?? selectedInstanceId;
259
- const query = withInstanceQuery(
260
- {
393
+ const instanceId = useEffectiveInstanceId(params.instanceId);
394
+
395
+ return useConsoleJsonQuery<PaginatedResponse<ConsoleCommitListItem>>({
396
+ queryKey: queryKeys.commits({ ...params, instanceId }),
397
+ path: '/console/commits',
398
+ query: {
261
399
  limit: params.limit,
262
400
  offset: params.offset,
263
401
  partitionId: params.partitionId,
402
+ instanceId,
264
403
  },
265
- instanceId
266
- );
267
-
268
- return useQuery<PaginatedResponse<ConsoleCommitListItem>>({
269
- queryKey: queryKeys.commits({ ...params, instanceId }),
270
- queryFn: () => {
271
- if (!client) throw new Error('Not connected');
272
- return unwrap(client.GET('/console/commits', { params: { query } }));
273
- },
274
- enabled: (options.enabled ?? true) && !!client,
404
+ errorMessage: 'Failed to fetch commits',
405
+ enabled: options.enabled,
275
406
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
276
407
  });
277
408
  }
278
409
 
279
410
  export function useCommitDetail(
280
411
  seq: string | number | undefined,
281
- options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
412
+ options: EntityLookupOptions = {}
282
413
  ) {
283
- const client = useApiClient();
284
- const { config: connectionConfig } = useConnection();
285
- const { instanceId: selectedInstanceId } = useInstanceContext();
286
- const instanceId = options.instanceId ?? selectedInstanceId;
414
+ const instanceId = useEffectiveInstanceId(options.instanceId);
287
415
 
288
- return useQuery<ConsoleCommitDetail>({
416
+ return useConsoleEntityQuery<ConsoleCommitDetail>({
289
417
  queryKey: queryKeys.commitDetail(seq, options.partitionId, instanceId),
290
- queryFn: async () => {
291
- if (!client || !connectionConfig) throw new Error('Not connected');
292
- if (seq === undefined) throw new Error('Commit sequence is required');
293
- const queryString = new URLSearchParams();
294
- if (options.partitionId)
295
- queryString.set('partitionId', options.partitionId);
296
- if (instanceId) queryString.set('instanceId', instanceId);
297
- const suffix = queryString.toString();
298
- const response = await fetch(
299
- `${connectionConfig.serverUrl}/console/commits/${serializePathSegment(seq)}${suffix ? `?${suffix}` : ''}`,
300
- { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
301
- );
302
- if (!response.ok) throw new Error('Failed to fetch commit detail');
303
- return response.json();
418
+ id: seq,
419
+ requiredMessage: 'Commit sequence is required',
420
+ path: (value) => `/console/commits/${encodeURIComponent(String(value))}`,
421
+ query: {
422
+ partitionId: options.partitionId,
423
+ instanceId,
304
424
  },
305
- enabled: (options.enabled ?? true) && seq !== undefined && !!client,
425
+ errorMessage: 'Failed to fetch commit detail',
426
+ enabled: options.enabled,
306
427
  });
307
428
  }
308
429
 
309
430
  export function useTimeline(
310
- params: {
311
- limit?: number;
312
- offset?: number;
313
- partitionId?: string;
314
- instanceId?: string;
315
- view?: 'all' | 'commits' | 'events';
316
- eventType?: 'push' | 'pull';
317
- actorId?: string;
318
- clientId?: string;
319
- requestId?: string;
320
- traceId?: string;
321
- table?: string;
322
- outcome?: string;
323
- search?: string;
324
- from?: string;
325
- to?: string;
326
- } = {},
327
- options: { refetchIntervalMs?: number; enabled?: boolean } = {}
431
+ params: TimelineParams = {},
432
+ options: RefetchableQueryOptions = {}
328
433
  ) {
329
- const client = useApiClient();
330
- const { instanceId: selectedInstanceId } = useInstanceContext();
331
- const instanceId = params.instanceId ?? selectedInstanceId;
332
- const query = withInstanceQuery(
333
- {
434
+ const instanceId = useEffectiveInstanceId(params.instanceId);
435
+
436
+ return useConsoleJsonQuery<PaginatedResponse<ConsoleTimelineItem>>({
437
+ queryKey: queryKeys.timeline({ ...params, instanceId }),
438
+ path: '/console/timeline',
439
+ query: {
334
440
  limit: params.limit,
335
441
  offset: params.offset,
336
442
  partitionId: params.partitionId,
@@ -345,586 +451,322 @@ export function useTimeline(
345
451
  search: params.search,
346
452
  from: params.from,
347
453
  to: params.to,
454
+ instanceId,
348
455
  },
349
- instanceId
350
- );
351
-
352
- return useQuery<PaginatedResponse<ConsoleTimelineItem>>({
353
- queryKey: queryKeys.timeline({ ...params, instanceId }),
354
- queryFn: () => {
355
- if (!client) throw new Error('Not connected');
356
- return unwrap(client.GET('/console/timeline', { params: { query } }));
357
- },
358
- enabled: (options.enabled ?? true) && !!client,
456
+ errorMessage: 'Failed to fetch timeline',
457
+ enabled: options.enabled,
359
458
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
360
459
  });
361
460
  }
362
461
 
363
462
  export function useClients(
364
- params: {
365
- limit?: number;
366
- offset?: number;
367
- partitionId?: string;
368
- instanceId?: string;
369
- } = {},
370
- options: { refetchIntervalMs?: number; enabled?: boolean } = {}
463
+ params: ListParams = {},
464
+ options: RefetchableQueryOptions = {}
371
465
  ) {
372
- const client = useApiClient();
373
- const { instanceId: selectedInstanceId } = useInstanceContext();
374
- const instanceId = params.instanceId ?? selectedInstanceId;
375
- const query = withInstanceQuery(
376
- {
466
+ const instanceId = useEffectiveInstanceId(params.instanceId);
467
+
468
+ return useConsoleJsonQuery<PaginatedResponse<ConsoleClient>>({
469
+ queryKey: queryKeys.clients({ ...params, instanceId }),
470
+ path: '/console/clients',
471
+ query: {
377
472
  limit: params.limit,
378
473
  offset: params.offset,
379
474
  partitionId: params.partitionId,
475
+ instanceId,
380
476
  },
381
- instanceId
382
- );
383
-
384
- return useQuery<PaginatedResponse<ConsoleClient>>({
385
- queryKey: queryKeys.clients({ ...params, instanceId }),
386
- queryFn: () => {
387
- if (!client) throw new Error('Not connected');
388
- return unwrap(client.GET('/console/clients', { params: { query } }));
389
- },
390
- enabled: (options.enabled ?? true) && !!client,
477
+ errorMessage: 'Failed to fetch clients',
478
+ enabled: options.enabled,
391
479
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
392
480
  });
393
481
  }
394
482
 
395
483
  export function useRequestEventDetail(
396
484
  id: string | number | undefined,
397
- options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
485
+ options: EntityLookupOptions = {}
398
486
  ) {
399
- const client = useApiClient();
400
- const { config: connectionConfig } = useConnection();
401
- const { instanceId: selectedInstanceId } = useInstanceContext();
402
- const instanceId = options.instanceId ?? selectedInstanceId;
487
+ const instanceId = useEffectiveInstanceId(options.instanceId);
403
488
 
404
- return useQuery<ConsoleRequestEvent>({
489
+ return useConsoleEntityQuery<ConsoleRequestEvent>({
405
490
  queryKey: queryKeys.eventDetail(id, options.partitionId, instanceId),
406
- queryFn: async () => {
407
- if (!client || !connectionConfig) throw new Error('Not connected');
408
- if (id === undefined) throw new Error('Event id is required');
409
- const queryString = new URLSearchParams();
410
- if (options.partitionId)
411
- queryString.set('partitionId', options.partitionId);
412
- if (instanceId) queryString.set('instanceId', instanceId);
413
- const suffix = queryString.toString();
414
- const response = await fetch(
415
- `${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}${suffix ? `?${suffix}` : ''}`,
416
- { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
417
- );
418
- if (!response.ok) throw new Error('Failed to fetch event detail');
419
- return response.json();
491
+ id,
492
+ requiredMessage: 'Event id is required',
493
+ path: (value) => `/console/events/${encodeURIComponent(String(value))}`,
494
+ query: {
495
+ partitionId: options.partitionId,
496
+ instanceId,
420
497
  },
421
- enabled: (options.enabled ?? true) && id !== undefined && !!client,
498
+ errorMessage: 'Failed to fetch event detail',
499
+ enabled: options.enabled,
422
500
  });
423
501
  }
424
502
 
425
503
  export function useRequestEventPayload(
426
504
  id: string | number | undefined,
427
- options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
505
+ options: EntityLookupOptions = {}
428
506
  ) {
429
- const client = useApiClient();
430
- const { config: connectionConfig } = useConnection();
431
- const { instanceId: selectedInstanceId } = useInstanceContext();
432
- const instanceId = options.instanceId ?? selectedInstanceId;
507
+ const instanceId = useEffectiveInstanceId(options.instanceId);
433
508
 
434
- return useQuery<ConsoleRequestPayload>({
509
+ return useConsoleEntityQuery<ConsoleRequestPayload>({
435
510
  queryKey: queryKeys.eventPayload(id, options.partitionId, instanceId),
436
- queryFn: async () => {
437
- if (!client || !connectionConfig) throw new Error('Not connected');
438
- if (id === undefined) throw new Error('Event id is required');
439
- const queryString = new URLSearchParams();
440
- if (options.partitionId)
441
- queryString.set('partitionId', options.partitionId);
442
- if (instanceId) queryString.set('instanceId', instanceId);
443
- const suffix = queryString.toString();
444
- const response = await fetch(
445
- `${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}/payload${suffix ? `?${suffix}` : ''}`,
446
- { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
447
- );
448
- if (!response.ok) throw new Error('Failed to fetch event payload');
449
- return response.json();
511
+ id,
512
+ requiredMessage: 'Event id is required',
513
+ path: (value) =>
514
+ `/console/events/${encodeURIComponent(String(value))}/payload`,
515
+ query: {
516
+ partitionId: options.partitionId,
517
+ instanceId,
450
518
  },
451
- enabled: (options.enabled ?? true) && id !== undefined && !!client,
519
+ errorMessage: 'Failed to fetch event payload',
520
+ enabled: options.enabled,
452
521
  });
453
522
  }
454
523
 
455
524
  export function useHandlers(options: { instanceId?: string } = {}) {
456
- const client = useApiClient();
457
- const { config: connectionConfig } = useConnection();
458
- const { instanceId: selectedInstanceId } = useInstanceContext();
459
- const instanceId = options.instanceId ?? selectedInstanceId;
525
+ const instanceId = useEffectiveInstanceId(options.instanceId);
460
526
 
461
- return useQuery<{ items: ConsoleHandler[] }>({
527
+ return useConsoleJsonQuery<{ items: ConsoleHandler[] }>({
462
528
  queryKey: queryKeys.handlers(instanceId),
463
- queryFn: async () => {
464
- if (!client || !connectionConfig) throw new Error('Not connected');
465
- const queryString = new URLSearchParams();
466
- if (instanceId) queryString.set('instanceId', instanceId);
467
- const response = await fetch(
468
- buildConsoleUrl(
469
- connectionConfig.serverUrl,
470
- '/console/handlers',
471
- queryString
472
- ),
473
- {
474
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
475
- }
476
- );
477
- if (!response.ok) throw new Error('Failed to fetch handlers');
478
- return response.json();
479
- },
480
- enabled: !!client && !!connectionConfig,
529
+ path: '/console/handlers',
530
+ query: { instanceId },
531
+ errorMessage: 'Failed to fetch handlers',
481
532
  });
482
533
  }
483
534
 
484
- export function usePrunePreview(
485
- options: { enabled?: boolean; instanceId?: string } = {}
486
- ) {
487
- const client = useApiClient();
488
- const { config: connectionConfig } = useConnection();
489
- const { instanceId: selectedInstanceId } = useInstanceContext();
490
- const instanceId = options.instanceId ?? selectedInstanceId;
535
+ export function usePrunePreview(options: PrunePreviewOptions = {}) {
536
+ const instanceId = useEffectiveInstanceId(options.instanceId);
491
537
 
492
- return useQuery<{ watermarkCommitSeq: number; commitsToDelete: number }>({
538
+ return useConsoleJsonQuery<{
539
+ watermarkCommitSeq: number;
540
+ commitsToDelete: number;
541
+ }>({
493
542
  queryKey: queryKeys.prunePreview(instanceId),
494
- queryFn: async () => {
495
- if (!client || !connectionConfig) throw new Error('Not connected');
496
- const queryString = new URLSearchParams();
497
- if (instanceId) queryString.set('instanceId', instanceId);
498
- const response = await fetch(
499
- buildConsoleUrl(
500
- connectionConfig.serverUrl,
501
- '/console/prune/preview',
502
- queryString
503
- ),
504
- {
505
- method: 'POST',
506
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
507
- }
508
- );
509
- if (!response.ok) throw new Error('Failed to fetch prune preview');
510
- return response.json();
511
- },
512
- enabled: !!client && !!connectionConfig && (options.enabled ?? true),
543
+ path: '/console/prune/preview',
544
+ query: { instanceId },
545
+ method: 'POST',
546
+ errorMessage: 'Failed to fetch prune preview',
547
+ enabled: options.enabled,
513
548
  });
514
549
  }
515
550
 
516
551
  export function useOperationEvents(
517
- params: {
518
- limit?: number;
519
- offset?: number;
520
- operationType?: ConsoleOperationType;
521
- partitionId?: string;
522
- instanceId?: string;
523
- } = {},
524
- options: { enabled?: boolean; refetchIntervalMs?: number } = {}
552
+ params: OperationEventsParams = {},
553
+ options: RefetchableQueryOptions = {}
525
554
  ) {
526
- const client = useApiClient();
527
- const { instanceId: selectedInstanceId } = useInstanceContext();
528
- const instanceId = params.instanceId ?? selectedInstanceId;
529
- const query = withInstanceQuery(
530
- {
555
+ const instanceId = useEffectiveInstanceId(params.instanceId);
556
+
557
+ return useConsoleJsonQuery<PaginatedResponse<ConsoleOperationEvent>>({
558
+ queryKey: queryKeys.operations({ ...params, instanceId }),
559
+ path: '/console/operations',
560
+ query: {
531
561
  limit: params.limit,
532
562
  offset: params.offset,
533
563
  operationType: params.operationType,
534
564
  partitionId: params.partitionId,
565
+ instanceId,
535
566
  },
536
- instanceId
537
- );
538
-
539
- return useQuery<PaginatedResponse<ConsoleOperationEvent>>({
540
- queryKey: queryKeys.operations({ ...params, instanceId }),
541
- queryFn: () => {
542
- if (!client) throw new Error('Not connected');
543
- return unwrap(client.GET('/console/operations', { params: { query } }));
544
- },
545
- enabled: (options.enabled ?? true) && !!client,
567
+ errorMessage: 'Failed to fetch operations',
568
+ enabled: options.enabled,
546
569
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
547
570
  });
548
571
  }
549
572
 
550
- export function useEvictClientMutation() {
551
- const client = useApiClient();
552
- const { config: connectionConfig } = useConnection();
553
- const { instanceId: selectedInstanceId } = useInstanceContext();
554
- const queryClient = useQueryClient();
573
+ export function useClearEventsMutation() {
574
+ return useConsoleJsonMutation<{ deletedCount: number }, void>({
575
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) =>
576
+ fetchConsoleJson<{ deletedCount: number }>({
577
+ connectionConfig,
578
+ path: '/console/events',
579
+ query: { instanceId: selectedInstanceId },
580
+ method: 'DELETE',
581
+ errorMessage: 'Failed to clear events',
582
+ }),
583
+ invalidateQueryKeys: [['console', 'events']],
584
+ });
585
+ }
555
586
 
556
- return useMutation<
557
- { evicted: boolean },
558
- Error,
559
- { clientId: string; partitionId?: string; instanceId?: string }
560
- >({
561
- mutationFn: async ({ clientId, partitionId, instanceId }) => {
562
- if (!client || !connectionConfig) throw new Error('Not connected');
563
- const effectiveInstanceId = instanceId ?? selectedInstanceId;
564
- const queryString = new URLSearchParams();
565
- if (partitionId) queryString.set('partitionId', partitionId);
566
- if (effectiveInstanceId)
567
- queryString.set('instanceId', effectiveInstanceId);
568
- const response = await fetch(
569
- buildConsoleUrl(
570
- connectionConfig.serverUrl,
571
- `/console/clients/${serializePathSegment(clientId)}`,
572
- queryString
573
- ),
574
- {
575
- method: 'DELETE',
576
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
577
- }
578
- );
579
- if (!response.ok) throw new Error('Failed to evict client');
580
- return response.json();
581
- },
582
- onSuccess: () => {
583
- queryClient.invalidateQueries({ queryKey: ['console', 'clients'] });
584
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
585
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
587
+ export function useEvictClientMutation() {
588
+ return useConsoleJsonMutation<{ evicted: boolean }, EvictClientRequest>({
589
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
590
+ const effectiveInstanceId = variables.instanceId ?? selectedInstanceId;
591
+ return fetchConsoleJson<{ evicted: boolean }>({
592
+ connectionConfig,
593
+ path: `/console/clients/${encodeURIComponent(variables.clientId)}`,
594
+ query: {
595
+ partitionId: variables.partitionId,
596
+ instanceId: effectiveInstanceId,
597
+ },
598
+ method: 'DELETE',
599
+ errorMessage: 'Failed to evict client',
600
+ });
586
601
  },
602
+ invalidateQueryKeys: [
603
+ ['console', 'clients'],
604
+ ['console', 'stats'],
605
+ ['console', 'operations'],
606
+ ],
587
607
  });
588
608
  }
589
609
 
590
610
  export function usePruneMutation() {
591
- const client = useApiClient();
592
- const { config: connectionConfig } = useConnection();
593
- const { instanceId } = useInstanceContext();
594
- const queryClient = useQueryClient();
595
-
596
- return useMutation<{ deletedCommits: number }, Error, void>({
597
- mutationFn: async () => {
598
- if (!client || !connectionConfig) throw new Error('Not connected');
599
- const queryString = new URLSearchParams();
600
- if (instanceId) queryString.set('instanceId', instanceId);
601
- const response = await fetch(
602
- buildConsoleUrl(
603
- connectionConfig.serverUrl,
604
- '/console/prune',
605
- queryString
606
- ),
607
- {
608
- method: 'POST',
609
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
610
- }
611
- );
612
- if (!response.ok) throw new Error('Failed to prune');
613
- return response.json();
614
- },
615
- onSuccess: () => {
616
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
617
- queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
618
- queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
619
- queryClient.invalidateQueries({
620
- queryKey: ['console', 'prune', 'preview'],
621
- });
622
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
623
- },
611
+ return useConsoleJsonMutation<{ deletedCommits: number }, void>({
612
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) =>
613
+ fetchConsoleJson<{ deletedCommits: number }>({
614
+ connectionConfig,
615
+ path: '/console/prune',
616
+ query: { instanceId: selectedInstanceId },
617
+ method: 'POST',
618
+ errorMessage: 'Failed to prune',
619
+ }),
620
+ invalidateQueryKeys: [
621
+ ['console', 'stats'],
622
+ ['console', 'commits'],
623
+ ['console', 'timeline'],
624
+ ['console', 'prune', 'preview'],
625
+ ['console', 'operations'],
626
+ ],
624
627
  });
625
628
  }
626
629
 
627
630
  export function useCompactMutation() {
628
- const client = useApiClient();
629
- const { config: connectionConfig } = useConnection();
630
- const { instanceId } = useInstanceContext();
631
- const queryClient = useQueryClient();
632
-
633
- return useMutation<{ deletedChanges: number }, Error, void>({
634
- mutationFn: async () => {
635
- if (!client || !connectionConfig) throw new Error('Not connected');
636
- const queryString = new URLSearchParams();
637
- if (instanceId) queryString.set('instanceId', instanceId);
638
- const response = await fetch(
639
- buildConsoleUrl(
640
- connectionConfig.serverUrl,
641
- '/console/compact',
642
- queryString
643
- ),
644
- {
645
- method: 'POST',
646
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
647
- }
648
- );
649
- if (!response.ok) throw new Error('Failed to compact');
650
- return response.json();
651
- },
652
- onSuccess: () => {
653
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
654
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
655
- },
631
+ return useConsoleJsonMutation<{ deletedChanges: number }, void>({
632
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) =>
633
+ fetchConsoleJson<{ deletedChanges: number }>({
634
+ connectionConfig,
635
+ path: '/console/compact',
636
+ query: { instanceId: selectedInstanceId },
637
+ method: 'POST',
638
+ errorMessage: 'Failed to compact',
639
+ }),
640
+ invalidateQueryKeys: [
641
+ ['console', 'stats'],
642
+ ['console', 'operations'],
643
+ ],
656
644
  });
657
645
  }
658
646
 
659
647
  export function useNotifyDataChangeMutation() {
660
- const client = useApiClient();
661
- const { config: connectionConfig } = useConnection();
662
- const { instanceId: selectedInstanceId } = useInstanceContext();
663
- const queryClient = useQueryClient();
664
-
665
- return useMutation<
648
+ return useConsoleJsonMutation<
666
649
  ConsoleNotifyDataChangeResponse,
667
- Error,
668
- { tables: string[]; partitionId?: string; instanceId?: string }
650
+ NotifyDataChangeRequest
669
651
  >({
670
- mutationFn: async (request) => {
671
- if (!client || !connectionConfig) throw new Error('Not connected');
672
- const effectiveInstanceId = request.instanceId ?? selectedInstanceId;
673
- const queryString = new URLSearchParams();
674
- if (effectiveInstanceId)
675
- queryString.set('instanceId', effectiveInstanceId);
676
- const response = await fetch(
677
- buildConsoleUrl(
678
- connectionConfig.serverUrl,
679
- '/console/notify-data-change',
680
- queryString
681
- ),
682
- {
683
- method: 'POST',
684
- headers: {
685
- Authorization: `Bearer ${connectionConfig.token}`,
686
- 'Content-Type': 'application/json',
687
- },
688
- body: JSON.stringify({
689
- tables: request.tables,
690
- partitionId: request.partitionId,
691
- }),
692
- }
693
- );
694
- if (!response.ok) throw new Error('Failed to notify data change');
695
- return response.json();
696
- },
697
- onSuccess: () => {
698
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
699
- queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
700
- queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
701
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
652
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
653
+ const effectiveInstanceId = variables.instanceId ?? selectedInstanceId;
654
+ return fetchConsoleJson<ConsoleNotifyDataChangeResponse>({
655
+ connectionConfig,
656
+ path: '/console/notify-data-change',
657
+ query: { instanceId: effectiveInstanceId },
658
+ method: 'POST',
659
+ body: {
660
+ tables: variables.tables,
661
+ partitionId: variables.partitionId,
662
+ },
663
+ errorMessage: 'Failed to notify data change',
664
+ });
702
665
  },
666
+ invalidateQueryKeys: [
667
+ ['console', 'stats'],
668
+ ['console', 'commits'],
669
+ ['console', 'timeline'],
670
+ ['console', 'operations'],
671
+ ],
703
672
  });
704
673
  }
705
674
 
706
- export function useApiKeys(
707
- params: {
708
- limit?: number;
709
- offset?: number;
710
- type?: 'relay' | 'proxy' | 'admin';
711
- status?: 'active' | 'revoked' | 'expiring';
712
- expiresWithinDays?: number;
713
- instanceId?: string;
714
- } = {}
715
- ) {
716
- const client = useApiClient();
717
- const { config: connectionConfig } = useConnection();
718
- const { instanceId: selectedInstanceId } = useInstanceContext();
719
- const instanceId = params.instanceId ?? selectedInstanceId;
675
+ export function useApiKeys(params: ApiKeysParams = {}) {
676
+ const instanceId = useEffectiveInstanceId(params.instanceId);
720
677
 
721
- return useQuery<PaginatedResponse<ConsoleApiKey>>({
678
+ return useConsoleJsonQuery<PaginatedResponse<ConsoleApiKey>>({
722
679
  queryKey: queryKeys.apiKeys({ ...params, instanceId }),
723
- queryFn: async () => {
724
- if (!client || !connectionConfig) throw new Error('Not connected');
725
- const queryString = new URLSearchParams();
726
- if (params.limit !== undefined)
727
- queryString.set('limit', String(params.limit));
728
- if (params.offset !== undefined)
729
- queryString.set('offset', String(params.offset));
730
- if (params.type) queryString.set('type', params.type);
731
- if (params.status) queryString.set('status', params.status);
732
- if (params.expiresWithinDays !== undefined) {
733
- queryString.set('expiresWithinDays', String(params.expiresWithinDays));
734
- }
735
- if (instanceId) queryString.set('instanceId', instanceId);
736
-
737
- const response = await fetch(
738
- buildConsoleUrl(
739
- connectionConfig.serverUrl,
740
- '/console/api-keys',
741
- queryString
742
- ),
743
- {
744
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
745
- }
746
- );
747
- if (!response.ok) throw new Error('Failed to fetch API keys');
748
- return response.json();
680
+ path: '/console/api-keys',
681
+ query: {
682
+ limit: params.limit,
683
+ offset: params.offset,
684
+ type: params.type,
685
+ status: params.status,
686
+ expiresWithinDays: params.expiresWithinDays,
687
+ instanceId,
749
688
  },
750
- enabled: !!client && !!connectionConfig,
689
+ errorMessage: 'Failed to fetch API keys',
751
690
  });
752
691
  }
753
692
 
754
693
  export function useCreateApiKeyMutation() {
755
- const client = useApiClient();
756
- const { config: connectionConfig } = useConnection();
757
- const { instanceId } = useInstanceContext();
758
- const queryClient = useQueryClient();
759
-
760
- return useMutation<
761
- { key: ConsoleApiKey; secretKey: string },
762
- Error,
763
- {
764
- name: string;
765
- keyType: 'relay' | 'proxy' | 'admin';
766
- scopeKeys?: string[];
767
- actorId?: string;
768
- expiresInDays?: number;
769
- }
770
- >({
771
- mutationFn: async (request) => {
772
- if (!client || !connectionConfig) throw new Error('Not connected');
773
- const queryString = new URLSearchParams();
774
- if (instanceId) queryString.set('instanceId', instanceId);
775
- const response = await fetch(
776
- buildConsoleUrl(
777
- connectionConfig.serverUrl,
778
- '/console/api-keys',
779
- queryString
780
- ),
781
- {
782
- method: 'POST',
783
- headers: {
784
- Authorization: `Bearer ${connectionConfig.token}`,
785
- 'Content-Type': 'application/json',
786
- },
787
- body: JSON.stringify(request),
788
- }
789
- );
790
- if (!response.ok) throw new Error('Failed to create API key');
791
- return response.json();
792
- },
793
- onSuccess: () => {
794
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
694
+ return useConsoleJsonMutation<ApiKeySecretResponse, CreateApiKeyRequest>({
695
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
696
+ return fetchConsoleJson<ApiKeySecretResponse>({
697
+ connectionConfig,
698
+ path: '/console/api-keys',
699
+ query: { instanceId: selectedInstanceId },
700
+ method: 'POST',
701
+ body: variables,
702
+ errorMessage: 'Failed to create API key',
703
+ });
795
704
  },
705
+ invalidateQueryKeys: [['console', 'api-keys']],
796
706
  });
797
707
  }
798
708
 
799
709
  export function useRevokeApiKeyMutation() {
800
- const client = useApiClient();
801
- const { config: connectionConfig } = useConnection();
802
- const { instanceId } = useInstanceContext();
803
- const queryClient = useQueryClient();
804
-
805
- return useMutation<{ revoked: boolean }, Error, string>({
806
- mutationFn: async (keyId) => {
807
- if (!client || !connectionConfig) throw new Error('Not connected');
808
- const queryString = new URLSearchParams();
809
- if (instanceId) queryString.set('instanceId', instanceId);
810
- const response = await fetch(
811
- buildConsoleUrl(
812
- connectionConfig.serverUrl,
813
- `/console/api-keys/${serializePathSegment(keyId)}`,
814
- queryString
815
- ),
816
- {
817
- method: 'DELETE',
818
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
819
- }
820
- );
821
- if (!response.ok) throw new Error('Failed to revoke API key');
822
- return response.json();
823
- },
824
- onSuccess: () => {
825
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
710
+ return useConsoleJsonMutation<{ revoked: boolean }, string>({
711
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
712
+ return fetchConsoleJson<{ revoked: boolean }>({
713
+ connectionConfig,
714
+ path: `/console/api-keys/${encodeURIComponent(variables)}`,
715
+ query: { instanceId: selectedInstanceId },
716
+ method: 'DELETE',
717
+ errorMessage: 'Failed to revoke API key',
718
+ });
826
719
  },
720
+ invalidateQueryKeys: [['console', 'api-keys']],
827
721
  });
828
722
  }
829
723
 
830
724
  export function useBulkRevokeApiKeysMutation() {
831
- const client = useApiClient();
832
- const { config: connectionConfig } = useConnection();
833
- const { instanceId } = useInstanceContext();
834
- const queryClient = useQueryClient();
835
-
836
- return useMutation<
725
+ return useConsoleJsonMutation<
837
726
  ConsoleApiKeyBulkRevokeResponse,
838
- Error,
839
727
  { keyIds: string[] }
840
728
  >({
841
- mutationFn: async (request) => {
842
- if (!client || !connectionConfig) throw new Error('Not connected');
843
- const queryString = new URLSearchParams();
844
- if (instanceId) queryString.set('instanceId', instanceId);
845
- const response = await fetch(
846
- buildConsoleUrl(
847
- connectionConfig.serverUrl,
848
- '/console/api-keys/bulk-revoke',
849
- queryString
850
- ),
851
- {
852
- method: 'POST',
853
- headers: {
854
- Authorization: `Bearer ${connectionConfig.token}`,
855
- 'Content-Type': 'application/json',
856
- },
857
- body: JSON.stringify(request),
858
- }
859
- );
860
- if (!response.ok) throw new Error('Failed to bulk revoke API keys');
861
- return response.json();
862
- },
863
- onSuccess: () => {
864
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
729
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
730
+ return fetchConsoleJson<ConsoleApiKeyBulkRevokeResponse>({
731
+ connectionConfig,
732
+ path: '/console/api-keys/bulk-revoke',
733
+ query: { instanceId: selectedInstanceId },
734
+ method: 'POST',
735
+ body: variables,
736
+ errorMessage: 'Failed to bulk revoke API keys',
737
+ });
865
738
  },
739
+ invalidateQueryKeys: [['console', 'api-keys']],
866
740
  });
867
741
  }
868
742
 
869
743
  export function useRotateApiKeyMutation() {
870
- const client = useApiClient();
871
- const { config: connectionConfig } = useConnection();
872
- const { instanceId } = useInstanceContext();
873
- const queryClient = useQueryClient();
874
-
875
- return useMutation<{ key: ConsoleApiKey; secretKey: string }, Error, string>({
876
- mutationFn: async (keyId) => {
877
- if (!client || !connectionConfig) throw new Error('Not connected');
878
- const queryString = new URLSearchParams();
879
- if (instanceId) queryString.set('instanceId', instanceId);
880
- const response = await fetch(
881
- buildConsoleUrl(
882
- connectionConfig.serverUrl,
883
- `/console/api-keys/${serializePathSegment(keyId)}/rotate`,
884
- queryString
885
- ),
886
- {
887
- method: 'POST',
888
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
889
- }
890
- );
891
- if (!response.ok) throw new Error('Failed to rotate API key');
892
- return response.json();
893
- },
894
- onSuccess: () => {
895
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
744
+ return useConsoleJsonMutation<ApiKeySecretResponse, string>({
745
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
746
+ return fetchConsoleJson<ApiKeySecretResponse>({
747
+ connectionConfig,
748
+ path: `/console/api-keys/${encodeURIComponent(variables)}/rotate`,
749
+ query: { instanceId: selectedInstanceId },
750
+ method: 'POST',
751
+ errorMessage: 'Failed to rotate API key',
752
+ });
896
753
  },
754
+ invalidateQueryKeys: [['console', 'api-keys']],
897
755
  });
898
756
  }
899
757
 
900
758
  export function useStageRotateApiKeyMutation() {
901
- const client = useApiClient();
902
- const { config: connectionConfig } = useConnection();
903
- const { instanceId } = useInstanceContext();
904
- const queryClient = useQueryClient();
905
-
906
- return useMutation<{ key: ConsoleApiKey; secretKey: string }, Error, string>({
907
- mutationFn: async (keyId) => {
908
- if (!client || !connectionConfig) throw new Error('Not connected');
909
- const queryString = new URLSearchParams();
910
- if (instanceId) queryString.set('instanceId', instanceId);
911
- const response = await fetch(
912
- buildConsoleUrl(
913
- connectionConfig.serverUrl,
914
- `/console/api-keys/${serializePathSegment(keyId)}/rotate/stage`,
915
- queryString
916
- ),
917
- {
918
- method: 'POST',
919
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
920
- }
921
- );
922
- if (!response.ok) throw new Error('Failed to stage-rotate API key');
923
- return response.json();
924
- },
925
- onSuccess: () => {
926
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
759
+ return useConsoleJsonMutation<ApiKeySecretResponse, string>({
760
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
761
+ return fetchConsoleJson<ApiKeySecretResponse>({
762
+ connectionConfig,
763
+ path: `/console/api-keys/${encodeURIComponent(variables)}/rotate/stage`,
764
+ query: { instanceId: selectedInstanceId },
765
+ method: 'POST',
766
+ errorMessage: 'Failed to stage-rotate API key',
767
+ });
927
768
  },
769
+ invalidateQueryKeys: [['console', 'api-keys']],
928
770
  });
929
771
  }
930
772
 
@@ -932,91 +774,45 @@ export function useStageRotateApiKeyMutation() {
932
774
  // Blob storage hooks
933
775
  // ---------------------------------------------------------------------------
934
776
 
935
- export function useBlobs(
936
- options: {
937
- prefix?: string;
938
- cursor?: string;
939
- limit?: number;
940
- refetchIntervalMs?: number;
941
- } = {}
942
- ) {
943
- const { config: connectionConfig } = useConnection();
944
- return useQuery<ConsoleBlobListResponse>({
777
+ export function useBlobs(options: BlobsOptions = {}) {
778
+ return useConsoleJsonQuery<ConsoleBlobListResponse>({
945
779
  queryKey: queryKeys.storage({
946
780
  prefix: options.prefix,
947
781
  cursor: options.cursor,
948
782
  limit: options.limit,
949
783
  }),
950
- queryFn: async () => {
951
- if (!connectionConfig) throw new Error('Not connected');
952
- const queryString = new URLSearchParams();
953
- if (options.prefix) queryString.set('prefix', options.prefix);
954
- if (options.cursor) queryString.set('cursor', options.cursor);
955
- if (options.limit) queryString.set('limit', String(options.limit));
956
- const response = await fetch(
957
- buildConsoleUrl(
958
- connectionConfig.serverUrl,
959
- '/console/storage',
960
- queryString
961
- ),
962
- {
963
- method: 'GET',
964
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
965
- }
966
- );
967
- if (!response.ok) throw new Error('Failed to list blobs');
968
- return response.json();
784
+ path: '/console/storage',
785
+ query: {
786
+ prefix: options.prefix,
787
+ cursor: options.cursor,
788
+ limit: options.limit,
969
789
  },
970
- enabled: !!connectionConfig,
790
+ errorMessage: 'Failed to list blobs',
971
791
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
972
792
  });
973
793
  }
974
794
 
975
795
  export function useDeleteBlobMutation() {
976
- const { config: connectionConfig } = useConnection();
977
- const queryClient = useQueryClient();
978
- return useMutation<{ deleted: boolean }, Error, string>({
979
- mutationFn: async (key: string) => {
980
- if (!connectionConfig) throw new Error('Not connected');
981
- const encodedKey = encodeURIComponent(key);
982
- const response = await fetch(
983
- buildConsoleUrl(
984
- connectionConfig.serverUrl,
985
- `/console/storage/${encodedKey}`,
986
- new URLSearchParams()
987
- ),
988
- {
989
- method: 'DELETE',
990
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
991
- }
992
- );
993
- if (!response.ok) throw new Error('Failed to delete blob');
994
- return response.json();
995
- },
996
- onSuccess: () => {
997
- queryClient.invalidateQueries({ queryKey: ['console', 'storage'] });
998
- },
796
+ return useConsoleJsonMutation<{ deleted: boolean }, string>({
797
+ mutationFn: async ({ connectionConfig, variables }) =>
798
+ fetchConsoleJson<{ deleted: boolean }>({
799
+ connectionConfig,
800
+ path: `/console/storage/${encodeURIComponent(variables)}`,
801
+ method: 'DELETE',
802
+ errorMessage: 'Failed to delete blob',
803
+ }),
804
+ invalidateQueryKeys: [['console', 'storage']],
999
805
  });
1000
806
  }
1001
807
 
1002
808
  export function useBlobDownload() {
1003
- const { config: connectionConfig } = useConnection();
809
+ const { config: connectionConfig, isConnected } = useConnection();
1004
810
  return async (key: string) => {
1005
- if (!connectionConfig) throw new Error('Not connected');
1006
- const encodedKey = encodeURIComponent(key);
1007
- const response = await fetch(
1008
- buildConsoleUrl(
1009
- connectionConfig.serverUrl,
1010
- `/console/storage/${encodedKey}/download`,
1011
- new URLSearchParams()
1012
- ),
1013
- {
1014
- method: 'GET',
1015
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
1016
- }
1017
- );
1018
- if (!response.ok) throw new Error('Failed to download blob');
1019
- const blob = await response.blob();
811
+ const blob = await fetchConsoleBlob({
812
+ connectionConfig: requireConnection(connectionConfig, isConnected),
813
+ path: `/console/storage/${encodeURIComponent(key)}/download`,
814
+ errorMessage: 'Failed to download blob',
815
+ });
1020
816
  const url = URL.createObjectURL(blob);
1021
817
  const a = document.createElement('a');
1022
818
  a.href = url;