@rozenite/storage-plugin 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +23 -0
- package/dist/devtools/assets/panel-DVxtKLI9.css +1 -0
- package/dist/devtools/assets/panel-DnjbFMGo.js +22 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +9 -5
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +253 -191
- package/dist/react-native/index.d.ts +29 -6
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +1 -0
- package/package.json +6 -6
- package/src/react-native/__tests__/import.test.ts +182 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +141 -0
- package/src/react-native/adapters/mmkv.ts +15 -0
- package/src/react-native/import.ts +67 -0
- package/src/react-native/storage-view.ts +16 -8
- package/src/react-native/useRozeniteStoragePlugin.ts +61 -36
- package/src/shared/__tests__/snapshot.test.ts +361 -0
- package/src/shared/messaging.ts +25 -1
- package/src/shared/snapshot.ts +276 -0
- package/src/shared/types.ts +1 -0
- package/src/ui/import-dialog.tsx +261 -0
- package/src/ui/panel.tsx +257 -57
- package/src/ui/utils.ts +30 -0
- package/dist/devtools/assets/panel-DMhXYHH4.css +0 -1
- package/dist/devtools/assets/panel-eGOuOVos.js +0 -22
package/src/ui/panel.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
2
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Download, Plus, Search, Upload } from 'lucide-react';
|
|
4
4
|
import type {
|
|
5
5
|
StorageDeleteEntryEvent,
|
|
6
6
|
StorageEventMap,
|
|
7
|
+
StorageImportResultEvent,
|
|
7
8
|
StorageSetEntryEvent,
|
|
8
9
|
StorageSnapshotEvent,
|
|
9
10
|
} from '../shared/messaging';
|
|
@@ -14,10 +15,18 @@ import type {
|
|
|
14
15
|
StorageTarget,
|
|
15
16
|
} from '../shared/types';
|
|
16
17
|
import { getStorageViewId } from '../shared/types';
|
|
18
|
+
import {
|
|
19
|
+
buildSnapshot,
|
|
20
|
+
computePreview,
|
|
21
|
+
parseSnapshot,
|
|
22
|
+
} from '../shared/snapshot';
|
|
17
23
|
import { EditableTable } from './editable-table';
|
|
18
24
|
import { AddEntryDialog } from './add-entry-dialog';
|
|
19
25
|
import { EntryDetailDialog } from './entry-detail-dialog';
|
|
20
26
|
import { EditEntryDialog } from './edit-entry-dialog';
|
|
27
|
+
import { ConfirmDialog } from './confirm-dialog';
|
|
28
|
+
import { ImportDialog, type ImportFlightState } from './import-dialog';
|
|
29
|
+
import { buildExportFilename, downloadJson } from './utils';
|
|
21
30
|
import './globals.css';
|
|
22
31
|
|
|
23
32
|
type StorageSnapshotState = {
|
|
@@ -25,10 +34,22 @@ type StorageSnapshotState = {
|
|
|
25
34
|
adapterName: string;
|
|
26
35
|
storageName: string;
|
|
27
36
|
capabilities: StorageCapabilities;
|
|
37
|
+
blacklist?: RegExp;
|
|
28
38
|
entries: StorageEntry[];
|
|
29
39
|
};
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
type AlertState = {
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
title: string;
|
|
44
|
+
message: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const sameTarget = (a: StorageTarget, b: StorageTarget) =>
|
|
48
|
+
a.adapterId === b.adapterId && a.storageId === b.storageId;
|
|
49
|
+
|
|
50
|
+
const getEntryTypeFromValue = (
|
|
51
|
+
value: StorageEntryValue,
|
|
52
|
+
): StorageEntry['type'] => {
|
|
32
53
|
if (typeof value === 'string') {
|
|
33
54
|
return 'string';
|
|
34
55
|
}
|
|
@@ -46,11 +67,11 @@ const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] =
|
|
|
46
67
|
|
|
47
68
|
export default function StoragePanel() {
|
|
48
69
|
const [snapshots, setSnapshots] = useState<Map<string, StorageSnapshotState>>(
|
|
49
|
-
new Map()
|
|
50
|
-
);
|
|
51
|
-
const [selectedStorageViewId, setSelectedStorageViewId] = useState<string | null>(
|
|
52
|
-
null
|
|
70
|
+
new Map(),
|
|
53
71
|
);
|
|
72
|
+
const [selectedStorageViewId, setSelectedStorageViewId] = useState<
|
|
73
|
+
string | null
|
|
74
|
+
>(null);
|
|
54
75
|
const [loading, setLoading] = useState(false);
|
|
55
76
|
const [searchTerm, setSearchTerm] = useState('');
|
|
56
77
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
@@ -58,6 +79,15 @@ export default function StoragePanel() {
|
|
|
58
79
|
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
|
59
80
|
const [editingEntry, setEditingEntry] = useState<StorageEntry | null>(null);
|
|
60
81
|
const [showEditDialog, setShowEditDialog] = useState(false);
|
|
82
|
+
const [importFlight, setImportFlight] = useState<ImportFlightState | null>(
|
|
83
|
+
null,
|
|
84
|
+
);
|
|
85
|
+
const [alertState, setAlertState] = useState<AlertState>({
|
|
86
|
+
isOpen: false,
|
|
87
|
+
title: '',
|
|
88
|
+
message: '',
|
|
89
|
+
});
|
|
90
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
61
91
|
|
|
62
92
|
const client = useRozeniteDevToolsClient<StorageEventMap>({
|
|
63
93
|
pluginId: '@rozenite/storage-plugin',
|
|
@@ -71,61 +101,91 @@ export default function StoragePanel() {
|
|
|
71
101
|
const snapshotSubscription = client.onMessage(
|
|
72
102
|
'snapshot',
|
|
73
103
|
(event: StorageSnapshotEvent) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
const viewId = getStorageViewId(event.target);
|
|
105
|
+
setSnapshots((previous) => {
|
|
106
|
+
const next = new Map(previous);
|
|
107
|
+
next.set(viewId, {
|
|
108
|
+
target: event.target,
|
|
109
|
+
adapterName: event.adapterName,
|
|
110
|
+
storageName: event.storageName,
|
|
111
|
+
capabilities: event.capabilities,
|
|
112
|
+
blacklist: event.blacklist
|
|
113
|
+
? new RegExp(event.blacklist.source, event.blacklist.flags)
|
|
114
|
+
: undefined,
|
|
115
|
+
entries: event.entries,
|
|
116
|
+
});
|
|
84
117
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
if (previous.size === 0 && !selectedStorageViewId) {
|
|
119
|
+
setSelectedStorageViewId(viewId);
|
|
120
|
+
}
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
125
|
+
if (viewId === selectedStorageViewId) {
|
|
126
|
+
setLoading(false);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
96
129
|
);
|
|
97
130
|
|
|
98
131
|
const setEntrySubscription = client.onMessage(
|
|
99
132
|
'set-entry',
|
|
100
133
|
(event: StorageSetEntryEvent) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
const viewId = getStorageViewId(event.target);
|
|
135
|
+
setSnapshots((previous) => {
|
|
136
|
+
const next = new Map(previous);
|
|
137
|
+
const current = next.get(viewId);
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
if (!current) {
|
|
140
|
+
return previous;
|
|
141
|
+
}
|
|
109
142
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
const existingIndex = current.entries.findIndex(
|
|
144
|
+
(entry) => entry.key === event.entry.key,
|
|
145
|
+
);
|
|
113
146
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
const entries =
|
|
148
|
+
existingIndex >= 0
|
|
149
|
+
? current.entries.map((entry) =>
|
|
150
|
+
entry.key === event.entry.key ? event.entry : entry,
|
|
151
|
+
)
|
|
152
|
+
: [...current.entries, event.entry];
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
next.set(viewId, {
|
|
155
|
+
...current,
|
|
156
|
+
entries,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return next;
|
|
124
160
|
});
|
|
125
161
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
setImportFlight((previous) => {
|
|
163
|
+
if (!previous || previous.phase !== 'importing') return previous;
|
|
164
|
+
if (!sameTarget(event.target, previous.target)) return previous;
|
|
165
|
+
return { ...previous, written: previous.written + 1 };
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const importResultSubscription = client.onMessage(
|
|
171
|
+
'import-result',
|
|
172
|
+
(event: StorageImportResultEvent) => {
|
|
173
|
+
setImportFlight((previous) => {
|
|
174
|
+
if (!previous || previous.phase !== 'importing') return previous;
|
|
175
|
+
if (!sameTarget(event.target, previous.target)) return previous;
|
|
176
|
+
if (event.ok) {
|
|
177
|
+
return { phase: 'result', ok: true, written: event.written };
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
phase: 'result',
|
|
181
|
+
ok: false,
|
|
182
|
+
written: event.written,
|
|
183
|
+
total: event.total,
|
|
184
|
+
failedKey: event.failedKey,
|
|
185
|
+
error: event.error ?? 'Unknown error',
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
},
|
|
129
189
|
);
|
|
130
190
|
|
|
131
191
|
const deleteEntrySubscription = client.onMessage(
|
|
@@ -148,7 +208,7 @@ export default function StoragePanel() {
|
|
|
148
208
|
|
|
149
209
|
return next;
|
|
150
210
|
});
|
|
151
|
-
}
|
|
211
|
+
},
|
|
152
212
|
);
|
|
153
213
|
|
|
154
214
|
client.send('get-snapshot', {
|
|
@@ -160,6 +220,7 @@ export default function StoragePanel() {
|
|
|
160
220
|
snapshotSubscription.remove();
|
|
161
221
|
setEntrySubscription.remove();
|
|
162
222
|
deleteEntrySubscription.remove();
|
|
223
|
+
importResultSubscription.remove();
|
|
163
224
|
};
|
|
164
225
|
}, [client, selectedStorageViewId]);
|
|
165
226
|
|
|
@@ -178,7 +239,7 @@ export default function StoragePanel() {
|
|
|
178
239
|
const separatorIndex = selectedStorageViewId.indexOf(':');
|
|
179
240
|
if (separatorIndex < 0) {
|
|
180
241
|
console.warn(
|
|
181
|
-
`[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}"
|
|
242
|
+
`[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}".`,
|
|
182
243
|
);
|
|
183
244
|
setLoading(false);
|
|
184
245
|
return;
|
|
@@ -198,7 +259,7 @@ export default function StoragePanel() {
|
|
|
198
259
|
}, [client, selectedStorageViewId, snapshots]);
|
|
199
260
|
|
|
200
261
|
const selectedStorage = selectedStorageViewId
|
|
201
|
-
? snapshots.get(selectedStorageViewId) ?? null
|
|
262
|
+
? (snapshots.get(selectedStorageViewId) ?? null)
|
|
202
263
|
: null;
|
|
203
264
|
|
|
204
265
|
const entries = selectedStorage?.entries ?? [];
|
|
@@ -206,15 +267,15 @@ export default function StoragePanel() {
|
|
|
206
267
|
const filteredEntries = useMemo(
|
|
207
268
|
() =>
|
|
208
269
|
entries.filter((entry) =>
|
|
209
|
-
entry.key.toLowerCase().includes(searchTerm.toLowerCase())
|
|
270
|
+
entry.key.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
210
271
|
),
|
|
211
|
-
[entries, searchTerm]
|
|
272
|
+
[entries, searchTerm],
|
|
212
273
|
);
|
|
213
274
|
|
|
214
275
|
const supportedTypes = selectedStorage?.capabilities.supportedTypes ?? [];
|
|
215
276
|
|
|
216
277
|
const updateEntriesForSelectedStorage = (
|
|
217
|
-
mutate: (entries: StorageEntry[]) => StorageEntry[]
|
|
278
|
+
mutate: (entries: StorageEntry[]) => StorageEntry[],
|
|
218
279
|
) => {
|
|
219
280
|
if (!selectedStorageViewId) {
|
|
220
281
|
return;
|
|
@@ -266,7 +327,7 @@ export default function StoragePanel() {
|
|
|
266
327
|
});
|
|
267
328
|
|
|
268
329
|
updateEntriesForSelectedStorage((currentEntries) =>
|
|
269
|
-
currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry))
|
|
330
|
+
currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry)),
|
|
270
331
|
);
|
|
271
332
|
};
|
|
272
333
|
|
|
@@ -282,7 +343,7 @@ export default function StoragePanel() {
|
|
|
282
343
|
});
|
|
283
344
|
|
|
284
345
|
updateEntriesForSelectedStorage((currentEntries) =>
|
|
285
|
-
currentEntries.filter((entry) => entry.key !== key)
|
|
346
|
+
currentEntries.filter((entry) => entry.key !== key),
|
|
286
347
|
);
|
|
287
348
|
};
|
|
288
349
|
|
|
@@ -297,7 +358,105 @@ export default function StoragePanel() {
|
|
|
297
358
|
entry,
|
|
298
359
|
});
|
|
299
360
|
|
|
300
|
-
updateEntriesForSelectedStorage((currentEntries) => [
|
|
361
|
+
updateEntriesForSelectedStorage((currentEntries) => [
|
|
362
|
+
...currentEntries,
|
|
363
|
+
entry,
|
|
364
|
+
]);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const showAlert = (title: string, message: string) =>
|
|
368
|
+
setAlertState({ isOpen: true, title, message });
|
|
369
|
+
|
|
370
|
+
const handleImportClick = () => {
|
|
371
|
+
if (!fileInputRef.current) return;
|
|
372
|
+
fileInputRef.current.value = '';
|
|
373
|
+
fileInputRef.current.click();
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const handleFileChange = async (
|
|
377
|
+
event: React.ChangeEvent<HTMLInputElement>,
|
|
378
|
+
) => {
|
|
379
|
+
const file = event.target.files?.[0];
|
|
380
|
+
if (!file || !selectedStorage) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let raw: unknown;
|
|
385
|
+
try {
|
|
386
|
+
const text = await file.text();
|
|
387
|
+
raw = JSON.parse(text);
|
|
388
|
+
} catch (parseError) {
|
|
389
|
+
showAlert(
|
|
390
|
+
'Could not read file',
|
|
391
|
+
parseError instanceof Error ? parseError.message : String(parseError),
|
|
392
|
+
);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const parsed = parseSnapshot(raw);
|
|
397
|
+
if (!parsed.ok) {
|
|
398
|
+
showAlert(
|
|
399
|
+
'Invalid snapshot',
|
|
400
|
+
`${parsed.error.path}: ${parsed.error.message}`,
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const preview = computePreview(parsed.snapshot, {
|
|
406
|
+
target: selectedStorage.target,
|
|
407
|
+
capabilities: selectedStorage.capabilities,
|
|
408
|
+
entryKeys: new Set(selectedStorage.entries.map((entry) => entry.key)),
|
|
409
|
+
isBlacklisted: selectedStorage.blacklist
|
|
410
|
+
? (key) => selectedStorage.blacklist!.test(key)
|
|
411
|
+
: () => false,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const skippedSet = new Set(preview.skippedKeys.map((s) => s.key));
|
|
415
|
+
const unsupportedSet = new Set(preview.unsupportedTypes.map((u) => u.key));
|
|
416
|
+
const entriesToWrite = parsed.snapshot.entries.filter(
|
|
417
|
+
(entry) => !skippedSet.has(entry.key) && !unsupportedSet.has(entry.key),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
setImportFlight({
|
|
421
|
+
phase: 'preview',
|
|
422
|
+
target: selectedStorage.target,
|
|
423
|
+
targetLabel: `${selectedStorage.adapterName} / ${selectedStorage.storageName}`,
|
|
424
|
+
snapshot: parsed.snapshot,
|
|
425
|
+
preview,
|
|
426
|
+
entriesToWrite,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const handleApplyImport = () => {
|
|
431
|
+
if (!client) return;
|
|
432
|
+
if (!importFlight || importFlight.phase !== 'preview') return;
|
|
433
|
+
|
|
434
|
+
client.send('import-entries', {
|
|
435
|
+
type: 'import-entries',
|
|
436
|
+
target: importFlight.target,
|
|
437
|
+
entries: importFlight.entriesToWrite,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
setImportFlight({
|
|
441
|
+
phase: 'importing',
|
|
442
|
+
target: importFlight.target,
|
|
443
|
+
total: importFlight.entriesToWrite.length,
|
|
444
|
+
written: 0,
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const handleCloseImport = () => setImportFlight(null);
|
|
449
|
+
|
|
450
|
+
const handleExport = () => {
|
|
451
|
+
if (!selectedStorage) return;
|
|
452
|
+
const snapshot = buildSnapshot({
|
|
453
|
+
target: selectedStorage.target,
|
|
454
|
+
adapterName: selectedStorage.adapterName,
|
|
455
|
+
storageName: selectedStorage.storageName,
|
|
456
|
+
capabilities: selectedStorage.capabilities,
|
|
457
|
+
entries: selectedStorage.entries,
|
|
458
|
+
});
|
|
459
|
+
downloadJson(snapshot, buildExportFilename(selectedStorage.target));
|
|
301
460
|
};
|
|
302
461
|
|
|
303
462
|
const storageOptions = [...snapshots.entries()].map(([viewId, snapshot]) => ({
|
|
@@ -346,6 +505,31 @@ export default function StoragePanel() {
|
|
|
346
505
|
<Plus className="h-3 w-3" />
|
|
347
506
|
Add Entry
|
|
348
507
|
</button>
|
|
508
|
+
<button
|
|
509
|
+
onClick={handleImportClick}
|
|
510
|
+
disabled={!selectedStorage}
|
|
511
|
+
className="flex items-center gap-1 px-3 h-8 text-xs bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed text-gray-100 rounded transition-colors"
|
|
512
|
+
title="Import entries from a JSON snapshot"
|
|
513
|
+
>
|
|
514
|
+
<Upload className="h-3 w-3" />
|
|
515
|
+
Import
|
|
516
|
+
</button>
|
|
517
|
+
<button
|
|
518
|
+
onClick={handleExport}
|
|
519
|
+
disabled={!selectedStorage || entries.length === 0}
|
|
520
|
+
className="flex items-center gap-1 px-3 h-8 text-xs bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed text-gray-100 rounded transition-colors"
|
|
521
|
+
title="Export entries to a JSON snapshot"
|
|
522
|
+
>
|
|
523
|
+
<Download className="h-3 w-3" />
|
|
524
|
+
Export
|
|
525
|
+
</button>
|
|
526
|
+
<input
|
|
527
|
+
ref={fileInputRef}
|
|
528
|
+
type="file"
|
|
529
|
+
accept="application/json,.json"
|
|
530
|
+
className="hidden"
|
|
531
|
+
onChange={handleFileChange}
|
|
532
|
+
/>
|
|
349
533
|
<div className="flex-1">
|
|
350
534
|
<div className="relative">
|
|
351
535
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
@@ -437,6 +621,22 @@ export default function StoragePanel() {
|
|
|
437
621
|
supportedTypes={supportedTypes}
|
|
438
622
|
entry={editingEntry}
|
|
439
623
|
/>
|
|
624
|
+
|
|
625
|
+
<ImportDialog
|
|
626
|
+
state={importFlight}
|
|
627
|
+
onApply={handleApplyImport}
|
|
628
|
+
onCancel={handleCloseImport}
|
|
629
|
+
onClose={handleCloseImport}
|
|
630
|
+
/>
|
|
631
|
+
|
|
632
|
+
<ConfirmDialog
|
|
633
|
+
isOpen={alertState.isOpen}
|
|
634
|
+
onClose={() => setAlertState((prev) => ({ ...prev, isOpen: false }))}
|
|
635
|
+
onConfirm={() => setAlertState((prev) => ({ ...prev, isOpen: false }))}
|
|
636
|
+
title={alertState.title}
|
|
637
|
+
message={alertState.message}
|
|
638
|
+
type="alert"
|
|
639
|
+
/>
|
|
440
640
|
</div>
|
|
441
641
|
);
|
|
442
642
|
}
|
package/src/ui/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StorageTarget } from '../shared/types';
|
|
2
|
+
|
|
3
|
+
export const downloadJson = (data: unknown, filename: string): void => {
|
|
4
|
+
const json = JSON.stringify(data, null, 2);
|
|
5
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
6
|
+
const url = URL.createObjectURL(blob);
|
|
7
|
+
const link = document.createElement('a');
|
|
8
|
+
link.href = url;
|
|
9
|
+
link.download = filename;
|
|
10
|
+
document.body.appendChild(link);
|
|
11
|
+
link.click();
|
|
12
|
+
document.body.removeChild(link);
|
|
13
|
+
URL.revokeObjectURL(url);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
17
|
+
|
|
18
|
+
export const buildExportFilename = (
|
|
19
|
+
target: StorageTarget,
|
|
20
|
+
now: Date = new Date(),
|
|
21
|
+
): string => {
|
|
22
|
+
const yyyy = now.getFullYear().toString().padStart(4, '0');
|
|
23
|
+
const mm = (now.getMonth() + 1).toString().padStart(2, '0');
|
|
24
|
+
const dd = now.getDate().toString().padStart(2, '0');
|
|
25
|
+
const hh = now.getHours().toString().padStart(2, '0');
|
|
26
|
+
const mi = now.getMinutes().toString().padStart(2, '0');
|
|
27
|
+
const ss = now.getSeconds().toString().padStart(2, '0');
|
|
28
|
+
const timestamp = `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
|
|
29
|
+
return `rozenite-storage-${sanitize(target.adapterId)}-${sanitize(target.storageId)}-${timestamp}.json`;
|
|
30
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 0 0% 9%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--chart-1: 12 76% 61%;--chart-2: 173 58% 39%;--chart-3: 197 37% 24%;--chart-4: 43 74% 66%;--chart-5: 27 87% 67%;--radius: .5rem;--sidebar-background: 0 0% 98%;--sidebar-foreground: 240 5.3% 26.1%;--sidebar-primary: 240 5.9% 10%;--sidebar-primary-foreground: 0 0% 98%;--sidebar-accent: 240 4.8% 95.9%;--sidebar-accent-foreground: 240 5.9% 10%;--sidebar-border: 220 13% 91%;--sidebar-ring: 217.2 91.2% 59.8%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.left-2{left:.5rem}.top-0{top:0}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.table{display:table}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-\[90vh\]{max-height:90vh}.min-h-0{min-height:0px}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[90vw\]{width:90vw}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-8{padding-left:2rem}.pr-3{padding-right:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-relaxed{line-height:1.625}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-cyan-500{--tw-text-opacity: 1;color:rgb(6 182 212 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-pink-500{--tw-text-opacity: 1;color:rgb(236 72 153 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#1f2937;border-radius:4px}::-webkit-scrollbar-thumb{background:#374151;border-radius:4px;border:1px solid #1f2937}::-webkit-scrollbar-thumb:hover{background:#4b5563}::-webkit-scrollbar-thumb:active{background:#6b7280}::-webkit-scrollbar-corner{background:#1f2937}.placeholder\:text-gray-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-400::placeholder{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-600:disabled{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}
|