@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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
CONSOLE_BASEPATH_META,
|
|
7
|
+
CONSOLE_SERVER_URL_META,
|
|
8
|
+
CONSOLE_TOKEN_META,
|
|
9
|
+
} from '../runtime-config';
|
|
10
|
+
import { createConsoleStaticResponder } from '../static-server';
|
|
11
|
+
|
|
12
|
+
const tempDirs: string[] = [];
|
|
13
|
+
|
|
14
|
+
function countMatches(value: string, pattern: RegExp): number {
|
|
15
|
+
return value.match(pattern)?.length ?? 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function createStaticFixture(): Promise<string> {
|
|
19
|
+
const staticDir = await mkdtemp(path.join(tmpdir(), 'syncular-console-'));
|
|
20
|
+
tempDirs.push(staticDir);
|
|
21
|
+
|
|
22
|
+
await mkdir(path.join(staticDir, 'assets'), { recursive: true });
|
|
23
|
+
await writeFile(
|
|
24
|
+
path.join(staticDir, 'index.html'),
|
|
25
|
+
`<!doctype html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<meta name="${CONSOLE_BASEPATH_META}" content="/old-basepath" />
|
|
29
|
+
<meta name="${CONSOLE_SERVER_URL_META}" content="https://old.example/api" />
|
|
30
|
+
<meta name="${CONSOLE_TOKEN_META}" content="old-token" />
|
|
31
|
+
<link rel="stylesheet" href="/assets/console.css" />
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<script type="module" src="/assets/main.js"></script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>`
|
|
37
|
+
);
|
|
38
|
+
await writeFile(
|
|
39
|
+
path.join(staticDir, 'assets', 'main.js'),
|
|
40
|
+
'console.log(1);\n'
|
|
41
|
+
);
|
|
42
|
+
await writeFile(path.join(staticDir, 'assets', 'console.css'), 'body{}\n');
|
|
43
|
+
|
|
44
|
+
return staticDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await Promise.all(
|
|
49
|
+
tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('createConsoleStaticResponder', () => {
|
|
54
|
+
it('serves index with prefilled meta tags and rewrites rooted asset paths', async () => {
|
|
55
|
+
const staticDir = await createStaticFixture();
|
|
56
|
+
const responder = createConsoleStaticResponder({
|
|
57
|
+
mountPath: '/console',
|
|
58
|
+
staticDir,
|
|
59
|
+
defaultPrefill: {
|
|
60
|
+
serverUrl: 'https://api.example.com',
|
|
61
|
+
token: 'default-token',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const response = await responder(new Request('http://localhost/console'));
|
|
66
|
+
if (!response) {
|
|
67
|
+
throw new Error('Expected console index response.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const html = await response.text();
|
|
71
|
+
expect(response.status).toBe(200);
|
|
72
|
+
expect(response.headers.get('cache-control')).toBe('no-store');
|
|
73
|
+
expect(html).toContain(
|
|
74
|
+
`<meta name="${CONSOLE_BASEPATH_META}" content="/console" />`
|
|
75
|
+
);
|
|
76
|
+
expect(html).toContain(
|
|
77
|
+
`<meta name="${CONSOLE_SERVER_URL_META}" content="https://api.example.com" />`
|
|
78
|
+
);
|
|
79
|
+
expect(html).toContain(
|
|
80
|
+
`<meta name="${CONSOLE_TOKEN_META}" content="default-token" />`
|
|
81
|
+
);
|
|
82
|
+
expect(html).toContain('href="/console/assets/console.css"');
|
|
83
|
+
expect(html).toContain('src="/console/assets/main.js"');
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
countMatches(html, new RegExp(`name="${CONSOLE_BASEPATH_META}"`, 'g'))
|
|
87
|
+
).toBe(1);
|
|
88
|
+
expect(
|
|
89
|
+
countMatches(html, new RegExp(`name="${CONSOLE_SERVER_URL_META}"`, 'g'))
|
|
90
|
+
).toBe(1);
|
|
91
|
+
expect(
|
|
92
|
+
countMatches(html, new RegExp(`name="${CONSOLE_TOKEN_META}"`, 'g'))
|
|
93
|
+
).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('applies request-level prefill overrides when serving index', async () => {
|
|
97
|
+
const staticDir = await createStaticFixture();
|
|
98
|
+
const responder = createConsoleStaticResponder({
|
|
99
|
+
mountPath: '/console',
|
|
100
|
+
staticDir,
|
|
101
|
+
defaultPrefill: {
|
|
102
|
+
basePath: '/console',
|
|
103
|
+
serverUrl: 'https://default.example/api',
|
|
104
|
+
token: 'default-token',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const response = await responder(
|
|
109
|
+
new Request('http://localhost/console/app'),
|
|
110
|
+
{
|
|
111
|
+
prefill: {
|
|
112
|
+
basePath: '/ops',
|
|
113
|
+
serverUrl: 'https://ops.example/api',
|
|
114
|
+
token: 'request-token',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
if (!response) {
|
|
119
|
+
throw new Error('Expected console index response.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const html = await response.text();
|
|
123
|
+
expect(response.status).toBe(200);
|
|
124
|
+
expect(html).toContain(
|
|
125
|
+
`<meta name="${CONSOLE_BASEPATH_META}" content="/ops" />`
|
|
126
|
+
);
|
|
127
|
+
expect(html).toContain(
|
|
128
|
+
`<meta name="${CONSOLE_SERVER_URL_META}" content="https://ops.example/api" />`
|
|
129
|
+
);
|
|
130
|
+
expect(html).toContain(
|
|
131
|
+
`<meta name="${CONSOLE_TOKEN_META}" content="request-token" />`
|
|
132
|
+
);
|
|
133
|
+
expect(html).toContain('href="/ops/assets/console.css"');
|
|
134
|
+
expect(html).toContain('src="/ops/assets/main.js"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('serves static assets, blocks traversal, and handles missing assets', async () => {
|
|
138
|
+
const staticDir = await createStaticFixture();
|
|
139
|
+
const responder = createConsoleStaticResponder({
|
|
140
|
+
mountPath: '/console',
|
|
141
|
+
staticDir,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const assetResponse = await responder(
|
|
145
|
+
new Request('http://localhost/console/assets/main.js')
|
|
146
|
+
);
|
|
147
|
+
if (!assetResponse) {
|
|
148
|
+
throw new Error('Expected static asset response.');
|
|
149
|
+
}
|
|
150
|
+
expect(assetResponse.status).toBe(200);
|
|
151
|
+
expect(assetResponse.headers.get('content-type')).toBe(
|
|
152
|
+
'text/javascript; charset=utf-8'
|
|
153
|
+
);
|
|
154
|
+
expect(assetResponse.headers.get('cache-control')).toBe(
|
|
155
|
+
'public, max-age=31536000, immutable'
|
|
156
|
+
);
|
|
157
|
+
expect(await assetResponse.text()).toContain('console.log(1);');
|
|
158
|
+
|
|
159
|
+
const forbidden = await responder(
|
|
160
|
+
new Request('http://localhost/console/%2e%2e%2fsecret.js')
|
|
161
|
+
);
|
|
162
|
+
if (!forbidden) {
|
|
163
|
+
throw new Error('Expected forbidden response.');
|
|
164
|
+
}
|
|
165
|
+
expect(forbidden.status).toBe(403);
|
|
166
|
+
|
|
167
|
+
const missing = await responder(
|
|
168
|
+
new Request('http://localhost/console/assets/missing.js')
|
|
169
|
+
);
|
|
170
|
+
if (!missing) {
|
|
171
|
+
throw new Error('Expected missing response.');
|
|
172
|
+
}
|
|
173
|
+
expect(missing.status).toBe(404);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns null for unsupported methods and non-matching mount paths', async () => {
|
|
177
|
+
const staticDir = await createStaticFixture();
|
|
178
|
+
const responder = createConsoleStaticResponder({
|
|
179
|
+
mountPath: '/console',
|
|
180
|
+
staticDir,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const postResponse = await responder(
|
|
184
|
+
new Request('http://localhost/console', { method: 'POST' })
|
|
185
|
+
);
|
|
186
|
+
const otherPathResponse = await responder(
|
|
187
|
+
new Request('http://localhost/admin')
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(postResponse).toBeNull();
|
|
191
|
+
expect(otherPathResponse).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useContext,
|
|
11
11
|
useEffect,
|
|
12
12
|
useMemo,
|
|
13
|
+
useRef,
|
|
13
14
|
useState,
|
|
14
15
|
} from 'react';
|
|
15
16
|
import {
|
|
@@ -17,7 +18,58 @@ import {
|
|
|
17
18
|
createConsoleClient,
|
|
18
19
|
testConnection,
|
|
19
20
|
} from '../lib/api';
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
export type ConnectionStorageMode = 'memory' | 'session' | 'local';
|
|
23
|
+
|
|
24
|
+
const CONNECTION_STORAGE_KEY = 'sync-console-connection';
|
|
25
|
+
|
|
26
|
+
function normalizeConfig(
|
|
27
|
+
config: ConnectionConfig | null | undefined
|
|
28
|
+
): ConnectionConfig | null {
|
|
29
|
+
if (!config) return null;
|
|
30
|
+
const serverUrl = config.serverUrl?.trim() ?? '';
|
|
31
|
+
const token = config.token?.trim() ?? '';
|
|
32
|
+
if (!serverUrl || !token) return null;
|
|
33
|
+
return { serverUrl, token };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getStorageForMode(mode: ConnectionStorageMode): Storage | null {
|
|
37
|
+
if (typeof window === 'undefined') return null;
|
|
38
|
+
if (mode === 'local') return window.localStorage;
|
|
39
|
+
if (mode === 'session') return window.sessionStorage;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readStoredConfig(
|
|
44
|
+
mode: ConnectionStorageMode
|
|
45
|
+
): ConnectionConfig | null {
|
|
46
|
+
const storage = getStorageForMode(mode);
|
|
47
|
+
if (!storage) return null;
|
|
48
|
+
try {
|
|
49
|
+
const raw = storage.getItem(CONNECTION_STORAGE_KEY);
|
|
50
|
+
if (!raw) return null;
|
|
51
|
+
return normalizeConfig(JSON.parse(raw) as ConnectionConfig);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeStoredConfig(
|
|
58
|
+
mode: ConnectionStorageMode,
|
|
59
|
+
config: ConnectionConfig | null
|
|
60
|
+
): void {
|
|
61
|
+
const storage = getStorageForMode(mode);
|
|
62
|
+
if (!storage) return;
|
|
63
|
+
try {
|
|
64
|
+
if (!config) {
|
|
65
|
+
storage.removeItem(CONNECTION_STORAGE_KEY);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
storage.setItem(CONNECTION_STORAGE_KEY, JSON.stringify(config));
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore storage write errors.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
21
73
|
|
|
22
74
|
interface ConnectionState {
|
|
23
75
|
isConnected: boolean;
|
|
@@ -44,6 +96,8 @@ interface ConnectionContextValue {
|
|
|
44
96
|
interface ConnectionProviderProps {
|
|
45
97
|
children: ReactNode;
|
|
46
98
|
defaultConfig?: ConnectionConfig | null;
|
|
99
|
+
autoConnect?: boolean;
|
|
100
|
+
storageMode?: ConnectionStorageMode;
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
const ConnectionContext = createContext<ConnectionContextValue | null>(null);
|
|
@@ -51,10 +105,35 @@ const ConnectionContext = createContext<ConnectionContextValue | null>(null);
|
|
|
51
105
|
export function ConnectionProvider({
|
|
52
106
|
children,
|
|
53
107
|
defaultConfig = null,
|
|
108
|
+
autoConnect = false,
|
|
109
|
+
storageMode = 'session',
|
|
54
110
|
}: ConnectionProviderProps) {
|
|
55
|
-
const [config,
|
|
56
|
-
|
|
57
|
-
|
|
111
|
+
const [config, setConfigState] = useState<ConnectionConfig | null>(() =>
|
|
112
|
+
readStoredConfig(storageMode)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const storedConfig = readStoredConfig(storageMode);
|
|
117
|
+
setConfigState(storedConfig);
|
|
118
|
+
}, [storageMode]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (typeof window === 'undefined') return;
|
|
122
|
+
if (storageMode !== 'local') {
|
|
123
|
+
window.localStorage.removeItem(CONNECTION_STORAGE_KEY);
|
|
124
|
+
}
|
|
125
|
+
if (storageMode === 'memory') {
|
|
126
|
+
window.sessionStorage.removeItem(CONNECTION_STORAGE_KEY);
|
|
127
|
+
}
|
|
128
|
+
}, [storageMode]);
|
|
129
|
+
|
|
130
|
+
const setConfigStorage = useCallback(
|
|
131
|
+
(nextConfig: ConnectionConfig | null) => {
|
|
132
|
+
const normalized = normalizeConfig(nextConfig);
|
|
133
|
+
setConfigState(normalized);
|
|
134
|
+
writeStoredConfig(storageMode, normalized);
|
|
135
|
+
},
|
|
136
|
+
[storageMode]
|
|
58
137
|
);
|
|
59
138
|
|
|
60
139
|
const [state, setState] = useState<ConnectionState>({
|
|
@@ -63,6 +142,7 @@ export function ConnectionProvider({
|
|
|
63
142
|
error: null,
|
|
64
143
|
client: null,
|
|
65
144
|
});
|
|
145
|
+
const lastAutoConnectConfigKeyRef = useRef<string | null>(null);
|
|
66
146
|
|
|
67
147
|
// Resolve initial config: saved config -> provided defaults
|
|
68
148
|
useEffect(() => {
|
|
@@ -90,18 +170,16 @@ export function ConnectionProvider({
|
|
|
90
170
|
return false;
|
|
91
171
|
}
|
|
92
172
|
|
|
93
|
-
const normalizedConfig
|
|
94
|
-
serverUrl: effectiveConfig.serverUrl?.trim() ?? '',
|
|
95
|
-
token: effectiveConfig.token?.trim() ?? '',
|
|
96
|
-
};
|
|
173
|
+
const normalizedConfig = normalizeConfig(effectiveConfig);
|
|
97
174
|
|
|
98
175
|
// Validate config has required fields
|
|
99
|
-
if (!normalizedConfig
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
176
|
+
if (!normalizedConfig) {
|
|
177
|
+
const hasServerUrl =
|
|
178
|
+
(effectiveConfig.serverUrl?.trim() ?? '').length > 0;
|
|
179
|
+
setState((s) => ({
|
|
180
|
+
...s,
|
|
181
|
+
error: hasServerUrl ? 'Token is required' : 'Server URL is required',
|
|
182
|
+
}));
|
|
105
183
|
return false;
|
|
106
184
|
}
|
|
107
185
|
|
|
@@ -113,24 +191,14 @@ export function ConnectionProvider({
|
|
|
113
191
|
|
|
114
192
|
try {
|
|
115
193
|
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
|
-
}
|
|
194
|
+
await testConnection(client);
|
|
127
195
|
setState({
|
|
128
|
-
isConnected:
|
|
196
|
+
isConnected: true,
|
|
129
197
|
isConnecting: false,
|
|
130
|
-
client
|
|
131
|
-
error:
|
|
198
|
+
client,
|
|
199
|
+
error: null,
|
|
132
200
|
});
|
|
133
|
-
return
|
|
201
|
+
return true;
|
|
134
202
|
} catch (err) {
|
|
135
203
|
setState({
|
|
136
204
|
isConnected: false,
|
|
@@ -192,6 +260,32 @@ export function ConnectionProvider({
|
|
|
192
260
|
[config, setConfig, state, connect, disconnect, clearError]
|
|
193
261
|
);
|
|
194
262
|
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (!autoConnect || state.isConnected || state.isConnecting) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const candidate = config ?? defaultConfig;
|
|
269
|
+
const key = normalizeConfigKey(candidate);
|
|
270
|
+
if (!candidate || !key) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (lastAutoConnectConfigKeyRef.current === key) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
lastAutoConnectConfigKeyRef.current = key;
|
|
279
|
+
void connect(candidate, { persistOverride: true });
|
|
280
|
+
}, [
|
|
281
|
+
autoConnect,
|
|
282
|
+
config,
|
|
283
|
+
defaultConfig,
|
|
284
|
+
state.isConnected,
|
|
285
|
+
state.isConnecting,
|
|
286
|
+
connect,
|
|
287
|
+
]);
|
|
288
|
+
|
|
195
289
|
return (
|
|
196
290
|
<ConnectionContext.Provider value={value}>
|
|
197
291
|
{children}
|
|
@@ -199,6 +293,18 @@ export function ConnectionProvider({
|
|
|
199
293
|
);
|
|
200
294
|
}
|
|
201
295
|
|
|
296
|
+
function normalizeConfigKey(config: ConnectionConfig | null): string | null {
|
|
297
|
+
if (!config) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const serverUrl = config.serverUrl?.trim() ?? '';
|
|
301
|
+
const token = config.token?.trim() ?? '';
|
|
302
|
+
if (!serverUrl || !token) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return `${serverUrl}\u0000${token}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
202
308
|
export function useConnection(): ConnectionContextValue {
|
|
203
309
|
const context = useContext(ConnectionContext);
|
|
204
310
|
if (!context) {
|
|
@@ -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
|
+
}
|