@syncular/console 0.0.4-26 → 0.0.6-100
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/dist/App.d.ts +3 -7
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +3 -3
- package/dist/App.js.map +1 -1
- package/dist/hooks/ConnectionContext.d.ts +4 -1
- package/dist/hooks/ConnectionContext.d.ts.map +1 -1
- package/dist/hooks/ConnectionContext.js +116 -28
- package/dist/hooks/ConnectionContext.js.map +1 -1
- package/dist/hooks/useConsoleApi.d.ts +11 -1
- package/dist/hooks/useConsoleApi.d.ts.map +1 -1
- package/dist/hooks/useConsoleApi.js +78 -0
- package/dist/hooks/useConsoleApi.js.map +1 -1
- package/dist/hooks/useLiveEvents.d.ts.map +1 -1
- package/dist/hooks/useLiveEvents.js +116 -3
- package/dist/hooks/useLiveEvents.js.map +1 -1
- package/dist/layout.d.ts +4 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +8 -7
- package/dist/layout.js.map +1 -1
- package/dist/lib/api.d.ts +1 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +36 -4
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/mount.d.ts +1 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +1 -1
- package/dist/mount.js.map +1 -1
- package/dist/pages/Config.d.ts +3 -1
- package/dist/pages/Config.d.ts.map +1 -1
- package/dist/pages/Config.js +24 -17
- package/dist/pages/Config.js.map +1 -1
- package/dist/pages/Fleet.d.ts +3 -1
- package/dist/pages/Fleet.d.ts.map +1 -1
- package/dist/pages/Fleet.js +6 -3
- package/dist/pages/Fleet.js.map +1 -1
- package/dist/pages/Ops.js.map +1 -1
- package/dist/pages/Storage.d.ts +2 -0
- package/dist/pages/Storage.d.ts.map +1 -0
- package/dist/pages/Storage.js +103 -0
- package/dist/pages/Storage.js.map +1 -0
- package/dist/pages/Stream.d.ts.map +1 -1
- package/dist/pages/Stream.js +2 -3
- package/dist/pages/Stream.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/routeTree.d.ts +1 -1
- package/dist/routeTree.d.ts.map +1 -1
- package/dist/routeTree.js +2 -0
- package/dist/routeTree.js.map +1 -1
- package/dist/routes/__root.d.ts +1 -1
- package/dist/routes/__root.d.ts.map +1 -1
- package/dist/routes/config.d.ts +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/fleet.d.ts +1 -1
- package/dist/routes/fleet.d.ts.map +1 -1
- package/dist/routes/index.d.ts +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/investigate-commit.d.ts +1 -1
- package/dist/routes/investigate-commit.d.ts.map +1 -1
- package/dist/routes/investigate-event.d.ts +1 -1
- package/dist/routes/investigate-event.d.ts.map +1 -1
- package/dist/routes/ops.d.ts +1 -1
- package/dist/routes/ops.d.ts.map +1 -1
- package/dist/routes/storage.d.ts +2 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +9 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/stream.d.ts +1 -1
- package/dist/routes/stream.d.ts.map +1 -1
- package/dist/static-server.d.ts.map +1 -1
- package/dist/static-server.js +6 -1
- package/dist/static-server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +9 -9
- package/src/App.tsx +12 -10
- package/src/__tests__/static-server.test.ts +193 -0
- package/src/hooks/ConnectionContext.tsx +135 -29
- package/src/hooks/useConsoleApi.ts +103 -0
- package/src/hooks/useLiveEvents.ts +142 -4
- package/src/layout.tsx +35 -5
- package/src/lib/api.ts +38 -5
- package/src/lib/types.ts +17 -0
- package/src/mount.tsx +6 -1
- package/src/pages/Config.tsx +57 -49
- package/src/pages/Fleet.tsx +19 -17
- package/src/pages/Storage.tsx +277 -0
- package/src/pages/Stream.tsx +6 -3
- package/src/pages/index.ts +1 -0
- package/src/routeTree.ts +2 -0
- package/src/routes/storage.tsx +9 -0
- package/src/static-server.ts +12 -1
- package/src/styles/globals.css +4 -1
- package/web-dist/assets/index-D8JLMM1I.js +86 -0
- package/web-dist/assets/index-D_fQabjS.css +1 -0
- package/web-dist/console.css +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/site.webmanifest +2 -2
- package/web-dist/assets/index-CTkQp6YC.js +0 -86
- package/web-dist/assets/index-j_U2SoXa.css +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
-
import type { LiveEvent } from '../lib/types';
|
|
6
|
+
import type { ConsoleRequestEvent, LiveEvent } from '../lib/types';
|
|
7
7
|
import { useConnection } from './ConnectionContext';
|
|
8
8
|
import { useInstanceContext } from './useInstanceContext';
|
|
9
9
|
|
|
@@ -35,6 +35,11 @@ interface UseLiveEventsResult {
|
|
|
35
35
|
clearEvents: () => void;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function isServiceWorkerServerMode(): boolean {
|
|
39
|
+
if (typeof window === 'undefined') return false;
|
|
40
|
+
return new URLSearchParams(window.location.search).get('swServer') === '1';
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
export function useLiveEvents(
|
|
39
44
|
options: UseLiveEventsOptions = {}
|
|
40
45
|
): UseLiveEventsResult {
|
|
@@ -65,10 +70,12 @@ export function useLiveEvents(
|
|
|
65
70
|
const reconnectAttemptsRef = useRef(0);
|
|
66
71
|
const lastActivityAtRef = useRef(0);
|
|
67
72
|
const lastEventTimestampRef = useRef<string | null>(null);
|
|
73
|
+
const lastEventIdRef = useRef<number | null>(null);
|
|
68
74
|
|
|
69
75
|
const clearEvents = useCallback(() => {
|
|
70
76
|
setEvents([]);
|
|
71
77
|
lastEventTimestampRef.current = null;
|
|
78
|
+
lastEventIdRef.current = null;
|
|
72
79
|
}, []);
|
|
73
80
|
|
|
74
81
|
useEffect(() => {
|
|
@@ -80,6 +87,113 @@ export function useLiveEvents(
|
|
|
80
87
|
const normalizedReplayLimit = Number.isFinite(replayLimit)
|
|
81
88
|
? Math.max(1, Math.min(500, Math.floor(replayLimit)))
|
|
82
89
|
: 100;
|
|
90
|
+
const usePollingFallback =
|
|
91
|
+
isServiceWorkerServerMode() || typeof WebSocket === 'undefined';
|
|
92
|
+
|
|
93
|
+
if (usePollingFallback) {
|
|
94
|
+
setConnectionState('connecting');
|
|
95
|
+
setIsConnected(false);
|
|
96
|
+
|
|
97
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
98
|
+
let isPolling = false;
|
|
99
|
+
|
|
100
|
+
const poll = async () => {
|
|
101
|
+
if (isCleanedUp || isPolling) return;
|
|
102
|
+
isPolling = true;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const baseUrl = new URL(config.serverUrl, window.location.origin);
|
|
106
|
+
const normalizedPath = baseUrl.pathname.endsWith('/')
|
|
107
|
+
? baseUrl.pathname.slice(0, -1)
|
|
108
|
+
: baseUrl.pathname;
|
|
109
|
+
baseUrl.pathname = `${normalizedPath}/console/events`;
|
|
110
|
+
baseUrl.search = '';
|
|
111
|
+
baseUrl.searchParams.set('limit', String(normalizedReplayLimit));
|
|
112
|
+
baseUrl.searchParams.set('offset', '0');
|
|
113
|
+
if (partitionId) {
|
|
114
|
+
baseUrl.searchParams.set('partitionId', partitionId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const response = await fetch(baseUrl.toString(), {
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${config.token}`,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`Live event polling failed (${response.status})`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const payload = (await response.json()) as {
|
|
128
|
+
items?: ConsoleRequestEvent[];
|
|
129
|
+
};
|
|
130
|
+
const rows = Array.isArray(payload.items) ? payload.items : [];
|
|
131
|
+
const filtered = rows
|
|
132
|
+
.filter((row) =>
|
|
133
|
+
effectiveInstanceId
|
|
134
|
+
? row.instanceId === effectiveInstanceId
|
|
135
|
+
: true
|
|
136
|
+
)
|
|
137
|
+
.sort((a, b) => a.eventId - b.eventId);
|
|
138
|
+
|
|
139
|
+
const previousLastId = lastEventIdRef.current ?? -1;
|
|
140
|
+
const newRows = filtered.filter(
|
|
141
|
+
(row) => row.eventId > previousLastId
|
|
142
|
+
);
|
|
143
|
+
if (newRows.length > 0) {
|
|
144
|
+
const mapped: LiveEvent[] = newRows
|
|
145
|
+
.map((row) => ({
|
|
146
|
+
type: row.eventType,
|
|
147
|
+
timestamp: row.createdAt,
|
|
148
|
+
data: row as unknown as Record<string, unknown>,
|
|
149
|
+
}))
|
|
150
|
+
.reverse();
|
|
151
|
+
|
|
152
|
+
setEvents((prev) => [...mapped, ...prev].slice(0, maxEvents));
|
|
153
|
+
const newest = newRows[newRows.length - 1]!;
|
|
154
|
+
lastEventIdRef.current = newest.eventId;
|
|
155
|
+
lastEventTimestampRef.current = newest.createdAt;
|
|
156
|
+
} else if (filtered.length > 0) {
|
|
157
|
+
const newest = filtered[filtered.length - 1]!;
|
|
158
|
+
lastEventIdRef.current = Math.max(
|
|
159
|
+
lastEventIdRef.current ?? -1,
|
|
160
|
+
newest.eventId
|
|
161
|
+
);
|
|
162
|
+
lastEventTimestampRef.current = newest.createdAt;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setError(null);
|
|
166
|
+
setIsConnected(true);
|
|
167
|
+
setConnectionState('connected');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (!isCleanedUp) {
|
|
170
|
+
setIsConnected(false);
|
|
171
|
+
setConnectionState('disconnected');
|
|
172
|
+
setError(
|
|
173
|
+
err instanceof Error
|
|
174
|
+
? err
|
|
175
|
+
: new Error('Live event polling failed')
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
isPolling = false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
void poll();
|
|
184
|
+
pollTimer = setInterval(() => {
|
|
185
|
+
void poll();
|
|
186
|
+
}, 2_000);
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
isCleanedUp = true;
|
|
190
|
+
if (pollTimer) {
|
|
191
|
+
clearInterval(pollTimer);
|
|
192
|
+
}
|
|
193
|
+
setIsConnected(false);
|
|
194
|
+
setConnectionState('disconnected');
|
|
195
|
+
};
|
|
196
|
+
}
|
|
83
197
|
|
|
84
198
|
const clearReconnectTimeout = () => {
|
|
85
199
|
if (!reconnectTimeoutRef.current) return;
|
|
@@ -129,7 +243,6 @@ export function useLiveEvents(
|
|
|
129
243
|
: baseUrl.pathname;
|
|
130
244
|
baseUrl.pathname = `${normalizedPath}/console/events/live`;
|
|
131
245
|
baseUrl.search = '';
|
|
132
|
-
baseUrl.searchParams.set('token', config.token);
|
|
133
246
|
if (lastEventTimestampRef.current) {
|
|
134
247
|
baseUrl.searchParams.set('since', lastEventTimestampRef.current);
|
|
135
248
|
}
|
|
@@ -152,8 +265,21 @@ export function useLiveEvents(
|
|
|
152
265
|
return;
|
|
153
266
|
}
|
|
154
267
|
reconnectAttemptsRef.current = 0;
|
|
155
|
-
markActivity();
|
|
156
268
|
setError(null);
|
|
269
|
+
setConnectionState('connecting');
|
|
270
|
+
setIsConnected(false);
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
ws.send(
|
|
274
|
+
JSON.stringify({
|
|
275
|
+
type: 'auth',
|
|
276
|
+
token: config.token,
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
} catch {
|
|
280
|
+
ws.close();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
157
283
|
|
|
158
284
|
clearStaleInterval();
|
|
159
285
|
staleCheckIntervalRef.current = setInterval(() => {
|
|
@@ -190,7 +316,19 @@ export function useLiveEvents(
|
|
|
190
316
|
markActivity();
|
|
191
317
|
|
|
192
318
|
// Skip control events
|
|
193
|
-
if (
|
|
319
|
+
if (
|
|
320
|
+
eventType === 'connected' ||
|
|
321
|
+
eventType === 'heartbeat' ||
|
|
322
|
+
eventType === 'auth_required'
|
|
323
|
+
) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (eventType === 'error') {
|
|
327
|
+
const message =
|
|
328
|
+
typeof data.message === 'string'
|
|
329
|
+
? data.message
|
|
330
|
+
: 'Live events authentication failed';
|
|
331
|
+
setError(new Error(message));
|
|
194
332
|
return;
|
|
195
333
|
}
|
|
196
334
|
|
package/src/layout.tsx
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Badge,
|
|
2
3
|
BottomBar,
|
|
3
4
|
Button,
|
|
4
5
|
ConnectionStatusBadge,
|
|
5
6
|
Input,
|
|
6
7
|
NavPill,
|
|
7
8
|
NavPillGroup,
|
|
9
|
+
navActionLinkClassName,
|
|
8
10
|
SyncularBrand,
|
|
9
11
|
TopNavigation,
|
|
10
12
|
} from '@syncular/ui';
|
|
11
13
|
import { Link, Outlet, useRouterState } from '@tanstack/react-router';
|
|
12
|
-
import { Settings } from 'lucide-react';
|
|
14
|
+
import { ArrowLeft, Settings } from 'lucide-react';
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
13
16
|
import { useMemo } from 'react';
|
|
14
17
|
import { useConnection } from './hooks/ConnectionContext';
|
|
15
18
|
import { useStats } from './hooks/useConsoleApi';
|
|
@@ -20,9 +23,17 @@ import { SYNCULAR_CONSOLE_ROOT_CLASS } from './theme-scope';
|
|
|
20
23
|
|
|
21
24
|
interface ConsoleLayoutProps {
|
|
22
25
|
basePath?: string;
|
|
26
|
+
appHref?: string;
|
|
27
|
+
modeBadge?: ReactNode;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
type ConsoleNavSuffix =
|
|
30
|
+
type ConsoleNavSuffix =
|
|
31
|
+
| ''
|
|
32
|
+
| '/stream'
|
|
33
|
+
| '/fleet'
|
|
34
|
+
| '/ops'
|
|
35
|
+
| '/storage'
|
|
36
|
+
| '/config';
|
|
26
37
|
|
|
27
38
|
interface ConsoleNavItem {
|
|
28
39
|
suffix: ConsoleNavSuffix;
|
|
@@ -34,6 +45,7 @@ const NAV_ITEMS: ConsoleNavItem[] = [
|
|
|
34
45
|
{ suffix: '/stream', label: 'Stream' },
|
|
35
46
|
{ suffix: '/fleet', label: 'Fleet' },
|
|
36
47
|
{ suffix: '/ops', label: 'Ops' },
|
|
48
|
+
{ suffix: '/storage', label: 'Storage' },
|
|
37
49
|
{ suffix: '/config', label: 'Config' },
|
|
38
50
|
];
|
|
39
51
|
|
|
@@ -49,7 +61,11 @@ function resolvePath(basePath: string, suffix: ConsoleNavSuffix): string {
|
|
|
49
61
|
return suffix ? `${basePath}${suffix}` : basePath;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
export function ConsoleLayout({
|
|
64
|
+
export function ConsoleLayout({
|
|
65
|
+
basePath,
|
|
66
|
+
appHref,
|
|
67
|
+
modeBadge,
|
|
68
|
+
}: ConsoleLayoutProps) {
|
|
53
69
|
const { connect, config, isConnected, isConnecting } = useConnection();
|
|
54
70
|
const { preferences } = usePreferences();
|
|
55
71
|
const { instanceId, rawInstanceId, setInstanceId, clearInstanceId } =
|
|
@@ -127,6 +143,14 @@ export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
|
|
|
127
143
|
}
|
|
128
144
|
right={
|
|
129
145
|
<div className="flex items-center gap-2">
|
|
146
|
+
{modeBadge ? (
|
|
147
|
+
<Badge
|
|
148
|
+
variant="flow"
|
|
149
|
+
className="hidden md:inline-flex px-2 py-1 text-[10px]"
|
|
150
|
+
>
|
|
151
|
+
{modeBadge}
|
|
152
|
+
</Badge>
|
|
153
|
+
) : null}
|
|
130
154
|
<div className="flex items-center gap-1">
|
|
131
155
|
<span className="font-mono text-[9px] text-neutral-500 uppercase tracking-wide">
|
|
132
156
|
Instance
|
|
@@ -179,14 +203,20 @@ export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
|
|
|
179
203
|
variant={pathname === configPath ? 'secondary' : 'ghost'}
|
|
180
204
|
size="icon"
|
|
181
205
|
>
|
|
182
|
-
<Settings />
|
|
206
|
+
<Settings className="h-3 w-3" />
|
|
183
207
|
</Button>
|
|
184
208
|
</Link>
|
|
209
|
+
{appHref ? (
|
|
210
|
+
<a href={appHref} className={navActionLinkClassName}>
|
|
211
|
+
<ArrowLeft className="h-3 w-3" />
|
|
212
|
+
Go to app
|
|
213
|
+
</a>
|
|
214
|
+
) : null}
|
|
185
215
|
</div>
|
|
186
216
|
}
|
|
187
217
|
/>
|
|
188
218
|
|
|
189
|
-
<main className="flex-1 overflow-auto
|
|
219
|
+
<main className="flex-1 overflow-auto pb-[32px]">
|
|
190
220
|
<div className="min-h-full">
|
|
191
221
|
{isConnected || pathname === configPath ? (
|
|
192
222
|
<div key={pathname} style={{ animation: 'pageIn 0.3s ease-out' }}>
|
package/src/lib/api.ts
CHANGED
|
@@ -16,11 +16,44 @@ export function createConsoleClient(config: ConnectionConfig): SyncClient {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function testConnection(client: SyncClient): Promise<
|
|
19
|
+
export async function testConnection(client: SyncClient): Promise<void> {
|
|
20
20
|
try {
|
|
21
|
-
const { error } = await client.GET('/console/stats');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const { error, response } = await client.GET('/console/stats');
|
|
22
|
+
if (!error) return;
|
|
23
|
+
|
|
24
|
+
const statusCode = response.status;
|
|
25
|
+
let detail: string | null = null;
|
|
26
|
+
if (typeof error === 'string') {
|
|
27
|
+
detail = error;
|
|
28
|
+
} else if (error && typeof error === 'object') {
|
|
29
|
+
const errorRecord = error as Record<string, unknown>;
|
|
30
|
+
const nestedError = errorRecord.error;
|
|
31
|
+
const nestedMessage = errorRecord.message;
|
|
32
|
+
if (typeof nestedError === 'string' && nestedError.length > 0) {
|
|
33
|
+
detail = nestedError;
|
|
34
|
+
} else if (
|
|
35
|
+
typeof nestedMessage === 'string' &&
|
|
36
|
+
nestedMessage.length > 0
|
|
37
|
+
) {
|
|
38
|
+
detail = nestedMessage;
|
|
39
|
+
} else {
|
|
40
|
+
try {
|
|
41
|
+
detail = JSON.stringify(errorRecord);
|
|
42
|
+
} catch {
|
|
43
|
+
detail = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
detail && detail.length > 0
|
|
50
|
+
? `Console API /console/stats returned ${statusCode}: ${detail}`
|
|
51
|
+
: `Console API /console/stats returned ${statusCode}`
|
|
52
|
+
);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Failed to connect to console API');
|
|
25
58
|
}
|
|
26
59
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -226,3 +226,20 @@ export interface LiveEvent {
|
|
|
226
226
|
timestamp: string;
|
|
227
227
|
data: Record<string, unknown>;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Blob storage
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
export interface ConsoleBlob {
|
|
235
|
+
key: string;
|
|
236
|
+
size: number;
|
|
237
|
+
uploaded: string;
|
|
238
|
+
httpMetadata?: { contentType?: string };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface ConsoleBlobListResponse {
|
|
242
|
+
items: ConsoleBlob[];
|
|
243
|
+
truncated: boolean;
|
|
244
|
+
cursor: string | null;
|
|
245
|
+
}
|
package/src/mount.tsx
CHANGED
|
@@ -7,6 +7,7 @@ interface MountSyncularConsoleOptions {
|
|
|
7
7
|
strictMode?: boolean;
|
|
8
8
|
basePath?: SyncularConsoleProps['basePath'];
|
|
9
9
|
defaultConfig?: SyncularConsoleProps['defaultConfig'];
|
|
10
|
+
autoConnect?: SyncularConsoleProps['autoConnect'];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function resolveContainer(containerOrSelector: Element | string): Element {
|
|
@@ -33,7 +34,11 @@ export function mountSyncularConsoleApp(
|
|
|
33
34
|
|
|
34
35
|
const root = createRoot(container);
|
|
35
36
|
const app = (
|
|
36
|
-
<App
|
|
37
|
+
<App
|
|
38
|
+
basePath={options.basePath}
|
|
39
|
+
defaultConfig={options.defaultConfig}
|
|
40
|
+
autoConnect={options.autoConnect}
|
|
41
|
+
/>
|
|
37
42
|
);
|
|
38
43
|
|
|
39
44
|
if (options.strictMode === false) {
|
package/src/pages/Config.tsx
CHANGED
|
@@ -48,12 +48,13 @@ import type {
|
|
|
48
48
|
ConsoleApiKeyBulkRevokeResponse,
|
|
49
49
|
} from '../lib/types';
|
|
50
50
|
|
|
51
|
-
export function Config() {
|
|
51
|
+
export function Config({ children }: { children?: import('react').ReactNode }) {
|
|
52
52
|
return (
|
|
53
53
|
<div className="space-y-4 px-5 py-5">
|
|
54
54
|
<ConnectionTab />
|
|
55
55
|
<ApiKeysTab />
|
|
56
56
|
<PreferencesTab />
|
|
57
|
+
{children}
|
|
57
58
|
</div>
|
|
58
59
|
);
|
|
59
60
|
}
|
|
@@ -79,15 +80,24 @@ function ConnectionTab() {
|
|
|
79
80
|
|
|
80
81
|
useEffect(() => {
|
|
81
82
|
const params = new URLSearchParams(window.location.search);
|
|
82
|
-
const urlToken = params.get('token');
|
|
83
83
|
const urlServer = params.get('server');
|
|
84
|
+
let shouldReplaceUrl = false;
|
|
84
85
|
|
|
85
|
-
if (urlToken) {
|
|
86
|
-
setToken(urlToken);
|
|
87
|
-
window.history.replaceState({}, '', window.location.pathname);
|
|
88
|
-
}
|
|
89
86
|
if (urlServer) {
|
|
90
87
|
setServerUrl(urlServer);
|
|
88
|
+
params.delete('server');
|
|
89
|
+
shouldReplaceUrl = true;
|
|
90
|
+
}
|
|
91
|
+
if (params.has('token')) {
|
|
92
|
+
params.delete('token');
|
|
93
|
+
shouldReplaceUrl = true;
|
|
94
|
+
}
|
|
95
|
+
if (shouldReplaceUrl) {
|
|
96
|
+
const nextQuery = params.toString();
|
|
97
|
+
const nextUrl = nextQuery
|
|
98
|
+
? `${window.location.pathname}?${nextQuery}`
|
|
99
|
+
: window.location.pathname;
|
|
100
|
+
window.history.replaceState({}, '', nextUrl);
|
|
91
101
|
}
|
|
92
102
|
}, []);
|
|
93
103
|
|
|
@@ -554,7 +564,7 @@ function ApiKeysTab() {
|
|
|
554
564
|
<Table>
|
|
555
565
|
<TableHeader>
|
|
556
566
|
<TableRow>
|
|
557
|
-
<TableHead>
|
|
567
|
+
<TableHead className="w-[28px]">
|
|
558
568
|
<Checkbox
|
|
559
569
|
checked={allSelectableChecked}
|
|
560
570
|
indeterminate={
|
|
@@ -572,16 +582,15 @@ function ApiKeysTab() {
|
|
|
572
582
|
aria-label="Select all active keys"
|
|
573
583
|
/>
|
|
574
584
|
</TableHead>
|
|
575
|
-
<TableHead>NAME</TableHead>
|
|
576
|
-
<TableHead>TYPE</TableHead>
|
|
577
|
-
<TableHead>KEY PREFIX</TableHead>
|
|
578
|
-
<TableHead>ACTOR</TableHead>
|
|
579
|
-
<TableHead>SCOPES</TableHead>
|
|
580
|
-
<TableHead>CREATED</TableHead>
|
|
581
|
-
<TableHead>LAST USED</TableHead>
|
|
582
|
-
<TableHead>EXPIRES</TableHead>
|
|
583
|
-
<TableHead>STATUS</TableHead>
|
|
584
|
-
<TableHead>ACTIONS</TableHead>
|
|
585
|
+
<TableHead className="w-[100px]">NAME</TableHead>
|
|
586
|
+
<TableHead className="w-[55px]">TYPE</TableHead>
|
|
587
|
+
<TableHead className="w-[90px]">KEY PREFIX</TableHead>
|
|
588
|
+
<TableHead className="w-[80px]">ACTOR</TableHead>
|
|
589
|
+
<TableHead className="w-[100px]">SCOPES</TableHead>
|
|
590
|
+
<TableHead className="w-[120px]">CREATED</TableHead>
|
|
591
|
+
<TableHead className="w-[120px]">LAST USED</TableHead>
|
|
592
|
+
<TableHead className="w-[120px]">EXPIRES</TableHead>
|
|
593
|
+
<TableHead className="flex-1">STATUS</TableHead>
|
|
585
594
|
</TableRow>
|
|
586
595
|
</TableHeader>
|
|
587
596
|
<TableBody>
|
|
@@ -592,8 +601,8 @@ function ApiKeysTab() {
|
|
|
592
601
|
);
|
|
593
602
|
|
|
594
603
|
return (
|
|
595
|
-
<TableRow key={apiKey.keyId}>
|
|
596
|
-
<TableCell>
|
|
604
|
+
<TableRow key={apiKey.keyId} className="group relative">
|
|
605
|
+
<TableCell className="w-[28px]">
|
|
597
606
|
<Checkbox
|
|
598
607
|
checked={selectedKeyIds.includes(apiKey.keyId)}
|
|
599
608
|
onCheckedChange={(checked) => {
|
|
@@ -609,8 +618,10 @@ function ApiKeysTab() {
|
|
|
609
618
|
disabled={apiKey.revokedAt !== null}
|
|
610
619
|
/>
|
|
611
620
|
</TableCell>
|
|
612
|
-
<TableCell className="font-medium">
|
|
613
|
-
|
|
621
|
+
<TableCell className="w-[100px] font-medium">
|
|
622
|
+
{apiKey.name}
|
|
623
|
+
</TableCell>
|
|
624
|
+
<TableCell className="w-[55px]">
|
|
614
625
|
<Badge
|
|
615
626
|
variant={
|
|
616
627
|
apiKey.keyType === 'admin'
|
|
@@ -623,63 +634,60 @@ function ApiKeysTab() {
|
|
|
623
634
|
{apiKey.keyType}
|
|
624
635
|
</Badge>
|
|
625
636
|
</TableCell>
|
|
626
|
-
<TableCell>
|
|
637
|
+
<TableCell className="w-[90px]">
|
|
627
638
|
<code className="font-mono text-[11px]">
|
|
628
639
|
{apiKey.keyPrefix}...
|
|
629
640
|
</code>
|
|
630
641
|
</TableCell>
|
|
631
|
-
<TableCell className="text-neutral-500">
|
|
642
|
+
<TableCell className="w-[80px] text-neutral-500">
|
|
632
643
|
{apiKey.actorId ?? '-'}
|
|
633
644
|
</TableCell>
|
|
634
|
-
<TableCell className="
|
|
645
|
+
<TableCell className="w-[100px] text-neutral-500">
|
|
635
646
|
<code className="font-mono text-[10px]">
|
|
636
647
|
{summarizeScopeKeys(apiKey.scopeKeys)}
|
|
637
648
|
</code>
|
|
638
649
|
</TableCell>
|
|
639
|
-
<TableCell className="text-neutral-500">
|
|
650
|
+
<TableCell className="w-[120px] text-neutral-500">
|
|
640
651
|
{formatOptionalDateTime(apiKey.createdAt)}
|
|
641
652
|
</TableCell>
|
|
642
|
-
<TableCell className="text-neutral-500">
|
|
653
|
+
<TableCell className="w-[120px] text-neutral-500">
|
|
643
654
|
{formatOptionalDateTime(apiKey.lastUsedAt)}
|
|
644
655
|
</TableCell>
|
|
645
|
-
<TableCell className="text-neutral-500">
|
|
656
|
+
<TableCell className="w-[120px] text-neutral-500">
|
|
646
657
|
{formatOptionalDateTime(apiKey.expiresAt)}
|
|
647
658
|
</TableCell>
|
|
648
|
-
<TableCell>
|
|
659
|
+
<TableCell className="flex-1">
|
|
649
660
|
<Badge
|
|
650
661
|
variant={getApiKeyStatusBadgeVariant(lifecycleStatus)}
|
|
651
662
|
>
|
|
652
663
|
{lifecycleStatus}
|
|
653
664
|
</Badge>
|
|
654
665
|
</TableCell>
|
|
655
|
-
|
|
656
|
-
<div className="flex items-center gap-1">
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
size="sm"
|
|
666
|
+
{apiKey.revokedAt === null && (
|
|
667
|
+
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
668
|
+
<button
|
|
669
|
+
type="button"
|
|
660
670
|
onClick={() => setStagingRotateKey(apiKey)}
|
|
661
|
-
|
|
671
|
+
className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-white hover:bg-white/[0.05] cursor-pointer transition-colors"
|
|
662
672
|
>
|
|
663
|
-
|
|
664
|
-
</
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
size="sm"
|
|
673
|
+
stage
|
|
674
|
+
</button>
|
|
675
|
+
<button
|
|
676
|
+
type="button"
|
|
668
677
|
onClick={() => setRotatingKeyId(apiKey.keyId)}
|
|
669
|
-
|
|
678
|
+
className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-white hover:bg-white/[0.05] cursor-pointer transition-colors"
|
|
670
679
|
>
|
|
671
|
-
|
|
672
|
-
</
|
|
673
|
-
<
|
|
674
|
-
|
|
675
|
-
size="sm"
|
|
680
|
+
rotate
|
|
681
|
+
</button>
|
|
682
|
+
<button
|
|
683
|
+
type="button"
|
|
676
684
|
onClick={() => setRevokingKeyId(apiKey.keyId)}
|
|
677
|
-
|
|
685
|
+
className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-offline hover:bg-offline/10 cursor-pointer transition-colors"
|
|
678
686
|
>
|
|
679
|
-
|
|
680
|
-
</
|
|
687
|
+
revoke
|
|
688
|
+
</button>
|
|
681
689
|
</div>
|
|
682
|
-
|
|
690
|
+
)}
|
|
683
691
|
</TableRow>
|
|
684
692
|
);
|
|
685
693
|
})}
|
package/src/pages/Fleet.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
DialogHeader,
|
|
8
8
|
DialogTitle,
|
|
9
9
|
EmptyState,
|
|
10
|
-
|
|
10
|
+
FleetTable,
|
|
11
11
|
Pagination,
|
|
12
12
|
PanelShell,
|
|
13
13
|
Spinner,
|
|
@@ -99,7 +99,11 @@ function mapToSyncNode(
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export function Fleet(
|
|
102
|
+
export function Fleet({
|
|
103
|
+
emptyState,
|
|
104
|
+
}: {
|
|
105
|
+
emptyState?: import('react').ReactNode;
|
|
106
|
+
} = {}) {
|
|
103
107
|
const [page, setPage] = useState(1);
|
|
104
108
|
const [evictingClientId, setEvictingClientId] = useState<string | null>(null);
|
|
105
109
|
const { preferences } = usePreferences();
|
|
@@ -167,22 +171,20 @@ export function Fleet() {
|
|
|
167
171
|
)}
|
|
168
172
|
|
|
169
173
|
{syncNodes.length === 0 ? (
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
174
|
+
(emptyState ?? (
|
|
175
|
+
<PanelShell>
|
|
176
|
+
<EmptyState message="No clients yet" />
|
|
177
|
+
</PanelShell>
|
|
178
|
+
))
|
|
173
179
|
) : (
|
|
174
|
-
<
|
|
175
|
-
{syncNodes
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
/>
|
|
184
|
-
))}
|
|
185
|
-
</div>
|
|
180
|
+
<FleetTable
|
|
181
|
+
clients={syncNodes}
|
|
182
|
+
headSeq={headSeq}
|
|
183
|
+
onEvict={(clientId) => {
|
|
184
|
+
const item = data?.items.find((c) => c.clientId === clientId);
|
|
185
|
+
setEvictingClientId(item?.clientId ?? clientId);
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
186
188
|
)}
|
|
187
189
|
|
|
188
190
|
{totalPages > 1 && (
|