@syncular/console 0.0.6-158 → 0.0.6-165

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.
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * React Query hooks for Console API
3
3
  */
4
- import { unwrap } from '@syncular/transport-http';
5
4
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
- import { useApiClient, useConnection } from './ConnectionContext.js';
5
+ import { useConnection } from './ConnectionContext.js';
7
6
  import { useInstanceContext } from './useInstanceContext.js';
8
7
  const queryKeys = {
9
8
  stats: (params) => ['console', 'stats', params],
@@ -26,668 +25,517 @@ function resolveRefetchInterval(refreshIntervalMs, defaultValueMs) {
26
25
  return false;
27
26
  return refreshIntervalMs ?? defaultValueMs;
28
27
  }
29
- function withInstanceQuery(query, instanceId) {
30
- if (!instanceId)
31
- return query;
32
- return { ...query, instanceId };
28
+ function buildQueryString(query) {
29
+ const params = new URLSearchParams();
30
+ if (!query)
31
+ return params;
32
+ for (const [key, value] of Object.entries(query)) {
33
+ if (value === undefined || value === null)
34
+ continue;
35
+ params.set(key, String(value));
36
+ }
37
+ return params;
33
38
  }
34
- function serializePathSegment(value) {
35
- return encodeURIComponent(String(value));
36
- }
37
- function buildConsoleUrl(serverUrl, path, queryString) {
39
+ function buildConsoleUrl(serverUrl, path, query) {
38
40
  const baseUrl = serverUrl.endsWith('/') ? serverUrl.slice(0, -1) : serverUrl;
41
+ const queryString = buildQueryString(query);
39
42
  const suffix = queryString?.toString();
40
43
  return `${baseUrl}${path}${suffix ? `?${suffix}` : ''}`;
41
44
  }
42
- export function useStats(options = {}) {
43
- const client = useApiClient();
44
- const { instanceId: selectedInstanceId } = useInstanceContext();
45
- const instanceId = options.instanceId ?? selectedInstanceId;
46
- const query = withInstanceQuery(options.partitionId ? { partitionId: options.partitionId } : {}, instanceId);
45
+ function requireConnection(connectionConfig, isConnected) {
46
+ if (!isConnected || !connectionConfig)
47
+ throw new Error('Not connected');
48
+ return connectionConfig;
49
+ }
50
+ async function fetchConsoleJson(args) {
51
+ const headers = {
52
+ Authorization: `Bearer ${args.connectionConfig.token}`,
53
+ };
54
+ if (args.body !== undefined) {
55
+ headers['Content-Type'] = 'application/json';
56
+ }
57
+ const response = await fetch(buildConsoleUrl(args.connectionConfig.serverUrl, args.path, args.query), {
58
+ method: args.method ?? 'GET',
59
+ headers,
60
+ body: args.body === undefined ? undefined : JSON.stringify(args.body),
61
+ });
62
+ if (!response.ok)
63
+ throw new Error(args.errorMessage);
64
+ return response.json();
65
+ }
66
+ async function fetchConsoleBlob(args) {
67
+ const response = await fetch(buildConsoleUrl(args.connectionConfig.serverUrl, args.path, args.query), {
68
+ method: 'GET',
69
+ headers: { Authorization: `Bearer ${args.connectionConfig.token}` },
70
+ });
71
+ if (!response.ok)
72
+ throw new Error(args.errorMessage);
73
+ return response.blob();
74
+ }
75
+ function useConsoleJsonQuery(options) {
76
+ const { config: connectionConfig, isConnected } = useConnection();
47
77
  return useQuery({
48
- queryKey: queryKeys.stats({
49
- partitionId: options.partitionId,
50
- instanceId,
78
+ queryKey: options.queryKey,
79
+ queryFn: () => fetchConsoleJson({
80
+ connectionConfig: requireConnection(connectionConfig, isConnected),
81
+ path: options.path,
82
+ query: options.query,
83
+ method: options.method,
84
+ errorMessage: options.errorMessage,
51
85
  }),
86
+ enabled: (options.enabled ?? true) && isConnected && !!connectionConfig,
87
+ refetchInterval: options.refetchInterval,
88
+ });
89
+ }
90
+ function useConsoleEntityQuery(options) {
91
+ const { config: connectionConfig, isConnected } = useConnection();
92
+ return useQuery({
93
+ queryKey: options.queryKey,
52
94
  queryFn: () => {
53
- if (!client)
54
- throw new Error('Not connected');
55
- return unwrap(client.GET('/console/stats', { params: { query } }));
95
+ if (options.id === undefined)
96
+ throw new Error(options.requiredMessage);
97
+ return fetchConsoleJson({
98
+ connectionConfig: requireConnection(connectionConfig, isConnected),
99
+ path: options.path(options.id),
100
+ query: options.query,
101
+ errorMessage: options.errorMessage,
102
+ });
56
103
  },
57
- enabled: !!client,
104
+ enabled: (options.enabled ?? true) &&
105
+ options.id !== undefined &&
106
+ isConnected &&
107
+ !!connectionConfig,
108
+ });
109
+ }
110
+ function invalidateConsoleQueries(queryClient, queryKeysToInvalidate) {
111
+ for (const queryKey of queryKeysToInvalidate) {
112
+ queryClient.invalidateQueries({ queryKey });
113
+ }
114
+ }
115
+ function useEffectiveInstanceId(instanceId) {
116
+ const { instanceId: selectedInstanceId } = useInstanceContext();
117
+ return instanceId ?? selectedInstanceId;
118
+ }
119
+ function useConsoleJsonMutation(options) {
120
+ const { config: connectionConfig, isConnected } = useConnection();
121
+ const { instanceId: selectedInstanceId } = useInstanceContext();
122
+ const queryClient = useQueryClient();
123
+ return useMutation({
124
+ mutationFn: (variables) => options.mutationFn({
125
+ connectionConfig: requireConnection(connectionConfig, isConnected),
126
+ variables,
127
+ selectedInstanceId,
128
+ }),
129
+ onSuccess: () => {
130
+ if (!options.invalidateQueryKeys)
131
+ return;
132
+ invalidateConsoleQueries(queryClient, options.invalidateQueryKeys);
133
+ },
134
+ });
135
+ }
136
+ export function useStats(options = {}) {
137
+ const instanceId = useEffectiveInstanceId(options.instanceId);
138
+ return useConsoleJsonQuery({
139
+ queryKey: queryKeys.stats({ partitionId: options.partitionId, instanceId }),
140
+ path: '/console/stats',
141
+ query: {
142
+ partitionId: options.partitionId,
143
+ instanceId,
144
+ },
145
+ errorMessage: 'Failed to fetch stats',
58
146
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 5000),
59
147
  });
60
148
  }
61
149
  export function useTimeseriesStats(params = {}, options = {}) {
62
- const client = useApiClient();
63
- const { config: connectionConfig } = useConnection();
64
- const { instanceId: selectedInstanceId } = useInstanceContext();
65
- const instanceId = params.instanceId ?? selectedInstanceId;
66
- return useQuery({
150
+ const instanceId = useEffectiveInstanceId(params.instanceId);
151
+ return useConsoleJsonQuery({
67
152
  queryKey: queryKeys.timeseries({ ...params, instanceId }),
68
- queryFn: async () => {
69
- if (!client || !connectionConfig)
70
- throw new Error('Not connected');
71
- // Use fetch directly since this endpoint may not be in OpenAPI yet
72
- const queryString = new URLSearchParams();
73
- if (params.interval)
74
- queryString.set('interval', params.interval);
75
- if (params.range)
76
- queryString.set('range', params.range);
77
- if (params.partitionId)
78
- queryString.set('partitionId', params.partitionId);
79
- if (instanceId)
80
- queryString.set('instanceId', instanceId);
81
- const response = await fetch(`${connectionConfig.serverUrl}/console/stats/timeseries?${queryString}`, { headers: { Authorization: `Bearer ${connectionConfig.token}` } });
82
- if (!response.ok)
83
- throw new Error('Failed to fetch timeseries stats');
84
- return response.json();
153
+ path: '/console/stats/timeseries',
154
+ query: {
155
+ interval: params.interval,
156
+ range: params.range,
157
+ partitionId: params.partitionId,
158
+ instanceId,
85
159
  },
86
- enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
160
+ errorMessage: 'Failed to fetch timeseries stats',
161
+ enabled: options.enabled,
87
162
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
88
163
  });
89
164
  }
90
165
  export function useLatencyStats(params = {}, options = {}) {
91
- const client = useApiClient();
92
- const { config: connectionConfig } = useConnection();
93
- const { instanceId: selectedInstanceId } = useInstanceContext();
94
- const instanceId = params.instanceId ?? selectedInstanceId;
95
- return useQuery({
166
+ const instanceId = useEffectiveInstanceId(params.instanceId);
167
+ return useConsoleJsonQuery({
96
168
  queryKey: queryKeys.latency({ ...params, instanceId }),
97
- queryFn: async () => {
98
- if (!client || !connectionConfig)
99
- throw new Error('Not connected');
100
- // Use fetch directly since this endpoint may not be in OpenAPI yet
101
- const queryString = new URLSearchParams();
102
- if (params.range)
103
- queryString.set('range', params.range);
104
- if (params.partitionId)
105
- queryString.set('partitionId', params.partitionId);
106
- if (instanceId)
107
- queryString.set('instanceId', instanceId);
108
- const response = await fetch(`${connectionConfig.serverUrl}/console/stats/latency?${queryString}`, { headers: { Authorization: `Bearer ${connectionConfig.token}` } });
109
- if (!response.ok)
110
- throw new Error('Failed to fetch latency stats');
111
- return response.json();
169
+ path: '/console/stats/latency',
170
+ query: {
171
+ range: params.range,
172
+ partitionId: params.partitionId,
173
+ instanceId,
112
174
  },
113
- enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
175
+ errorMessage: 'Failed to fetch latency stats',
176
+ enabled: options.enabled,
114
177
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
115
178
  });
116
179
  }
117
180
  export function useCommits(params = {}, options = {}) {
118
- const client = useApiClient();
119
- const { instanceId: selectedInstanceId } = useInstanceContext();
120
- const instanceId = params.instanceId ?? selectedInstanceId;
121
- const query = withInstanceQuery({
122
- limit: params.limit,
123
- offset: params.offset,
124
- partitionId: params.partitionId,
125
- }, instanceId);
126
- return useQuery({
181
+ const instanceId = useEffectiveInstanceId(params.instanceId);
182
+ return useConsoleJsonQuery({
127
183
  queryKey: queryKeys.commits({ ...params, instanceId }),
128
- queryFn: () => {
129
- if (!client)
130
- throw new Error('Not connected');
131
- return unwrap(client.GET('/console/commits', { params: { query } }));
184
+ path: '/console/commits',
185
+ query: {
186
+ limit: params.limit,
187
+ offset: params.offset,
188
+ partitionId: params.partitionId,
189
+ instanceId,
132
190
  },
133
- enabled: (options.enabled ?? true) && !!client,
191
+ errorMessage: 'Failed to fetch commits',
192
+ enabled: options.enabled,
134
193
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
135
194
  });
136
195
  }
137
196
  export function useCommitDetail(seq, options = {}) {
138
- const client = useApiClient();
139
- const { config: connectionConfig } = useConnection();
140
- const { instanceId: selectedInstanceId } = useInstanceContext();
141
- const instanceId = options.instanceId ?? selectedInstanceId;
142
- return useQuery({
197
+ const instanceId = useEffectiveInstanceId(options.instanceId);
198
+ return useConsoleEntityQuery({
143
199
  queryKey: queryKeys.commitDetail(seq, options.partitionId, instanceId),
144
- queryFn: async () => {
145
- if (!client || !connectionConfig)
146
- throw new Error('Not connected');
147
- if (seq === undefined)
148
- throw new Error('Commit sequence is required');
149
- const queryString = new URLSearchParams();
150
- if (options.partitionId)
151
- queryString.set('partitionId', options.partitionId);
152
- if (instanceId)
153
- queryString.set('instanceId', instanceId);
154
- const suffix = queryString.toString();
155
- const response = await fetch(`${connectionConfig.serverUrl}/console/commits/${serializePathSegment(seq)}${suffix ? `?${suffix}` : ''}`, { headers: { Authorization: `Bearer ${connectionConfig.token}` } });
156
- if (!response.ok)
157
- throw new Error('Failed to fetch commit detail');
158
- return response.json();
200
+ id: seq,
201
+ requiredMessage: 'Commit sequence is required',
202
+ path: (value) => `/console/commits/${encodeURIComponent(String(value))}`,
203
+ query: {
204
+ partitionId: options.partitionId,
205
+ instanceId,
159
206
  },
160
- enabled: (options.enabled ?? true) && seq !== undefined && !!client,
207
+ errorMessage: 'Failed to fetch commit detail',
208
+ enabled: options.enabled,
161
209
  });
162
210
  }
163
211
  export function useTimeline(params = {}, options = {}) {
164
- const client = useApiClient();
165
- const { instanceId: selectedInstanceId } = useInstanceContext();
166
- const instanceId = params.instanceId ?? selectedInstanceId;
167
- const query = withInstanceQuery({
168
- limit: params.limit,
169
- offset: params.offset,
170
- partitionId: params.partitionId,
171
- view: params.view,
172
- eventType: params.eventType,
173
- actorId: params.actorId,
174
- clientId: params.clientId,
175
- requestId: params.requestId,
176
- traceId: params.traceId,
177
- table: params.table,
178
- outcome: params.outcome,
179
- search: params.search,
180
- from: params.from,
181
- to: params.to,
182
- }, instanceId);
183
- return useQuery({
212
+ const instanceId = useEffectiveInstanceId(params.instanceId);
213
+ return useConsoleJsonQuery({
184
214
  queryKey: queryKeys.timeline({ ...params, instanceId }),
185
- queryFn: () => {
186
- if (!client)
187
- throw new Error('Not connected');
188
- return unwrap(client.GET('/console/timeline', { params: { query } }));
215
+ path: '/console/timeline',
216
+ query: {
217
+ limit: params.limit,
218
+ offset: params.offset,
219
+ partitionId: params.partitionId,
220
+ view: params.view,
221
+ eventType: params.eventType,
222
+ actorId: params.actorId,
223
+ clientId: params.clientId,
224
+ requestId: params.requestId,
225
+ traceId: params.traceId,
226
+ table: params.table,
227
+ outcome: params.outcome,
228
+ search: params.search,
229
+ from: params.from,
230
+ to: params.to,
231
+ instanceId,
189
232
  },
190
- enabled: (options.enabled ?? true) && !!client,
233
+ errorMessage: 'Failed to fetch timeline',
234
+ enabled: options.enabled,
191
235
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
192
236
  });
193
237
  }
194
238
  export function useClients(params = {}, options = {}) {
195
- const client = useApiClient();
196
- const { instanceId: selectedInstanceId } = useInstanceContext();
197
- const instanceId = params.instanceId ?? selectedInstanceId;
198
- const query = withInstanceQuery({
199
- limit: params.limit,
200
- offset: params.offset,
201
- partitionId: params.partitionId,
202
- }, instanceId);
203
- return useQuery({
239
+ const instanceId = useEffectiveInstanceId(params.instanceId);
240
+ return useConsoleJsonQuery({
204
241
  queryKey: queryKeys.clients({ ...params, instanceId }),
205
- queryFn: () => {
206
- if (!client)
207
- throw new Error('Not connected');
208
- return unwrap(client.GET('/console/clients', { params: { query } }));
242
+ path: '/console/clients',
243
+ query: {
244
+ limit: params.limit,
245
+ offset: params.offset,
246
+ partitionId: params.partitionId,
247
+ instanceId,
209
248
  },
210
- enabled: (options.enabled ?? true) && !!client,
249
+ errorMessage: 'Failed to fetch clients',
250
+ enabled: options.enabled,
211
251
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
212
252
  });
213
253
  }
214
254
  export function useRequestEventDetail(id, options = {}) {
215
- const client = useApiClient();
216
- const { config: connectionConfig } = useConnection();
217
- const { instanceId: selectedInstanceId } = useInstanceContext();
218
- const instanceId = options.instanceId ?? selectedInstanceId;
219
- return useQuery({
255
+ const instanceId = useEffectiveInstanceId(options.instanceId);
256
+ return useConsoleEntityQuery({
220
257
  queryKey: queryKeys.eventDetail(id, options.partitionId, instanceId),
221
- queryFn: async () => {
222
- if (!client || !connectionConfig)
223
- throw new Error('Not connected');
224
- if (id === undefined)
225
- throw new Error('Event id is required');
226
- const queryString = new URLSearchParams();
227
- if (options.partitionId)
228
- queryString.set('partitionId', options.partitionId);
229
- if (instanceId)
230
- queryString.set('instanceId', instanceId);
231
- const suffix = queryString.toString();
232
- const response = await fetch(`${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}${suffix ? `?${suffix}` : ''}`, { headers: { Authorization: `Bearer ${connectionConfig.token}` } });
233
- if (!response.ok)
234
- throw new Error('Failed to fetch event detail');
235
- return response.json();
258
+ id,
259
+ requiredMessage: 'Event id is required',
260
+ path: (value) => `/console/events/${encodeURIComponent(String(value))}`,
261
+ query: {
262
+ partitionId: options.partitionId,
263
+ instanceId,
236
264
  },
237
- enabled: (options.enabled ?? true) && id !== undefined && !!client,
265
+ errorMessage: 'Failed to fetch event detail',
266
+ enabled: options.enabled,
238
267
  });
239
268
  }
240
269
  export function useRequestEventPayload(id, options = {}) {
241
- const client = useApiClient();
242
- const { config: connectionConfig } = useConnection();
243
- const { instanceId: selectedInstanceId } = useInstanceContext();
244
- const instanceId = options.instanceId ?? selectedInstanceId;
245
- return useQuery({
270
+ const instanceId = useEffectiveInstanceId(options.instanceId);
271
+ return useConsoleEntityQuery({
246
272
  queryKey: queryKeys.eventPayload(id, options.partitionId, instanceId),
247
- queryFn: async () => {
248
- if (!client || !connectionConfig)
249
- throw new Error('Not connected');
250
- if (id === undefined)
251
- throw new Error('Event id is required');
252
- const queryString = new URLSearchParams();
253
- if (options.partitionId)
254
- queryString.set('partitionId', options.partitionId);
255
- if (instanceId)
256
- queryString.set('instanceId', instanceId);
257
- const suffix = queryString.toString();
258
- const response = await fetch(`${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}/payload${suffix ? `?${suffix}` : ''}`, { headers: { Authorization: `Bearer ${connectionConfig.token}` } });
259
- if (!response.ok)
260
- throw new Error('Failed to fetch event payload');
261
- return response.json();
273
+ id,
274
+ requiredMessage: 'Event id is required',
275
+ path: (value) => `/console/events/${encodeURIComponent(String(value))}/payload`,
276
+ query: {
277
+ partitionId: options.partitionId,
278
+ instanceId,
262
279
  },
263
- enabled: (options.enabled ?? true) && id !== undefined && !!client,
280
+ errorMessage: 'Failed to fetch event payload',
281
+ enabled: options.enabled,
264
282
  });
265
283
  }
266
284
  export function useHandlers(options = {}) {
267
- const client = useApiClient();
268
- const { config: connectionConfig } = useConnection();
269
- const { instanceId: selectedInstanceId } = useInstanceContext();
270
- const instanceId = options.instanceId ?? selectedInstanceId;
271
- return useQuery({
285
+ const instanceId = useEffectiveInstanceId(options.instanceId);
286
+ return useConsoleJsonQuery({
272
287
  queryKey: queryKeys.handlers(instanceId),
273
- queryFn: async () => {
274
- if (!client || !connectionConfig)
275
- throw new Error('Not connected');
276
- const queryString = new URLSearchParams();
277
- if (instanceId)
278
- queryString.set('instanceId', instanceId);
279
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/handlers', queryString), {
280
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
281
- });
282
- if (!response.ok)
283
- throw new Error('Failed to fetch handlers');
284
- return response.json();
285
- },
286
- enabled: !!client && !!connectionConfig,
288
+ path: '/console/handlers',
289
+ query: { instanceId },
290
+ errorMessage: 'Failed to fetch handlers',
287
291
  });
288
292
  }
289
293
  export function usePrunePreview(options = {}) {
290
- const client = useApiClient();
291
- const { config: connectionConfig } = useConnection();
292
- const { instanceId: selectedInstanceId } = useInstanceContext();
293
- const instanceId = options.instanceId ?? selectedInstanceId;
294
- return useQuery({
294
+ const instanceId = useEffectiveInstanceId(options.instanceId);
295
+ return useConsoleJsonQuery({
295
296
  queryKey: queryKeys.prunePreview(instanceId),
296
- queryFn: async () => {
297
- if (!client || !connectionConfig)
298
- throw new Error('Not connected');
299
- const queryString = new URLSearchParams();
300
- if (instanceId)
301
- queryString.set('instanceId', instanceId);
302
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/prune/preview', queryString), {
303
- method: 'POST',
304
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
305
- });
306
- if (!response.ok)
307
- throw new Error('Failed to fetch prune preview');
308
- return response.json();
309
- },
310
- enabled: !!client && !!connectionConfig && (options.enabled ?? true),
297
+ path: '/console/prune/preview',
298
+ query: { instanceId },
299
+ method: 'POST',
300
+ errorMessage: 'Failed to fetch prune preview',
301
+ enabled: options.enabled,
311
302
  });
312
303
  }
313
304
  export function useOperationEvents(params = {}, options = {}) {
314
- const client = useApiClient();
315
- const { instanceId: selectedInstanceId } = useInstanceContext();
316
- const instanceId = params.instanceId ?? selectedInstanceId;
317
- const query = withInstanceQuery({
318
- limit: params.limit,
319
- offset: params.offset,
320
- operationType: params.operationType,
321
- partitionId: params.partitionId,
322
- }, instanceId);
323
- return useQuery({
305
+ const instanceId = useEffectiveInstanceId(params.instanceId);
306
+ return useConsoleJsonQuery({
324
307
  queryKey: queryKeys.operations({ ...params, instanceId }),
325
- queryFn: () => {
326
- if (!client)
327
- throw new Error('Not connected');
328
- return unwrap(client.GET('/console/operations', { params: { query } }));
308
+ path: '/console/operations',
309
+ query: {
310
+ limit: params.limit,
311
+ offset: params.offset,
312
+ operationType: params.operationType,
313
+ partitionId: params.partitionId,
314
+ instanceId,
329
315
  },
330
- enabled: (options.enabled ?? true) && !!client,
316
+ errorMessage: 'Failed to fetch operations',
317
+ enabled: options.enabled,
331
318
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
332
319
  });
333
320
  }
321
+ export function useClearEventsMutation() {
322
+ return useConsoleJsonMutation({
323
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) => fetchConsoleJson({
324
+ connectionConfig,
325
+ path: '/console/events',
326
+ query: { instanceId: selectedInstanceId },
327
+ method: 'DELETE',
328
+ errorMessage: 'Failed to clear events',
329
+ }),
330
+ invalidateQueryKeys: [['console', 'events']],
331
+ });
332
+ }
334
333
  export function useEvictClientMutation() {
335
- const client = useApiClient();
336
- const { config: connectionConfig } = useConnection();
337
- const { instanceId: selectedInstanceId } = useInstanceContext();
338
- const queryClient = useQueryClient();
339
- return useMutation({
340
- mutationFn: async ({ clientId, partitionId, instanceId }) => {
341
- if (!client || !connectionConfig)
342
- throw new Error('Not connected');
343
- const effectiveInstanceId = instanceId ?? selectedInstanceId;
344
- const queryString = new URLSearchParams();
345
- if (partitionId)
346
- queryString.set('partitionId', partitionId);
347
- if (effectiveInstanceId)
348
- queryString.set('instanceId', effectiveInstanceId);
349
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/clients/${serializePathSegment(clientId)}`, queryString), {
334
+ return useConsoleJsonMutation({
335
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
336
+ const effectiveInstanceId = variables.instanceId ?? selectedInstanceId;
337
+ return fetchConsoleJson({
338
+ connectionConfig,
339
+ path: `/console/clients/${encodeURIComponent(variables.clientId)}`,
340
+ query: {
341
+ partitionId: variables.partitionId,
342
+ instanceId: effectiveInstanceId,
343
+ },
350
344
  method: 'DELETE',
351
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
345
+ errorMessage: 'Failed to evict client',
352
346
  });
353
- if (!response.ok)
354
- throw new Error('Failed to evict client');
355
- return response.json();
356
- },
357
- onSuccess: () => {
358
- queryClient.invalidateQueries({ queryKey: ['console', 'clients'] });
359
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
360
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
361
347
  },
348
+ invalidateQueryKeys: [
349
+ ['console', 'clients'],
350
+ ['console', 'stats'],
351
+ ['console', 'operations'],
352
+ ],
362
353
  });
363
354
  }
364
355
  export function usePruneMutation() {
365
- const client = useApiClient();
366
- const { config: connectionConfig } = useConnection();
367
- const { instanceId } = useInstanceContext();
368
- const queryClient = useQueryClient();
369
- return useMutation({
370
- mutationFn: async () => {
371
- if (!client || !connectionConfig)
372
- throw new Error('Not connected');
373
- const queryString = new URLSearchParams();
374
- if (instanceId)
375
- queryString.set('instanceId', instanceId);
376
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/prune', queryString), {
377
- method: 'POST',
378
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
379
- });
380
- if (!response.ok)
381
- throw new Error('Failed to prune');
382
- return response.json();
383
- },
384
- onSuccess: () => {
385
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
386
- queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
387
- queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
388
- queryClient.invalidateQueries({
389
- queryKey: ['console', 'prune', 'preview'],
390
- });
391
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
392
- },
356
+ return useConsoleJsonMutation({
357
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) => fetchConsoleJson({
358
+ connectionConfig,
359
+ path: '/console/prune',
360
+ query: { instanceId: selectedInstanceId },
361
+ method: 'POST',
362
+ errorMessage: 'Failed to prune',
363
+ }),
364
+ invalidateQueryKeys: [
365
+ ['console', 'stats'],
366
+ ['console', 'commits'],
367
+ ['console', 'timeline'],
368
+ ['console', 'prune', 'preview'],
369
+ ['console', 'operations'],
370
+ ],
393
371
  });
394
372
  }
395
373
  export function useCompactMutation() {
396
- const client = useApiClient();
397
- const { config: connectionConfig } = useConnection();
398
- const { instanceId } = useInstanceContext();
399
- const queryClient = useQueryClient();
400
- return useMutation({
401
- mutationFn: async () => {
402
- if (!client || !connectionConfig)
403
- throw new Error('Not connected');
404
- const queryString = new URLSearchParams();
405
- if (instanceId)
406
- queryString.set('instanceId', instanceId);
407
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/compact', queryString), {
408
- method: 'POST',
409
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
410
- });
411
- if (!response.ok)
412
- throw new Error('Failed to compact');
413
- return response.json();
414
- },
415
- onSuccess: () => {
416
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
417
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
418
- },
374
+ return useConsoleJsonMutation({
375
+ mutationFn: async ({ connectionConfig, selectedInstanceId }) => fetchConsoleJson({
376
+ connectionConfig,
377
+ path: '/console/compact',
378
+ query: { instanceId: selectedInstanceId },
379
+ method: 'POST',
380
+ errorMessage: 'Failed to compact',
381
+ }),
382
+ invalidateQueryKeys: [
383
+ ['console', 'stats'],
384
+ ['console', 'operations'],
385
+ ],
419
386
  });
420
387
  }
421
388
  export function useNotifyDataChangeMutation() {
422
- const client = useApiClient();
423
- const { config: connectionConfig } = useConnection();
424
- const { instanceId: selectedInstanceId } = useInstanceContext();
425
- const queryClient = useQueryClient();
426
- return useMutation({
427
- mutationFn: async (request) => {
428
- if (!client || !connectionConfig)
429
- throw new Error('Not connected');
430
- const effectiveInstanceId = request.instanceId ?? selectedInstanceId;
431
- const queryString = new URLSearchParams();
432
- if (effectiveInstanceId)
433
- queryString.set('instanceId', effectiveInstanceId);
434
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/notify-data-change', queryString), {
389
+ return useConsoleJsonMutation({
390
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
391
+ const effectiveInstanceId = variables.instanceId ?? selectedInstanceId;
392
+ return fetchConsoleJson({
393
+ connectionConfig,
394
+ path: '/console/notify-data-change',
395
+ query: { instanceId: effectiveInstanceId },
435
396
  method: 'POST',
436
- headers: {
437
- Authorization: `Bearer ${connectionConfig.token}`,
438
- 'Content-Type': 'application/json',
397
+ body: {
398
+ tables: variables.tables,
399
+ partitionId: variables.partitionId,
439
400
  },
440
- body: JSON.stringify({
441
- tables: request.tables,
442
- partitionId: request.partitionId,
443
- }),
401
+ errorMessage: 'Failed to notify data change',
444
402
  });
445
- if (!response.ok)
446
- throw new Error('Failed to notify data change');
447
- return response.json();
448
- },
449
- onSuccess: () => {
450
- queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
451
- queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
452
- queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
453
- queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
454
403
  },
404
+ invalidateQueryKeys: [
405
+ ['console', 'stats'],
406
+ ['console', 'commits'],
407
+ ['console', 'timeline'],
408
+ ['console', 'operations'],
409
+ ],
455
410
  });
456
411
  }
457
412
  export function useApiKeys(params = {}) {
458
- const client = useApiClient();
459
- const { config: connectionConfig } = useConnection();
460
- const { instanceId: selectedInstanceId } = useInstanceContext();
461
- const instanceId = params.instanceId ?? selectedInstanceId;
462
- return useQuery({
413
+ const instanceId = useEffectiveInstanceId(params.instanceId);
414
+ return useConsoleJsonQuery({
463
415
  queryKey: queryKeys.apiKeys({ ...params, instanceId }),
464
- queryFn: async () => {
465
- if (!client || !connectionConfig)
466
- throw new Error('Not connected');
467
- const queryString = new URLSearchParams();
468
- if (params.limit !== undefined)
469
- queryString.set('limit', String(params.limit));
470
- if (params.offset !== undefined)
471
- queryString.set('offset', String(params.offset));
472
- if (params.type)
473
- queryString.set('type', params.type);
474
- if (params.status)
475
- queryString.set('status', params.status);
476
- if (params.expiresWithinDays !== undefined) {
477
- queryString.set('expiresWithinDays', String(params.expiresWithinDays));
478
- }
479
- if (instanceId)
480
- queryString.set('instanceId', instanceId);
481
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/api-keys', queryString), {
482
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
483
- });
484
- if (!response.ok)
485
- throw new Error('Failed to fetch API keys');
486
- return response.json();
416
+ path: '/console/api-keys',
417
+ query: {
418
+ limit: params.limit,
419
+ offset: params.offset,
420
+ type: params.type,
421
+ status: params.status,
422
+ expiresWithinDays: params.expiresWithinDays,
423
+ instanceId,
487
424
  },
488
- enabled: !!client && !!connectionConfig,
425
+ errorMessage: 'Failed to fetch API keys',
489
426
  });
490
427
  }
491
428
  export function useCreateApiKeyMutation() {
492
- const client = useApiClient();
493
- const { config: connectionConfig } = useConnection();
494
- const { instanceId } = useInstanceContext();
495
- const queryClient = useQueryClient();
496
- return useMutation({
497
- mutationFn: async (request) => {
498
- if (!client || !connectionConfig)
499
- throw new Error('Not connected');
500
- const queryString = new URLSearchParams();
501
- if (instanceId)
502
- queryString.set('instanceId', instanceId);
503
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/api-keys', queryString), {
429
+ return useConsoleJsonMutation({
430
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
431
+ return fetchConsoleJson({
432
+ connectionConfig,
433
+ path: '/console/api-keys',
434
+ query: { instanceId: selectedInstanceId },
504
435
  method: 'POST',
505
- headers: {
506
- Authorization: `Bearer ${connectionConfig.token}`,
507
- 'Content-Type': 'application/json',
508
- },
509
- body: JSON.stringify(request),
436
+ body: variables,
437
+ errorMessage: 'Failed to create API key',
510
438
  });
511
- if (!response.ok)
512
- throw new Error('Failed to create API key');
513
- return response.json();
514
- },
515
- onSuccess: () => {
516
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
517
439
  },
440
+ invalidateQueryKeys: [['console', 'api-keys']],
518
441
  });
519
442
  }
520
443
  export function useRevokeApiKeyMutation() {
521
- const client = useApiClient();
522
- const { config: connectionConfig } = useConnection();
523
- const { instanceId } = useInstanceContext();
524
- const queryClient = useQueryClient();
525
- return useMutation({
526
- mutationFn: async (keyId) => {
527
- if (!client || !connectionConfig)
528
- throw new Error('Not connected');
529
- const queryString = new URLSearchParams();
530
- if (instanceId)
531
- queryString.set('instanceId', instanceId);
532
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/api-keys/${serializePathSegment(keyId)}`, queryString), {
444
+ return useConsoleJsonMutation({
445
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
446
+ return fetchConsoleJson({
447
+ connectionConfig,
448
+ path: `/console/api-keys/${encodeURIComponent(variables)}`,
449
+ query: { instanceId: selectedInstanceId },
533
450
  method: 'DELETE',
534
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
451
+ errorMessage: 'Failed to revoke API key',
535
452
  });
536
- if (!response.ok)
537
- throw new Error('Failed to revoke API key');
538
- return response.json();
539
- },
540
- onSuccess: () => {
541
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
542
453
  },
454
+ invalidateQueryKeys: [['console', 'api-keys']],
543
455
  });
544
456
  }
545
457
  export function useBulkRevokeApiKeysMutation() {
546
- const client = useApiClient();
547
- const { config: connectionConfig } = useConnection();
548
- const { instanceId } = useInstanceContext();
549
- const queryClient = useQueryClient();
550
- return useMutation({
551
- mutationFn: async (request) => {
552
- if (!client || !connectionConfig)
553
- throw new Error('Not connected');
554
- const queryString = new URLSearchParams();
555
- if (instanceId)
556
- queryString.set('instanceId', instanceId);
557
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/api-keys/bulk-revoke', queryString), {
458
+ return useConsoleJsonMutation({
459
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
460
+ return fetchConsoleJson({
461
+ connectionConfig,
462
+ path: '/console/api-keys/bulk-revoke',
463
+ query: { instanceId: selectedInstanceId },
558
464
  method: 'POST',
559
- headers: {
560
- Authorization: `Bearer ${connectionConfig.token}`,
561
- 'Content-Type': 'application/json',
562
- },
563
- body: JSON.stringify(request),
465
+ body: variables,
466
+ errorMessage: 'Failed to bulk revoke API keys',
564
467
  });
565
- if (!response.ok)
566
- throw new Error('Failed to bulk revoke API keys');
567
- return response.json();
568
- },
569
- onSuccess: () => {
570
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
571
468
  },
469
+ invalidateQueryKeys: [['console', 'api-keys']],
572
470
  });
573
471
  }
574
472
  export function useRotateApiKeyMutation() {
575
- const client = useApiClient();
576
- const { config: connectionConfig } = useConnection();
577
- const { instanceId } = useInstanceContext();
578
- const queryClient = useQueryClient();
579
- return useMutation({
580
- mutationFn: async (keyId) => {
581
- if (!client || !connectionConfig)
582
- throw new Error('Not connected');
583
- const queryString = new URLSearchParams();
584
- if (instanceId)
585
- queryString.set('instanceId', instanceId);
586
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/api-keys/${serializePathSegment(keyId)}/rotate`, queryString), {
473
+ return useConsoleJsonMutation({
474
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
475
+ return fetchConsoleJson({
476
+ connectionConfig,
477
+ path: `/console/api-keys/${encodeURIComponent(variables)}/rotate`,
478
+ query: { instanceId: selectedInstanceId },
587
479
  method: 'POST',
588
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
480
+ errorMessage: 'Failed to rotate API key',
589
481
  });
590
- if (!response.ok)
591
- throw new Error('Failed to rotate API key');
592
- return response.json();
593
- },
594
- onSuccess: () => {
595
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
596
482
  },
483
+ invalidateQueryKeys: [['console', 'api-keys']],
597
484
  });
598
485
  }
599
486
  export function useStageRotateApiKeyMutation() {
600
- const client = useApiClient();
601
- const { config: connectionConfig } = useConnection();
602
- const { instanceId } = useInstanceContext();
603
- const queryClient = useQueryClient();
604
- return useMutation({
605
- mutationFn: async (keyId) => {
606
- if (!client || !connectionConfig)
607
- throw new Error('Not connected');
608
- const queryString = new URLSearchParams();
609
- if (instanceId)
610
- queryString.set('instanceId', instanceId);
611
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/api-keys/${serializePathSegment(keyId)}/rotate/stage`, queryString), {
487
+ return useConsoleJsonMutation({
488
+ mutationFn: async ({ connectionConfig, variables, selectedInstanceId }) => {
489
+ return fetchConsoleJson({
490
+ connectionConfig,
491
+ path: `/console/api-keys/${encodeURIComponent(variables)}/rotate/stage`,
492
+ query: { instanceId: selectedInstanceId },
612
493
  method: 'POST',
613
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
494
+ errorMessage: 'Failed to stage-rotate API key',
614
495
  });
615
- if (!response.ok)
616
- throw new Error('Failed to stage-rotate API key');
617
- return response.json();
618
- },
619
- onSuccess: () => {
620
- queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
621
496
  },
497
+ invalidateQueryKeys: [['console', 'api-keys']],
622
498
  });
623
499
  }
624
500
  // ---------------------------------------------------------------------------
625
501
  // Blob storage hooks
626
502
  // ---------------------------------------------------------------------------
627
503
  export function useBlobs(options = {}) {
628
- const { config: connectionConfig } = useConnection();
629
- return useQuery({
504
+ return useConsoleJsonQuery({
630
505
  queryKey: queryKeys.storage({
631
506
  prefix: options.prefix,
632
507
  cursor: options.cursor,
633
508
  limit: options.limit,
634
509
  }),
635
- queryFn: async () => {
636
- if (!connectionConfig)
637
- throw new Error('Not connected');
638
- const queryString = new URLSearchParams();
639
- if (options.prefix)
640
- queryString.set('prefix', options.prefix);
641
- if (options.cursor)
642
- queryString.set('cursor', options.cursor);
643
- if (options.limit)
644
- queryString.set('limit', String(options.limit));
645
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, '/console/storage', queryString), {
646
- method: 'GET',
647
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
648
- });
649
- if (!response.ok)
650
- throw new Error('Failed to list blobs');
651
- return response.json();
510
+ path: '/console/storage',
511
+ query: {
512
+ prefix: options.prefix,
513
+ cursor: options.cursor,
514
+ limit: options.limit,
652
515
  },
653
- enabled: !!connectionConfig,
516
+ errorMessage: 'Failed to list blobs',
654
517
  refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
655
518
  });
656
519
  }
657
520
  export function useDeleteBlobMutation() {
658
- const { config: connectionConfig } = useConnection();
659
- const queryClient = useQueryClient();
660
- return useMutation({
661
- mutationFn: async (key) => {
662
- if (!connectionConfig)
663
- throw new Error('Not connected');
664
- const encodedKey = encodeURIComponent(key);
665
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/storage/${encodedKey}`, new URLSearchParams()), {
666
- method: 'DELETE',
667
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
668
- });
669
- if (!response.ok)
670
- throw new Error('Failed to delete blob');
671
- return response.json();
672
- },
673
- onSuccess: () => {
674
- queryClient.invalidateQueries({ queryKey: ['console', 'storage'] });
675
- },
521
+ return useConsoleJsonMutation({
522
+ mutationFn: async ({ connectionConfig, variables }) => fetchConsoleJson({
523
+ connectionConfig,
524
+ path: `/console/storage/${encodeURIComponent(variables)}`,
525
+ method: 'DELETE',
526
+ errorMessage: 'Failed to delete blob',
527
+ }),
528
+ invalidateQueryKeys: [['console', 'storage']],
676
529
  });
677
530
  }
678
531
  export function useBlobDownload() {
679
- const { config: connectionConfig } = useConnection();
532
+ const { config: connectionConfig, isConnected } = useConnection();
680
533
  return async (key) => {
681
- if (!connectionConfig)
682
- throw new Error('Not connected');
683
- const encodedKey = encodeURIComponent(key);
684
- const response = await fetch(buildConsoleUrl(connectionConfig.serverUrl, `/console/storage/${encodedKey}/download`, new URLSearchParams()), {
685
- method: 'GET',
686
- headers: { Authorization: `Bearer ${connectionConfig.token}` },
534
+ const blob = await fetchConsoleBlob({
535
+ connectionConfig: requireConnection(connectionConfig, isConnected),
536
+ path: `/console/storage/${encodeURIComponent(key)}/download`,
537
+ errorMessage: 'Failed to download blob',
687
538
  });
688
- if (!response.ok)
689
- throw new Error('Failed to download blob');
690
- const blob = await response.blob();
691
539
  const url = URL.createObjectURL(blob);
692
540
  const a = document.createElement('a');
693
541
  a.href = url;