@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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
EmptyState,
|
|
10
|
+
Input,
|
|
11
|
+
SectionCard,
|
|
12
|
+
Spinner,
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow,
|
|
19
|
+
} from '@syncular/ui';
|
|
20
|
+
import { useState } from 'react';
|
|
21
|
+
import {
|
|
22
|
+
useBlobDownload,
|
|
23
|
+
useBlobs,
|
|
24
|
+
useDeleteBlobMutation,
|
|
25
|
+
} from '../hooks/useConsoleApi';
|
|
26
|
+
|
|
27
|
+
function formatFileSize(bytes: number): string {
|
|
28
|
+
if (bytes === 0) return '0 B';
|
|
29
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
30
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
31
|
+
const value = bytes / 1024 ** i;
|
|
32
|
+
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatDateTime(iso: string): string {
|
|
36
|
+
const parsed = Date.parse(iso);
|
|
37
|
+
if (!Number.isFinite(parsed)) return iso;
|
|
38
|
+
return new Date(parsed).toLocaleString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Storage() {
|
|
42
|
+
const [prefixInput, setPrefixInput] = useState('');
|
|
43
|
+
const [activePrefix, setActivePrefix] = useState<string | undefined>(
|
|
44
|
+
undefined
|
|
45
|
+
);
|
|
46
|
+
const [cursor, setCursor] = useState<string | undefined>(undefined);
|
|
47
|
+
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
|
48
|
+
const [deletingKey, setDeletingKey] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const { data, isLoading, error } = useBlobs({
|
|
51
|
+
prefix: activePrefix,
|
|
52
|
+
cursor,
|
|
53
|
+
limit: 100,
|
|
54
|
+
});
|
|
55
|
+
const deleteMutation = useDeleteBlobMutation();
|
|
56
|
+
const download = useBlobDownload();
|
|
57
|
+
|
|
58
|
+
function handleFilter() {
|
|
59
|
+
const trimmed = prefixInput.trim();
|
|
60
|
+
setActivePrefix(trimmed.length > 0 ? trimmed : undefined);
|
|
61
|
+
setCursor(undefined);
|
|
62
|
+
setCursorHistory([]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleClearFilter() {
|
|
66
|
+
setPrefixInput('');
|
|
67
|
+
setActivePrefix(undefined);
|
|
68
|
+
setCursor(undefined);
|
|
69
|
+
setCursorHistory([]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleNextPage() {
|
|
73
|
+
if (data?.cursor) {
|
|
74
|
+
setCursorHistory((prev) => [...prev, cursor ?? '']);
|
|
75
|
+
setCursor(data.cursor);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handlePrevPage() {
|
|
80
|
+
setCursorHistory((prev) => {
|
|
81
|
+
const next = [...prev];
|
|
82
|
+
const prevCursor = next.pop();
|
|
83
|
+
setCursor(prevCursor && prevCursor.length > 0 ? prevCursor : undefined);
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleDelete() {
|
|
89
|
+
if (!deletingKey) return;
|
|
90
|
+
deleteMutation.mutate(deletingKey, {
|
|
91
|
+
onSuccess: () => setDeletingKey(null),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isLoading) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col gap-4 px-5 py-5">
|
|
98
|
+
<div className="flex items-center justify-center h-[200px]">
|
|
99
|
+
<Spinner size="lg" />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex flex-col gap-4 px-5 py-5">
|
|
108
|
+
<div className="flex items-center justify-center h-[200px]">
|
|
109
|
+
<p className="text-danger font-mono text-[11px]">
|
|
110
|
+
Failed to load storage items: {error.message}
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const items = data?.items ?? [];
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex flex-col gap-4 px-5 py-5">
|
|
121
|
+
<SectionCard
|
|
122
|
+
title="Storage"
|
|
123
|
+
actions={
|
|
124
|
+
<div className="flex items-center gap-2">
|
|
125
|
+
<Input
|
|
126
|
+
placeholder="Prefix filter..."
|
|
127
|
+
value={prefixInput}
|
|
128
|
+
onChange={(e) => setPrefixInput(e.target.value)}
|
|
129
|
+
onKeyDown={(e) => {
|
|
130
|
+
if (e.key === 'Enter') handleFilter();
|
|
131
|
+
}}
|
|
132
|
+
className="h-7 w-48 text-xs"
|
|
133
|
+
/>
|
|
134
|
+
<Button variant="default" size="sm" onClick={handleFilter}>
|
|
135
|
+
Filter
|
|
136
|
+
</Button>
|
|
137
|
+
{activePrefix && (
|
|
138
|
+
<Button variant="ghost" size="sm" onClick={handleClearFilter}>
|
|
139
|
+
Clear
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
{items.length === 0 ? (
|
|
146
|
+
<EmptyState
|
|
147
|
+
message={
|
|
148
|
+
activePrefix
|
|
149
|
+
? `No storage items matching prefix "${activePrefix}".`
|
|
150
|
+
: 'No storage items found.'
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
) : (
|
|
154
|
+
<>
|
|
155
|
+
<div className="overflow-x-auto">
|
|
156
|
+
<Table>
|
|
157
|
+
<TableHeader>
|
|
158
|
+
<TableRow>
|
|
159
|
+
<TableHead>Key</TableHead>
|
|
160
|
+
<TableHead>Size</TableHead>
|
|
161
|
+
<TableHead>Type</TableHead>
|
|
162
|
+
<TableHead>Uploaded</TableHead>
|
|
163
|
+
<TableHead>Actions</TableHead>
|
|
164
|
+
</TableRow>
|
|
165
|
+
</TableHeader>
|
|
166
|
+
<TableBody>
|
|
167
|
+
{items.map((blob) => (
|
|
168
|
+
<TableRow key={blob.key}>
|
|
169
|
+
<TableCell
|
|
170
|
+
className="font-mono text-xs max-w-[320px]"
|
|
171
|
+
title={blob.key}
|
|
172
|
+
>
|
|
173
|
+
{blob.key}
|
|
174
|
+
</TableCell>
|
|
175
|
+
<TableCell className="font-mono text-xs text-neutral-400 whitespace-nowrap">
|
|
176
|
+
{formatFileSize(blob.size)}
|
|
177
|
+
</TableCell>
|
|
178
|
+
<TableCell>
|
|
179
|
+
{blob.httpMetadata?.contentType ? (
|
|
180
|
+
<Badge variant="ghost">
|
|
181
|
+
{blob.httpMetadata.contentType}
|
|
182
|
+
</Badge>
|
|
183
|
+
) : (
|
|
184
|
+
<span className="text-neutral-500 text-xs">--</span>
|
|
185
|
+
)}
|
|
186
|
+
</TableCell>
|
|
187
|
+
<TableCell className="whitespace-nowrap text-xs text-neutral-400">
|
|
188
|
+
{formatDateTime(blob.uploaded)}
|
|
189
|
+
</TableCell>
|
|
190
|
+
<TableCell>
|
|
191
|
+
<div className="flex items-center gap-1">
|
|
192
|
+
<Button
|
|
193
|
+
variant="ghost"
|
|
194
|
+
size="sm"
|
|
195
|
+
onClick={() => void download(blob.key)}
|
|
196
|
+
>
|
|
197
|
+
Download
|
|
198
|
+
</Button>
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="sm"
|
|
202
|
+
onClick={() => setDeletingKey(blob.key)}
|
|
203
|
+
>
|
|
204
|
+
Delete
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
</TableCell>
|
|
208
|
+
</TableRow>
|
|
209
|
+
))}
|
|
210
|
+
</TableBody>
|
|
211
|
+
</Table>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Pagination */}
|
|
215
|
+
<div className="flex items-center justify-between px-2 py-2">
|
|
216
|
+
<Button
|
|
217
|
+
variant="ghost"
|
|
218
|
+
size="sm"
|
|
219
|
+
disabled={cursorHistory.length === 0}
|
|
220
|
+
onClick={handlePrevPage}
|
|
221
|
+
>
|
|
222
|
+
Previous
|
|
223
|
+
</Button>
|
|
224
|
+
<Button
|
|
225
|
+
variant="ghost"
|
|
226
|
+
size="sm"
|
|
227
|
+
disabled={!data?.truncated}
|
|
228
|
+
onClick={handleNextPage}
|
|
229
|
+
>
|
|
230
|
+
Next
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</SectionCard>
|
|
236
|
+
|
|
237
|
+
{/* Delete confirmation dialog */}
|
|
238
|
+
<Dialog
|
|
239
|
+
open={deletingKey !== null}
|
|
240
|
+
onOpenChange={() => setDeletingKey(null)}
|
|
241
|
+
>
|
|
242
|
+
<DialogContent>
|
|
243
|
+
<DialogHeader>
|
|
244
|
+
<DialogTitle>Delete Storage Item</DialogTitle>
|
|
245
|
+
</DialogHeader>
|
|
246
|
+
<div className="px-5 py-4 flex flex-col gap-4">
|
|
247
|
+
<p className="font-mono text-[11px] text-neutral-300">
|
|
248
|
+
Are you sure you want to delete{' '}
|
|
249
|
+
<span className="font-mono text-white">{deletingKey}</span>?
|
|
250
|
+
</p>
|
|
251
|
+
<p className="font-mono text-[10px] text-neutral-500">
|
|
252
|
+
This action cannot be undone.
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
255
|
+
<DialogFooter>
|
|
256
|
+
<Button variant="default" onClick={() => setDeletingKey(null)}>
|
|
257
|
+
Cancel
|
|
258
|
+
</Button>
|
|
259
|
+
<Button
|
|
260
|
+
variant="destructive"
|
|
261
|
+
onClick={handleDelete}
|
|
262
|
+
disabled={deleteMutation.isPending}
|
|
263
|
+
>
|
|
264
|
+
{deleteMutation.isPending ? (
|
|
265
|
+
<>
|
|
266
|
+
<Spinner size="sm" /> Deleting...
|
|
267
|
+
</>
|
|
268
|
+
) : (
|
|
269
|
+
'Delete'
|
|
270
|
+
)}
|
|
271
|
+
</Button>
|
|
272
|
+
</DialogFooter>
|
|
273
|
+
</DialogContent>
|
|
274
|
+
</Dialog>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
package/src/pages/index.ts
CHANGED
package/src/routeTree.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Route as indexRoute } from './routes/index';
|
|
|
5
5
|
import { Route as investigateCommitRoute } from './routes/investigate-commit';
|
|
6
6
|
import { Route as investigateEventRoute } from './routes/investigate-event';
|
|
7
7
|
import { Route as opsRoute } from './routes/ops';
|
|
8
|
+
import { Route as storageRoute } from './routes/storage';
|
|
8
9
|
import { Route as streamRoute } from './routes/stream';
|
|
9
10
|
|
|
10
11
|
export const routeTree = rootRoute.addChildren([
|
|
@@ -14,5 +15,6 @@ export const routeTree = rootRoute.addChildren([
|
|
|
14
15
|
investigateEventRoute,
|
|
15
16
|
fleetRoute,
|
|
16
17
|
opsRoute,
|
|
18
|
+
storageRoute,
|
|
17
19
|
configRoute,
|
|
18
20
|
]);
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
CONSOLE_BASEPATH_META,
|
|
7
|
+
CONSOLE_SERVER_URL_META,
|
|
8
|
+
CONSOLE_TOKEN_META,
|
|
9
|
+
normalizeBasePath,
|
|
10
|
+
} from './runtime-config';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MOUNT_PATH = '/console';
|
|
13
|
+
const DEFAULT_INDEX_CACHE_CONTROL = 'no-store';
|
|
14
|
+
const DEFAULT_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
15
|
+
|
|
16
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
17
|
+
'.css': 'text/css; charset=utf-8',
|
|
18
|
+
'.html': 'text/html; charset=utf-8',
|
|
19
|
+
'.ico': 'image/x-icon',
|
|
20
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
21
|
+
'.json': 'application/json; charset=utf-8',
|
|
22
|
+
'.manifest': 'application/manifest+json; charset=utf-8',
|
|
23
|
+
'.png': 'image/png',
|
|
24
|
+
'.svg': 'image/svg+xml',
|
|
25
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
26
|
+
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface ConsoleUiPrefill {
|
|
30
|
+
basePath?: string;
|
|
31
|
+
serverUrl?: string;
|
|
32
|
+
token?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateConsoleStaticResponderOptions {
|
|
36
|
+
mountPath?: string;
|
|
37
|
+
staticDir?: string;
|
|
38
|
+
defaultPrefill?: ConsoleUiPrefill;
|
|
39
|
+
indexCacheControl?: string;
|
|
40
|
+
assetCacheControl?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ServeConsoleStaticRequestOptions {
|
|
44
|
+
prefill?: ConsoleUiPrefill;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ConsoleStaticResponder = (
|
|
48
|
+
request: Request,
|
|
49
|
+
options?: ServeConsoleStaticRequestOptions
|
|
50
|
+
) => Promise<Response | null>;
|
|
51
|
+
|
|
52
|
+
function normalizeMountPath(mountPath: string | undefined): string {
|
|
53
|
+
const value = mountPath?.trim() ?? '';
|
|
54
|
+
if (!value || value === '/') return '/';
|
|
55
|
+
const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
|
|
56
|
+
return withLeadingSlash.replace(/\/+$/g, '') || '/';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRequestPath(pathname: string): string {
|
|
60
|
+
const decodedPathname = decodeURIComponent(pathname);
|
|
61
|
+
return decodedPathname === '' ? '/' : decodedPathname;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function matchesMountPath(pathname: string, mountPath: string): boolean {
|
|
65
|
+
if (mountPath === '/') return pathname.startsWith('/');
|
|
66
|
+
return pathname === mountPath || pathname.startsWith(`${mountPath}/`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function relativePathForMount(pathname: string, mountPath: string): string {
|
|
70
|
+
if (mountPath === '/') return pathname;
|
|
71
|
+
const withoutPrefix = pathname.slice(mountPath.length);
|
|
72
|
+
return withoutPrefix === '' ? '/' : withoutPrefix;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function escapeHtmlAttribute(value: string): string {
|
|
76
|
+
return value
|
|
77
|
+
.replaceAll('&', '&')
|
|
78
|
+
.replaceAll('"', '"')
|
|
79
|
+
.replaceAll('<', '<')
|
|
80
|
+
.replaceAll('>', '>');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function withMetaTag(html: string, name: string, value: string): string {
|
|
84
|
+
const escapedValue = escapeHtmlAttribute(value);
|
|
85
|
+
const pattern = new RegExp(`<meta name="${name}" content="[^"]*"\\s*/?>`);
|
|
86
|
+
if (pattern.test(html)) {
|
|
87
|
+
return html.replace(
|
|
88
|
+
pattern,
|
|
89
|
+
`<meta name="${name}" content="${escapedValue}" />`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return html.replace(
|
|
94
|
+
'</head>',
|
|
95
|
+
` <meta name="${name}" content="${escapedValue}" />\n </head>`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isWithinDirectory(baseDir: string, targetPath: string): boolean {
|
|
100
|
+
return (
|
|
101
|
+
targetPath === baseDir || targetPath.startsWith(`${baseDir}${path.sep}`)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function contentTypeFor(pathname: string): string {
|
|
106
|
+
const extension = path.extname(pathname).toLowerCase();
|
|
107
|
+
return CONTENT_TYPES[extension] ?? 'application/octet-stream';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveConsoleStaticDir(): string {
|
|
111
|
+
const staticDirFromModule = fileURLToPath(
|
|
112
|
+
new URL('../web-dist', import.meta.url)
|
|
113
|
+
);
|
|
114
|
+
return path.resolve(staticDirFromModule);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderIndexHtml(args: {
|
|
118
|
+
template: string;
|
|
119
|
+
mountPath: string;
|
|
120
|
+
prefill?: ConsoleUiPrefill;
|
|
121
|
+
}): string {
|
|
122
|
+
const resolvedBasePath = normalizeBasePath(
|
|
123
|
+
args.prefill?.basePath ?? args.mountPath
|
|
124
|
+
);
|
|
125
|
+
const resolvedServerUrl = args.prefill?.serverUrl ?? '';
|
|
126
|
+
const resolvedToken = args.prefill?.token ?? '';
|
|
127
|
+
|
|
128
|
+
return withMetaTag(
|
|
129
|
+
withMetaTag(
|
|
130
|
+
withMetaTag(args.template, CONSOLE_BASEPATH_META, resolvedBasePath),
|
|
131
|
+
CONSOLE_SERVER_URL_META,
|
|
132
|
+
resolvedServerUrl
|
|
133
|
+
),
|
|
134
|
+
CONSOLE_TOKEN_META,
|
|
135
|
+
resolvedToken
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createConsoleStaticResponder(
|
|
140
|
+
options: CreateConsoleStaticResponderOptions = {}
|
|
141
|
+
): ConsoleStaticResponder {
|
|
142
|
+
const mountPath = normalizeMountPath(options.mountPath ?? DEFAULT_MOUNT_PATH);
|
|
143
|
+
const staticDir = path.resolve(
|
|
144
|
+
options.staticDir ?? resolveConsoleStaticDir()
|
|
145
|
+
);
|
|
146
|
+
const indexPath = path.join(staticDir, 'index.html');
|
|
147
|
+
|
|
148
|
+
if (!existsSync(indexPath)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Console distributable missing: ${indexPath}. Build @syncular/console before serving static assets.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const indexTemplate = readFileSync(indexPath, 'utf8');
|
|
155
|
+
const indexCacheControl =
|
|
156
|
+
options.indexCacheControl ?? DEFAULT_INDEX_CACHE_CONTROL;
|
|
157
|
+
const assetCacheControl =
|
|
158
|
+
options.assetCacheControl ?? DEFAULT_ASSET_CACHE_CONTROL;
|
|
159
|
+
const defaultPrefill = options.defaultPrefill;
|
|
160
|
+
|
|
161
|
+
return async (request, requestOptions = {}) => {
|
|
162
|
+
const method = request.method.toUpperCase();
|
|
163
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const url = new URL(request.url);
|
|
168
|
+
const pathname = normalizeRequestPath(url.pathname);
|
|
169
|
+
if (!matchesMountPath(pathname, mountPath)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const relativePath = relativePathForMount(pathname, mountPath);
|
|
174
|
+
const effectivePrefill = {
|
|
175
|
+
...defaultPrefill,
|
|
176
|
+
...requestOptions.prefill,
|
|
177
|
+
};
|
|
178
|
+
const sendIndex = () => {
|
|
179
|
+
const html = renderIndexHtml({
|
|
180
|
+
template: indexTemplate,
|
|
181
|
+
mountPath,
|
|
182
|
+
prefill: effectivePrefill,
|
|
183
|
+
});
|
|
184
|
+
return new Response(method === 'HEAD' ? undefined : html, {
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
188
|
+
'Cache-Control': indexCacheControl,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
relativePath === '/' ||
|
|
195
|
+
relativePath === '/index.html' ||
|
|
196
|
+
path.extname(relativePath).length === 0
|
|
197
|
+
) {
|
|
198
|
+
return sendIndex();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const candidatePath = path.resolve(staticDir, `.${relativePath}`);
|
|
202
|
+
if (!isWithinDirectory(staticDir, candidatePath)) {
|
|
203
|
+
return new Response('Forbidden', { status: 403 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!existsSync(candidatePath) || !statSync(candidatePath).isFile()) {
|
|
207
|
+
return new Response('Not Found', { status: 404 });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const body = method === 'HEAD' ? undefined : await readFile(candidatePath);
|
|
211
|
+
return new Response(body, {
|
|
212
|
+
status: 200,
|
|
213
|
+
headers: {
|
|
214
|
+
'Content-Type': contentTypeFor(candidatePath),
|
|
215
|
+
'Cache-Control': assetCacheControl,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/styles/globals.css
CHANGED