@syncular/console 0.0.4-25 → 0.0.5-42
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 +1 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +2 -1
- package/dist/App.js.map +1 -1
- package/dist/browser-main.js +1 -1
- package/dist/browser-main.js.map +1 -1
- package/dist/hooks/ConnectionContext.d.ts.map +1 -1
- package/dist/hooks/ConnectionContext.js +5 -14
- 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 +94 -0
- package/dist/hooks/useLiveEvents.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +3 -1
- 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.map +1 -1
- package/dist/mount.js +4 -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 +2 -3
- 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 +2 -2
- 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/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/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/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -0
- package/dist/static-server.d.ts +18 -0
- package/dist/static-server.d.ts.map +1 -0
- package/dist/static-server.js +137 -0
- package/dist/static-server.js.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/theme-scope.d.ts +3 -0
- package/dist/theme-scope.d.ts.map +1 -0
- package/dist/theme-scope.js +5 -0
- package/dist/theme-scope.js.map +1 -0
- package/package.json +20 -9
- package/src/App.tsx +6 -1
- package/src/browser-main.tsx +1 -1
- package/src/hooks/ConnectionContext.tsx +5 -15
- package/src/hooks/useConsoleApi.ts +103 -0
- package/src/hooks/useLiveEvents.ts +115 -1
- package/src/index.ts +1 -0
- package/src/layout.tsx +12 -2
- package/src/lib/api.ts +38 -5
- package/src/lib/types.ts +17 -0
- package/src/mount.tsx +5 -1
- package/src/pages/Config.tsx +2 -1
- package/src/pages/Fleet.tsx +19 -17
- package/src/pages/Ops.tsx +4 -2
- package/src/pages/Storage.tsx +277 -0
- package/src/pages/index.ts +1 -0
- package/src/routeTree.ts +2 -0
- package/src/routes/storage.tsx +9 -0
- package/src/server.ts +2 -0
- package/src/static-server.ts +219 -0
- package/src/styles/globals.css +8 -1
- package/src/theme-scope.ts +5 -0
- package/web-dist/assets/index-BhPtRvK0.css +1 -0
- package/web-dist/assets/index-Fyq7dTrO.js +86 -0
- package/web-dist/console.css +1 -0
- package/web-dist/index.html +4 -4
- package/web-dist/chunk-7ayekhzx.css +0 -1
- package/web-dist/chunk-myppbvt5.js +0 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/console",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5-42",
|
|
4
4
|
"description": "Embeddable Syncular console UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,9 +48,18 @@
|
|
|
48
48
|
"default": "./dist/browser-main.js"
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
|
+
"./server": {
|
|
52
|
+
"bun": "./src/server.ts",
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./dist/server.d.ts",
|
|
55
|
+
"default": "./dist/server.js"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
51
58
|
"./static": "./web-dist/index.html",
|
|
52
59
|
"./static/*": "./web-dist/*",
|
|
53
|
-
"./styles.css": "./
|
|
60
|
+
"./styles.css": "./dist/styles.css",
|
|
61
|
+
"./styles.source.css": "./src/styles/globals.css",
|
|
62
|
+
"./static/console.css": "./web-dist/console.css"
|
|
54
63
|
},
|
|
55
64
|
"scripts": {
|
|
56
65
|
"tsgo": "tsgo --noEmit",
|
|
@@ -59,12 +68,12 @@
|
|
|
59
68
|
"release": "bunx syncular-publish"
|
|
60
69
|
},
|
|
61
70
|
"dependencies": {
|
|
62
|
-
"@syncular/observability-sentry": "0.0.
|
|
63
|
-
"@syncular/transport-http": "0.0.
|
|
64
|
-
"@syncular/ui": "0.0.
|
|
71
|
+
"@syncular/observability-sentry": "0.0.5-42",
|
|
72
|
+
"@syncular/transport-http": "0.0.5-42",
|
|
73
|
+
"@syncular/ui": "0.0.5-42",
|
|
65
74
|
"@tanstack/react-query": "^5.90.21",
|
|
66
|
-
"@tanstack/react-router": "^1.
|
|
67
|
-
"lucide-react": "^0.
|
|
75
|
+
"@tanstack/react-router": "^1.161.3",
|
|
76
|
+
"lucide-react": "^0.575.0"
|
|
68
77
|
},
|
|
69
78
|
"peerDependencies": {
|
|
70
79
|
"react": "^19.0.0",
|
|
@@ -72,12 +81,14 @@
|
|
|
72
81
|
},
|
|
73
82
|
"devDependencies": {
|
|
74
83
|
"@syncular/config": "0.0.0",
|
|
84
|
+
"@tailwindcss/vite": "^4.2.0",
|
|
75
85
|
"@types/react": "^19",
|
|
76
86
|
"@types/react-dom": "^19",
|
|
77
|
-
"
|
|
87
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
78
88
|
"react": "^19.2.4",
|
|
79
89
|
"react-dom": "^19.2.4",
|
|
80
|
-
"tailwindcss": "^4.
|
|
90
|
+
"tailwindcss": "^4.2.0",
|
|
91
|
+
"vite": "^7.3.1"
|
|
81
92
|
},
|
|
82
93
|
"files": [
|
|
83
94
|
"dist",
|
package/src/App.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveConsoleBasePathFromMeta,
|
|
10
10
|
resolveConsoleConnectionConfigFromMeta,
|
|
11
11
|
} from './runtime-config';
|
|
12
|
+
import { SYNCULAR_CONSOLE_ROOT_CLASS } from './theme-scope';
|
|
12
13
|
|
|
13
14
|
const routerForTypes = createRouter({ routeTree, basepath: '/' });
|
|
14
15
|
|
|
@@ -61,5 +62,9 @@ function SyncularConsole(props: SyncularConsoleProps) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function App(props: SyncularConsoleProps) {
|
|
64
|
-
return
|
|
65
|
+
return (
|
|
66
|
+
<div className={SYNCULAR_CONSOLE_ROOT_CLASS}>
|
|
67
|
+
<SyncularConsole {...props} />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
65
70
|
}
|
package/src/browser-main.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { initAndConfigureBrowserSentry } from '@syncular/observability-sentry';
|
|
1
|
+
import { initAndConfigureBrowserSentry } from '@syncular/observability-sentry/browser';
|
|
2
2
|
import { mountSyncularConsoleApp } from './mount';
|
|
3
3
|
import { resolveConsoleBrowserSentryOptions } from './sentry';
|
|
4
4
|
|
|
@@ -113,24 +113,14 @@ export function ConnectionProvider({
|
|
|
113
113
|
|
|
114
114
|
try {
|
|
115
115
|
const client = createConsoleClient(normalizedConfig);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (ok) {
|
|
119
|
-
setState({
|
|
120
|
-
isConnected: true,
|
|
121
|
-
isConnecting: false,
|
|
122
|
-
client,
|
|
123
|
-
error: null,
|
|
124
|
-
});
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
116
|
+
await testConnection(client);
|
|
127
117
|
setState({
|
|
128
|
-
isConnected:
|
|
118
|
+
isConnected: true,
|
|
129
119
|
isConnecting: false,
|
|
130
|
-
client
|
|
131
|
-
error:
|
|
120
|
+
client,
|
|
121
|
+
error: null,
|
|
132
122
|
});
|
|
133
|
-
return
|
|
123
|
+
return true;
|
|
134
124
|
} catch (err) {
|
|
135
125
|
setState({
|
|
136
126
|
isConnected: false,
|
|
@@ -7,6 +7,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
|
7
7
|
import type {
|
|
8
8
|
ConsoleApiKey,
|
|
9
9
|
ConsoleApiKeyBulkRevokeResponse,
|
|
10
|
+
ConsoleBlobListResponse,
|
|
10
11
|
ConsoleClient,
|
|
11
12
|
ConsoleCommitDetail,
|
|
12
13
|
ConsoleCommitListItem,
|
|
@@ -104,6 +105,8 @@ const queryKeys = {
|
|
|
104
105
|
expiresWithinDays?: number;
|
|
105
106
|
instanceId?: string;
|
|
106
107
|
}) => ['console', 'api-keys', params] as const,
|
|
108
|
+
storage: (params?: Record<string, unknown>) =>
|
|
109
|
+
['console', 'storage', params] as const,
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
function resolveRefetchInterval(
|
|
@@ -924,3 +927,103 @@ export function useStageRotateApiKeyMutation() {
|
|
|
924
927
|
},
|
|
925
928
|
});
|
|
926
929
|
}
|
|
930
|
+
|
|
931
|
+
// ---------------------------------------------------------------------------
|
|
932
|
+
// Blob storage hooks
|
|
933
|
+
// ---------------------------------------------------------------------------
|
|
934
|
+
|
|
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>({
|
|
945
|
+
queryKey: queryKeys.storage({
|
|
946
|
+
prefix: options.prefix,
|
|
947
|
+
cursor: options.cursor,
|
|
948
|
+
limit: options.limit,
|
|
949
|
+
}),
|
|
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();
|
|
969
|
+
},
|
|
970
|
+
enabled: !!connectionConfig,
|
|
971
|
+
refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
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
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export function useBlobDownload() {
|
|
1003
|
+
const { config: connectionConfig } = useConnection();
|
|
1004
|
+
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();
|
|
1020
|
+
const url = URL.createObjectURL(blob);
|
|
1021
|
+
const a = document.createElement('a');
|
|
1022
|
+
a.href = url;
|
|
1023
|
+
a.download = key.split('/').pop() || key;
|
|
1024
|
+
document.body.appendChild(a);
|
|
1025
|
+
a.click();
|
|
1026
|
+
document.body.removeChild(a);
|
|
1027
|
+
URL.revokeObjectURL(url);
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
@@ -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;
|
package/src/index.ts
CHANGED
package/src/layout.tsx
CHANGED
|
@@ -16,12 +16,19 @@ import { useStats } from './hooks/useConsoleApi';
|
|
|
16
16
|
import { useInstanceContext } from './hooks/useInstanceContext';
|
|
17
17
|
import { usePartitionContext } from './hooks/usePartitionContext';
|
|
18
18
|
import { usePreferences } from './hooks/usePreferences';
|
|
19
|
+
import { SYNCULAR_CONSOLE_ROOT_CLASS } from './theme-scope';
|
|
19
20
|
|
|
20
21
|
interface ConsoleLayoutProps {
|
|
21
22
|
basePath?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
type ConsoleNavSuffix =
|
|
25
|
+
type ConsoleNavSuffix =
|
|
26
|
+
| ''
|
|
27
|
+
| '/stream'
|
|
28
|
+
| '/fleet'
|
|
29
|
+
| '/ops'
|
|
30
|
+
| '/storage'
|
|
31
|
+
| '/config';
|
|
25
32
|
|
|
26
33
|
interface ConsoleNavItem {
|
|
27
34
|
suffix: ConsoleNavSuffix;
|
|
@@ -33,6 +40,7 @@ const NAV_ITEMS: ConsoleNavItem[] = [
|
|
|
33
40
|
{ suffix: '/stream', label: 'Stream' },
|
|
34
41
|
{ suffix: '/fleet', label: 'Fleet' },
|
|
35
42
|
{ suffix: '/ops', label: 'Ops' },
|
|
43
|
+
{ suffix: '/storage', label: 'Storage' },
|
|
36
44
|
{ suffix: '/config', label: 'Config' },
|
|
37
45
|
];
|
|
38
46
|
|
|
@@ -104,7 +112,9 @@ export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
|
|
|
104
112
|
: [];
|
|
105
113
|
|
|
106
114
|
return (
|
|
107
|
-
<div
|
|
115
|
+
<div
|
|
116
|
+
className={`${SYNCULAR_CONSOLE_ROOT_CLASS} h-screen bg-background text-foreground flex flex-col`}
|
|
117
|
+
>
|
|
108
118
|
<TopNavigation
|
|
109
119
|
brand={
|
|
110
120
|
<Link to={commandPath}>
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot, type Root } from 'react-dom/client';
|
|
3
3
|
import { App, type SyncularConsoleProps } from './App';
|
|
4
|
+
import { applyConsoleThemeScope } from './theme-scope';
|
|
4
5
|
|
|
5
6
|
interface MountSyncularConsoleOptions {
|
|
6
7
|
strictMode?: boolean;
|
|
@@ -27,7 +28,10 @@ export function mountSyncularConsoleApp(
|
|
|
27
28
|
containerOrSelector: Element | string,
|
|
28
29
|
options: MountSyncularConsoleOptions = {}
|
|
29
30
|
): Root {
|
|
30
|
-
const
|
|
31
|
+
const container = resolveContainer(containerOrSelector);
|
|
32
|
+
applyConsoleThemeScope(container);
|
|
33
|
+
|
|
34
|
+
const root = createRoot(container);
|
|
31
35
|
const app = (
|
|
32
36
|
<App basePath={options.basePath} defaultConfig={options.defaultConfig} />
|
|
33
37
|
);
|
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
|
}
|
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 && (
|
package/src/pages/Ops.tsx
CHANGED
|
@@ -684,8 +684,10 @@ function OperationsAuditView({ partitionId }: { partitionId?: string }) {
|
|
|
684
684
|
</TableRow>
|
|
685
685
|
</TableHeader>
|
|
686
686
|
<TableBody>
|
|
687
|
-
{(data?.items ?? []).map((event) => (
|
|
688
|
-
<TableRow
|
|
687
|
+
{(data?.items ?? []).map((event, index) => (
|
|
688
|
+
<TableRow
|
|
689
|
+
key={`${event.operationId}:${event.createdAt}:${index}`}
|
|
690
|
+
>
|
|
689
691
|
<TableCell className="whitespace-nowrap text-xs text-neutral-400">
|
|
690
692
|
{formatDateTime(event.createdAt)}
|
|
691
693
|
</TableCell>
|