@syncular/console 0.0.0
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.
- package/package.json +67 -0
- package/src/App.tsx +44 -0
- package/src/hooks/ConnectionContext.tsx +213 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useConsoleApi.ts +926 -0
- package/src/hooks/useInstanceContext.ts +31 -0
- package/src/hooks/useLiveEvents.ts +267 -0
- package/src/hooks/useLocalStorage.ts +40 -0
- package/src/hooks/usePartitionContext.ts +31 -0
- package/src/hooks/usePreferences.ts +72 -0
- package/src/hooks/useRequestEvents.ts +35 -0
- package/src/hooks/useTimeRange.ts +34 -0
- package/src/index.ts +8 -0
- package/src/layout.tsx +240 -0
- package/src/lib/api.ts +26 -0
- package/src/lib/topology.ts +102 -0
- package/src/lib/types.ts +228 -0
- package/src/mount.tsx +39 -0
- package/src/pages/Command.tsx +382 -0
- package/src/pages/Config.tsx +1190 -0
- package/src/pages/Fleet.tsx +242 -0
- package/src/pages/Ops.tsx +753 -0
- package/src/pages/Stream.tsx +854 -0
- package/src/pages/index.ts +5 -0
- package/src/routeTree.ts +18 -0
- package/src/routes/__root.tsx +6 -0
- package/src/routes/config.tsx +9 -0
- package/src/routes/fleet.tsx +9 -0
- package/src/routes/index.tsx +9 -0
- package/src/routes/investigate-commit.tsx +14 -0
- package/src/routes/investigate-event.tsx +14 -0
- package/src/routes/ops.tsx +9 -0
- package/src/routes/stream.tsx +9 -0
- package/src/sentry.ts +70 -0
- package/src/styles/globals.css +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
3
|
+
|
|
4
|
+
interface InstanceContext {
|
|
5
|
+
instanceId: string | undefined;
|
|
6
|
+
rawInstanceId: string;
|
|
7
|
+
setInstanceId: (value: string) => void;
|
|
8
|
+
clearInstanceId: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const INSTANCE_STORAGE_KEY = 'console:instance-id';
|
|
12
|
+
|
|
13
|
+
export function useInstanceContext(): InstanceContext {
|
|
14
|
+
const [rawInstanceId, setRawInstanceId] = useLocalStorage<string>(
|
|
15
|
+
INSTANCE_STORAGE_KEY,
|
|
16
|
+
''
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const normalizedInstanceId = rawInstanceId.trim();
|
|
20
|
+
|
|
21
|
+
return useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
instanceId:
|
|
24
|
+
normalizedInstanceId.length > 0 ? normalizedInstanceId : undefined,
|
|
25
|
+
rawInstanceId,
|
|
26
|
+
setInstanceId: setRawInstanceId,
|
|
27
|
+
clearInstanceId: () => setRawInstanceId(''),
|
|
28
|
+
}),
|
|
29
|
+
[normalizedInstanceId, rawInstanceId, setRawInstanceId]
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for WebSocket live events from the console API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import type { LiveEvent } from '../lib/types';
|
|
7
|
+
import { useConnection } from './ConnectionContext';
|
|
8
|
+
import { useInstanceContext } from './useInstanceContext';
|
|
9
|
+
|
|
10
|
+
interface UseLiveEventsOptions {
|
|
11
|
+
/** Maximum number of events to keep in the buffer */
|
|
12
|
+
maxEvents?: number;
|
|
13
|
+
/** Whether to connect automatically */
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/** Mark socket stale when no control/data messages are received in this window */
|
|
16
|
+
staleAfterMs?: number;
|
|
17
|
+
/** Maximum number of server replayed events per reconnect (1-500) */
|
|
18
|
+
replayLimit?: number;
|
|
19
|
+
/** Optional partition filter for emitted events */
|
|
20
|
+
partitionId?: string;
|
|
21
|
+
/** Optional instance filter for emitted events */
|
|
22
|
+
instanceId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface UseLiveEventsResult {
|
|
26
|
+
/** Recent events (newest first) */
|
|
27
|
+
events: LiveEvent[];
|
|
28
|
+
/** Whether currently connected to the WebSocket */
|
|
29
|
+
isConnected: boolean;
|
|
30
|
+
/** Connection lifecycle state */
|
|
31
|
+
connectionState: 'connecting' | 'connected' | 'stale' | 'disconnected';
|
|
32
|
+
/** Any error that occurred */
|
|
33
|
+
error: Error | null;
|
|
34
|
+
/** Clear all events from the buffer */
|
|
35
|
+
clearEvents: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useLiveEvents(
|
|
39
|
+
options: UseLiveEventsOptions = {}
|
|
40
|
+
): UseLiveEventsResult {
|
|
41
|
+
const {
|
|
42
|
+
maxEvents = 100,
|
|
43
|
+
enabled = true,
|
|
44
|
+
staleAfterMs = 65_000,
|
|
45
|
+
replayLimit = 100,
|
|
46
|
+
partitionId,
|
|
47
|
+
instanceId,
|
|
48
|
+
} = options;
|
|
49
|
+
const { config, isConnected: apiConnected } = useConnection();
|
|
50
|
+
const { instanceId: selectedInstanceId } = useInstanceContext();
|
|
51
|
+
const effectiveInstanceId = instanceId ?? selectedInstanceId;
|
|
52
|
+
const [events, setEvents] = useState<LiveEvent[]>([]);
|
|
53
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
54
|
+
const [connectionState, setConnectionState] = useState<
|
|
55
|
+
'connecting' | 'connected' | 'stale' | 'disconnected'
|
|
56
|
+
>('disconnected');
|
|
57
|
+
const [error, setError] = useState<Error | null>(null);
|
|
58
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
59
|
+
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
60
|
+
null
|
|
61
|
+
);
|
|
62
|
+
const staleCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
|
|
63
|
+
null
|
|
64
|
+
);
|
|
65
|
+
const reconnectAttemptsRef = useRef(0);
|
|
66
|
+
const lastActivityAtRef = useRef(0);
|
|
67
|
+
const lastEventTimestampRef = useRef<string | null>(null);
|
|
68
|
+
|
|
69
|
+
const clearEvents = useCallback(() => {
|
|
70
|
+
setEvents([]);
|
|
71
|
+
lastEventTimestampRef.current = null;
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!enabled || !apiConnected || !config?.serverUrl || !config?.token) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let isCleanedUp = false;
|
|
80
|
+
const normalizedReplayLimit = Number.isFinite(replayLimit)
|
|
81
|
+
? Math.max(1, Math.min(500, Math.floor(replayLimit)))
|
|
82
|
+
: 100;
|
|
83
|
+
|
|
84
|
+
const clearReconnectTimeout = () => {
|
|
85
|
+
if (!reconnectTimeoutRef.current) return;
|
|
86
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
87
|
+
reconnectTimeoutRef.current = null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const clearStaleInterval = () => {
|
|
91
|
+
if (!staleCheckIntervalRef.current) return;
|
|
92
|
+
clearInterval(staleCheckIntervalRef.current);
|
|
93
|
+
staleCheckIntervalRef.current = null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const scheduleReconnect = () => {
|
|
97
|
+
if (isCleanedUp || reconnectTimeoutRef.current) return;
|
|
98
|
+
reconnectAttemptsRef.current += 1;
|
|
99
|
+
const baseDelayMs = Math.min(
|
|
100
|
+
30_000,
|
|
101
|
+
1_000 * 2 ** Math.max(0, reconnectAttemptsRef.current - 1)
|
|
102
|
+
);
|
|
103
|
+
const jitterMs = Math.floor(baseDelayMs * 0.2 * Math.random());
|
|
104
|
+
const delayMs = baseDelayMs + jitterMs;
|
|
105
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
106
|
+
reconnectTimeoutRef.current = null;
|
|
107
|
+
if (!isCleanedUp) {
|
|
108
|
+
connect();
|
|
109
|
+
}
|
|
110
|
+
}, delayMs);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const markActivity = () => {
|
|
114
|
+
lastActivityAtRef.current = Date.now();
|
|
115
|
+
setIsConnected(true);
|
|
116
|
+
setConnectionState('connected');
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const connect = () => {
|
|
120
|
+
if (isCleanedUp) return;
|
|
121
|
+
setConnectionState('connecting');
|
|
122
|
+
clearReconnectTimeout();
|
|
123
|
+
|
|
124
|
+
const wsUrl = (() => {
|
|
125
|
+
const baseUrl = new URL(config.serverUrl, window.location.origin);
|
|
126
|
+
baseUrl.protocol = baseUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
127
|
+
const normalizedPath = baseUrl.pathname.endsWith('/')
|
|
128
|
+
? baseUrl.pathname.slice(0, -1)
|
|
129
|
+
: baseUrl.pathname;
|
|
130
|
+
baseUrl.pathname = `${normalizedPath}/console/events/live`;
|
|
131
|
+
baseUrl.search = '';
|
|
132
|
+
baseUrl.searchParams.set('token', config.token);
|
|
133
|
+
if (lastEventTimestampRef.current) {
|
|
134
|
+
baseUrl.searchParams.set('since', lastEventTimestampRef.current);
|
|
135
|
+
}
|
|
136
|
+
baseUrl.searchParams.set('replayLimit', String(normalizedReplayLimit));
|
|
137
|
+
if (partitionId) {
|
|
138
|
+
baseUrl.searchParams.set('partitionId', partitionId);
|
|
139
|
+
}
|
|
140
|
+
if (effectiveInstanceId) {
|
|
141
|
+
baseUrl.searchParams.set('instanceId', effectiveInstanceId);
|
|
142
|
+
}
|
|
143
|
+
return baseUrl.toString();
|
|
144
|
+
})();
|
|
145
|
+
|
|
146
|
+
const ws = new WebSocket(wsUrl);
|
|
147
|
+
wsRef.current = ws;
|
|
148
|
+
|
|
149
|
+
ws.onopen = () => {
|
|
150
|
+
if (isCleanedUp) {
|
|
151
|
+
ws.close();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
reconnectAttemptsRef.current = 0;
|
|
155
|
+
markActivity();
|
|
156
|
+
setError(null);
|
|
157
|
+
|
|
158
|
+
clearStaleInterval();
|
|
159
|
+
staleCheckIntervalRef.current = setInterval(() => {
|
|
160
|
+
const socket = wsRef.current;
|
|
161
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
|
162
|
+
const lastActivityAt = lastActivityAtRef.current;
|
|
163
|
+
if (!lastActivityAt) return;
|
|
164
|
+
const elapsedMs = Date.now() - lastActivityAt;
|
|
165
|
+
if (elapsedMs <= staleAfterMs) return;
|
|
166
|
+
|
|
167
|
+
setIsConnected(false);
|
|
168
|
+
setConnectionState('stale');
|
|
169
|
+
socket.close();
|
|
170
|
+
}, 1000);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
ws.onclose = () => {
|
|
174
|
+
if (isCleanedUp) return;
|
|
175
|
+
setIsConnected(false);
|
|
176
|
+
setConnectionState('disconnected');
|
|
177
|
+
clearStaleInterval();
|
|
178
|
+
scheduleReconnect();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
ws.onerror = () => {
|
|
182
|
+
if (isCleanedUp) return;
|
|
183
|
+
setError(new Error('WebSocket connection failed'));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
187
|
+
try {
|
|
188
|
+
const data = JSON.parse(event.data);
|
|
189
|
+
const eventType = data.type;
|
|
190
|
+
markActivity();
|
|
191
|
+
|
|
192
|
+
// Skip control events
|
|
193
|
+
if (eventType === 'connected' || eventType === 'heartbeat') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const liveEvent: LiveEvent = {
|
|
198
|
+
type: eventType as LiveEvent['type'],
|
|
199
|
+
timestamp: data.timestamp || new Date().toISOString(),
|
|
200
|
+
data,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (partitionId && liveEvent.data.partitionId !== partitionId) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
effectiveInstanceId &&
|
|
208
|
+
liveEvent.data.instanceId !== effectiveInstanceId
|
|
209
|
+
) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lastEventTimestampMs = Date.parse(
|
|
214
|
+
lastEventTimestampRef.current ?? ''
|
|
215
|
+
);
|
|
216
|
+
const liveEventTimestampMs = Date.parse(liveEvent.timestamp);
|
|
217
|
+
if (
|
|
218
|
+
Number.isFinite(liveEventTimestampMs) &&
|
|
219
|
+
(!Number.isFinite(lastEventTimestampMs) ||
|
|
220
|
+
liveEventTimestampMs > lastEventTimestampMs)
|
|
221
|
+
) {
|
|
222
|
+
lastEventTimestampRef.current = liveEvent.timestamp;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setEvents((prev) => {
|
|
226
|
+
const newEvents = [liveEvent, ...prev];
|
|
227
|
+
return newEvents.slice(0, maxEvents);
|
|
228
|
+
});
|
|
229
|
+
} catch {
|
|
230
|
+
// Ignore parse errors
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
connect();
|
|
236
|
+
|
|
237
|
+
return () => {
|
|
238
|
+
isCleanedUp = true;
|
|
239
|
+
clearReconnectTimeout();
|
|
240
|
+
clearStaleInterval();
|
|
241
|
+
if (wsRef.current) {
|
|
242
|
+
wsRef.current.close();
|
|
243
|
+
wsRef.current = null;
|
|
244
|
+
}
|
|
245
|
+
setIsConnected(false);
|
|
246
|
+
setConnectionState('disconnected');
|
|
247
|
+
};
|
|
248
|
+
}, [
|
|
249
|
+
enabled,
|
|
250
|
+
apiConnected,
|
|
251
|
+
config?.serverUrl,
|
|
252
|
+
config?.token,
|
|
253
|
+
maxEvents,
|
|
254
|
+
partitionId,
|
|
255
|
+
effectiveInstanceId,
|
|
256
|
+
replayLimit,
|
|
257
|
+
staleAfterMs,
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
events,
|
|
262
|
+
isConnected,
|
|
263
|
+
connectionState,
|
|
264
|
+
error,
|
|
265
|
+
clearEvents,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useLocalStorage<T>(
|
|
4
|
+
key: string,
|
|
5
|
+
defaultValue: T
|
|
6
|
+
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
7
|
+
const [value, setValue] = useState<T>(() => {
|
|
8
|
+
if (typeof window === 'undefined') return defaultValue;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const item = window.localStorage.getItem(key);
|
|
12
|
+
if (item === null) return defaultValue;
|
|
13
|
+
return JSON.parse(item) as T;
|
|
14
|
+
} catch {
|
|
15
|
+
return defaultValue;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
try {
|
|
21
|
+
if (value === null || value === undefined) {
|
|
22
|
+
window.localStorage.removeItem(key);
|
|
23
|
+
} else {
|
|
24
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore write errors
|
|
28
|
+
}
|
|
29
|
+
}, [key, value]);
|
|
30
|
+
|
|
31
|
+
const setValueWrapper = useCallback((newValue: T | ((prev: T) => T)) => {
|
|
32
|
+
setValue((prev) =>
|
|
33
|
+
typeof newValue === 'function'
|
|
34
|
+
? (newValue as (prev: T) => T)(prev)
|
|
35
|
+
: newValue
|
|
36
|
+
);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return [value, setValueWrapper];
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
3
|
+
|
|
4
|
+
interface PartitionContext {
|
|
5
|
+
partitionId: string | undefined;
|
|
6
|
+
rawPartitionId: string;
|
|
7
|
+
setPartitionId: (value: string) => void;
|
|
8
|
+
clearPartitionId: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PARTITION_STORAGE_KEY = 'console:partition-id';
|
|
12
|
+
|
|
13
|
+
export function usePartitionContext(): PartitionContext {
|
|
14
|
+
const [rawPartitionId, setRawPartitionId] = useLocalStorage<string>(
|
|
15
|
+
PARTITION_STORAGE_KEY,
|
|
16
|
+
''
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const normalizedPartitionId = rawPartitionId.trim();
|
|
20
|
+
|
|
21
|
+
return useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
partitionId:
|
|
24
|
+
normalizedPartitionId.length > 0 ? normalizedPartitionId : undefined,
|
|
25
|
+
rawPartitionId,
|
|
26
|
+
setPartitionId: setRawPartitionId,
|
|
27
|
+
clearPartitionId: () => setRawPartitionId(''),
|
|
28
|
+
}),
|
|
29
|
+
[normalizedPartitionId, rawPartitionId, setRawPartitionId]
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI preferences hook with localStorage persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
6
|
+
|
|
7
|
+
interface ConsolePreferences {
|
|
8
|
+
/** Refresh interval for auto-updating data (in seconds) */
|
|
9
|
+
refreshInterval: number;
|
|
10
|
+
/** Time format: 'relative' (e.g., "5 minutes ago") or 'absolute' (e.g., "2024-01-15 10:30") */
|
|
11
|
+
timeFormat: 'relative' | 'absolute';
|
|
12
|
+
/** Show sparklines in stats cards */
|
|
13
|
+
showSparklines: boolean;
|
|
14
|
+
/** Number of items per page in tables */
|
|
15
|
+
pageSize: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PREFERENCES: ConsolePreferences = {
|
|
19
|
+
refreshInterval: 5,
|
|
20
|
+
timeFormat: 'relative',
|
|
21
|
+
showSparklines: true,
|
|
22
|
+
pageSize: 20,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function usePreferences() {
|
|
26
|
+
const [preferences, setPreferences] = useLocalStorage<ConsolePreferences>(
|
|
27
|
+
'console:preferences',
|
|
28
|
+
DEFAULT_PREFERENCES
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const updatePreference = <K extends keyof ConsolePreferences>(
|
|
32
|
+
key: K,
|
|
33
|
+
value: ConsolePreferences[K]
|
|
34
|
+
) => {
|
|
35
|
+
setPreferences((prev) => ({
|
|
36
|
+
...prev,
|
|
37
|
+
[key]: value,
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const resetPreferences = () => {
|
|
42
|
+
setPreferences(DEFAULT_PREFERENCES);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
preferences,
|
|
47
|
+
setPreferences,
|
|
48
|
+
updatePreference,
|
|
49
|
+
resetPreferences,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Available refresh interval options
|
|
55
|
+
*/
|
|
56
|
+
export const REFRESH_INTERVAL_OPTIONS = [
|
|
57
|
+
{ value: 5, label: '5 seconds' },
|
|
58
|
+
{ value: 10, label: '10 seconds' },
|
|
59
|
+
{ value: 30, label: '30 seconds' },
|
|
60
|
+
{ value: 60, label: '1 minute' },
|
|
61
|
+
{ value: 0, label: 'Manual only' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Available page size options
|
|
66
|
+
*/
|
|
67
|
+
export const PAGE_SIZE_OPTIONS = [
|
|
68
|
+
{ value: 10, label: '10' },
|
|
69
|
+
{ value: 20, label: '20' },
|
|
70
|
+
{ value: 50, label: '50' },
|
|
71
|
+
{ value: 100, label: '100' },
|
|
72
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Query hooks for Request Events
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
|
+
import { useApiClient, useConnection } from './ConnectionContext';
|
|
7
|
+
import { useInstanceContext } from './useInstanceContext';
|
|
8
|
+
|
|
9
|
+
export function useClearEventsMutation() {
|
|
10
|
+
const client = useApiClient();
|
|
11
|
+
const { config: connectionConfig } = useConnection();
|
|
12
|
+
const { instanceId } = useInstanceContext();
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
|
|
15
|
+
return useMutation<{ deletedCount: number }, Error, void>({
|
|
16
|
+
mutationFn: async () => {
|
|
17
|
+
if (!client || !connectionConfig) throw new Error('Not connected');
|
|
18
|
+
const queryString = new URLSearchParams();
|
|
19
|
+
if (instanceId) queryString.set('instanceId', instanceId);
|
|
20
|
+
const suffix = queryString.toString();
|
|
21
|
+
const response = await fetch(
|
|
22
|
+
`${connectionConfig.serverUrl}/console/events${suffix ? `?${suffix}` : ''}`,
|
|
23
|
+
{
|
|
24
|
+
method: 'DELETE',
|
|
25
|
+
headers: { Authorization: `Bearer ${connectionConfig.token}` },
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
if (!response.ok) throw new Error('Failed to clear events');
|
|
29
|
+
return response.json();
|
|
30
|
+
},
|
|
31
|
+
onSuccess: () => {
|
|
32
|
+
queryClient.invalidateQueries({ queryKey: ['console', 'events'] });
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global time range context for dashboard charts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createContext } from 'react';
|
|
6
|
+
import type { TimeseriesRange } from '../lib/types';
|
|
7
|
+
import { useLocalStorage } from './useLocalStorage';
|
|
8
|
+
|
|
9
|
+
interface TimeRangeContextValue {
|
|
10
|
+
/** Current time range */
|
|
11
|
+
range: TimeseriesRange;
|
|
12
|
+
/** Set the time range */
|
|
13
|
+
setRange: (range: TimeseriesRange) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TimeRangeContext = createContext<TimeRangeContextValue | null>(
|
|
17
|
+
null
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook to create time range state with localStorage persistence.
|
|
22
|
+
* Use this at the provider level.
|
|
23
|
+
*/
|
|
24
|
+
export function useTimeRangeState(): TimeRangeContextValue {
|
|
25
|
+
const [range, setRange] = useLocalStorage<TimeseriesRange>(
|
|
26
|
+
'console:time-range',
|
|
27
|
+
'24h'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
range,
|
|
32
|
+
setRange,
|
|
33
|
+
};
|
|
34
|
+
}
|