@instantdb/components 0.0.1
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/.env +2 -0
- package/.turbo/turbo-build.log +18 -0
- package/README.md +78 -0
- package/app/App.css +38 -0
- package/app/App.tsx +61 -0
- package/app/index.css +18 -0
- package/app/main.tsx +10 -0
- package/dist/components/StyleMe.d.ts +15 -0
- package/dist/components/StyleMe.d.ts.map +1 -0
- package/dist/components/error-boundary.d.ts +17 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts +14 -0
- package/dist/components/explorer/edit-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/edit-row-dialog.d.ts +10 -0
- package/dist/components/explorer/edit-row-dialog.d.ts.map +1 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts +15 -0
- package/dist/components/explorer/expandable-deleted-attr.d.ts.map +1 -0
- package/dist/components/explorer/explorer-layout.d.ts +8 -0
- package/dist/components/explorer/explorer-layout.d.ts.map +1 -0
- package/dist/components/explorer/index.d.ts +44 -0
- package/dist/components/explorer/index.d.ts.map +1 -0
- package/dist/components/explorer/inner-explorer.d.ts +16 -0
- package/dist/components/explorer/inner-explorer.d.ts.map +1 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts +10 -0
- package/dist/components/explorer/new-namespace-dialog.d.ts.map +1 -0
- package/dist/components/explorer/query-inspector.d.ts +11 -0
- package/dist/components/explorer/query-inspector.d.ts.map +1 -0
- package/dist/components/explorer/recently-deleted.d.ts +36 -0
- package/dist/components/explorer/recently-deleted.d.ts.map +1 -0
- package/dist/components/explorer/search-input.d.ts +9 -0
- package/dist/components/explorer/search-input.d.ts.map +1 -0
- package/dist/components/explorer/table-components.d.ts +16 -0
- package/dist/components/explorer/table-components.d.ts.map +1 -0
- package/dist/components/explorer/view-settings.d.ts +10 -0
- package/dist/components/explorer/view-settings.d.ts.map +1 -0
- package/dist/components/rosePineDawnTheme.d.ts +13 -0
- package/dist/components/rosePineDawnTheme.d.ts.map +1 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/toast.d.ts +4 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/ui.d.ts +336 -0
- package/dist/components/ui.d.ts.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/hooks/explorer.d.ts +29 -0
- package/dist/hooks/explorer.d.ts.map +1 -0
- package/dist/hooks/useAttrNotes.d.ts +10 -0
- package/dist/hooks/useAttrNotes.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.d.ts +3 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useColumnVisibility.d.ts +12 -0
- package/dist/hooks/useColumnVisibility.d.ts.map +1 -0
- package/dist/hooks/useEditBlobConstraints.d.ts +32 -0
- package/dist/hooks/useEditBlobConstraints.d.ts.map +1 -0
- package/dist/hooks/useExplorerHistory.d.ts +1 -0
- package/dist/hooks/useExplorerHistory.d.ts.map +1 -0
- package/dist/hooks/useIsOverflow.d.ts +6 -0
- package/dist/hooks/useIsOverflow.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.d.ts +2 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts +3 -0
- package/dist/hooks/useMonacoJSONSchema.d.ts.map +1 -0
- package/dist/hooks/useStableDB.d.ts +7 -0
- package/dist/hooks/useStableDB.d.ts.map +1 -0
- package/dist/index.cjs +15 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9270 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/indexingJobs.d.ts +24 -0
- package/dist/utils/indexingJobs.d.ts.map +1 -0
- package/dist/utils/parsePermsJSON.d.ts +11 -0
- package/dist/utils/parsePermsJSON.d.ts.map +1 -0
- package/dist/utils/renames.d.ts +3 -0
- package/dist/utils/renames.d.ts.map +1 -0
- package/dist/utils/tableWidthSize.d.ts +9 -0
- package/dist/utils/tableWidthSize.d.ts.map +1 -0
- package/index.html +13 -0
- package/package.json +109 -0
- package/src/components/StyleMe.tsx +97 -0
- package/src/components/error-boundary.tsx +76 -0
- package/src/components/explorer/edit-namespace-dialog.tsx +1886 -0
- package/src/components/explorer/edit-row-dialog.tsx +1151 -0
- package/src/components/explorer/expandable-deleted-attr.tsx +170 -0
- package/src/components/explorer/explorer-layout.tsx +156 -0
- package/src/components/explorer/index.tsx +217 -0
- package/src/components/explorer/inner-explorer.tsx +1341 -0
- package/src/components/explorer/new-namespace-dialog.tsx +54 -0
- package/src/components/explorer/query-inspector.tsx +394 -0
- package/src/components/explorer/recently-deleted.tsx +344 -0
- package/src/components/explorer/search-input.tsx +358 -0
- package/src/components/explorer/table-components.tsx +341 -0
- package/src/components/explorer/view-settings.tsx +75 -0
- package/src/components/rosePineDawnTheme.ts +45 -0
- package/src/components/select.tsx +198 -0
- package/src/components/toast.tsx +18 -0
- package/src/components/ui.tsx +1561 -0
- package/src/config.ts +61 -0
- package/src/hooks/explorer.tsx +125 -0
- package/src/hooks/useAttrNotes.ts +27 -0
- package/src/hooks/useClickOutside.ts +23 -0
- package/src/hooks/useColumnVisibility.ts +39 -0
- package/src/hooks/useEditBlobConstraints.ts +185 -0
- package/src/hooks/useExplorerHistory.ts +0 -0
- package/src/hooks/useIsOverflow.ts +24 -0
- package/src/hooks/useLocalStorage.ts +51 -0
- package/src/hooks/useMonacoJSONSchema.ts +41 -0
- package/src/hooks/useStableDB.ts +30 -0
- package/src/index.tsx +8 -0
- package/src/schema.ts +285 -0
- package/src/style.css +5 -0
- package/src/types.ts +359 -0
- package/src/utils/format.ts +13 -0
- package/src/utils/indexingJobs.ts +126 -0
- package/src/utils/parsePermsJSON.ts +35 -0
- package/src/utils/renames.ts +42 -0
- package/src/utils/tableWidthSize.ts +62 -0
- package/tailwind.config.cjs +42 -0
- package/tsconfig.json +22 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.ts +49 -0
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
DndContext,
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
KeyboardSensor,
|
|
6
|
+
MouseSensor,
|
|
7
|
+
TouchSensor,
|
|
8
|
+
useSensor,
|
|
9
|
+
useSensors,
|
|
10
|
+
} from '@dnd-kit/core';
|
|
11
|
+
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
|
12
|
+
import {
|
|
13
|
+
arrayMove,
|
|
14
|
+
horizontalListSortingStrategy,
|
|
15
|
+
SortableContext,
|
|
16
|
+
} from '@dnd-kit/sortable';
|
|
17
|
+
import {
|
|
18
|
+
ArrowUpOnSquareIcon,
|
|
19
|
+
PencilSquareIcon,
|
|
20
|
+
TrashIcon,
|
|
21
|
+
XMarkIcon,
|
|
22
|
+
} from '@heroicons/react/24/outline';
|
|
23
|
+
import { coerceToDate, tx } from '@instantdb/core';
|
|
24
|
+
import { InstantReactAbstractDatabase } from '@instantdb/react';
|
|
25
|
+
import {
|
|
26
|
+
ColumnDef,
|
|
27
|
+
ColumnSizingState,
|
|
28
|
+
getCoreRowModel,
|
|
29
|
+
useReactTable,
|
|
30
|
+
} from '@tanstack/react-table';
|
|
31
|
+
import {
|
|
32
|
+
ArrowLeftIcon,
|
|
33
|
+
ArrowRightIcon,
|
|
34
|
+
CurlyBraces,
|
|
35
|
+
FileDown,
|
|
36
|
+
PlusIcon,
|
|
37
|
+
Table,
|
|
38
|
+
} from 'lucide-react';
|
|
39
|
+
import React, {
|
|
40
|
+
useEffect,
|
|
41
|
+
useLayoutEffect,
|
|
42
|
+
useMemo,
|
|
43
|
+
useRef,
|
|
44
|
+
useState,
|
|
45
|
+
} from 'react';
|
|
46
|
+
import { useExplorerProps, useExplorerState } from '.';
|
|
47
|
+
import { SearchInput } from './search-input';
|
|
48
|
+
|
|
49
|
+
import { errorToast, successToast } from '@lib/components/toast';
|
|
50
|
+
import {
|
|
51
|
+
ActionButton,
|
|
52
|
+
ActionForm,
|
|
53
|
+
Button,
|
|
54
|
+
Checkbox,
|
|
55
|
+
cn,
|
|
56
|
+
Content,
|
|
57
|
+
Dialog,
|
|
58
|
+
DropdownMenu,
|
|
59
|
+
DropdownMenuContent,
|
|
60
|
+
DropdownMenuItem,
|
|
61
|
+
DropdownMenuSeparator,
|
|
62
|
+
DropdownMenuTrigger,
|
|
63
|
+
Fence,
|
|
64
|
+
IconButton,
|
|
65
|
+
Select,
|
|
66
|
+
} from '@lib/components/ui';
|
|
67
|
+
import { SearchFilter, useNamespacesQuery } from '@lib/hooks/explorer';
|
|
68
|
+
import { useColumnVisibility } from '@lib/hooks/useColumnVisibility';
|
|
69
|
+
import { useLocalStorage } from '@lib/hooks/useLocalStorage';
|
|
70
|
+
import { SchemaAttr, SchemaNamespace } from '@lib/types';
|
|
71
|
+
import { formatBytes } from '@lib/utils/format';
|
|
72
|
+
import { getTableWidthSize } from '@lib/utils/tableWidthSize';
|
|
73
|
+
import { ArrowRightFromLine } from 'lucide-react';
|
|
74
|
+
import { TableCell, TableHeader } from './table-components';
|
|
75
|
+
import { ViewSettings } from './view-settings';
|
|
76
|
+
|
|
77
|
+
import { isObject } from 'lodash';
|
|
78
|
+
import { EditNamespaceDialog } from './edit-namespace-dialog';
|
|
79
|
+
import { EditRowDialog } from './edit-row-dialog';
|
|
80
|
+
|
|
81
|
+
const fallbackItems: any[] = [];
|
|
82
|
+
|
|
83
|
+
export type TableColMeta = {
|
|
84
|
+
sortable?: boolean;
|
|
85
|
+
disablePadding: boolean;
|
|
86
|
+
isLink?: boolean;
|
|
87
|
+
attr: SchemaAttr;
|
|
88
|
+
copyable?: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const InnerExplorer: React.FC<{
|
|
92
|
+
db: InstantReactAbstractDatabase<any, any>;
|
|
93
|
+
namespaces: SchemaNamespace[];
|
|
94
|
+
}> = ({ db, namespaces }) => {
|
|
95
|
+
const { explorerState, history } = useExplorerState();
|
|
96
|
+
const explorerProps = useExplorerProps();
|
|
97
|
+
|
|
98
|
+
const currentNav = explorerState;
|
|
99
|
+
const selectedNamespace = namespaces.find(
|
|
100
|
+
(ns) => ns.id === currentNav.namespace,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const [limit, setLimit] = useState(50);
|
|
104
|
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
105
|
+
const [uploadingFile, setUploadingFile] = useState(false);
|
|
106
|
+
const [customPath, setCustomPath] = useState('');
|
|
107
|
+
const [deleteDataConfirmationOpen, setDeleteDataConfirmationOpen] =
|
|
108
|
+
useState(false);
|
|
109
|
+
const [editNs, setEditNs] = useState<SchemaNamespace | null>(null);
|
|
110
|
+
const [editableRowId, setEditableRowId] = useState<string | null>(null);
|
|
111
|
+
const [addItemDialogOpen, setAddItemDialogOpen] = useState(false);
|
|
112
|
+
const nsRef = useRef<HTMLDivElement>(null);
|
|
113
|
+
const lastSelectedIdRef = useRef<string | null>(null);
|
|
114
|
+
const [offsets, setOffsets] = useState<{ [namespace: string]: number }>({});
|
|
115
|
+
|
|
116
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
117
|
+
const isSystemCatalogNs = selectedNamespace?.name?.startsWith('$') ?? false;
|
|
118
|
+
const sanitizedNsName = selectedNamespace?.name ?? '';
|
|
119
|
+
const readOnlyNs =
|
|
120
|
+
isSystemCatalogNs && !['$users', '$files'].includes(sanitizedNsName);
|
|
121
|
+
const offset = offsets[sanitizedNsName] || 0;
|
|
122
|
+
|
|
123
|
+
const sortAttr = currentNav?.sortAttr || 'serverCreatedAt';
|
|
124
|
+
const sortAsc = currentNav?.sortAsc ?? true;
|
|
125
|
+
|
|
126
|
+
const handleRangeSelection = (currentId: string, checked: boolean) => {
|
|
127
|
+
const allItemIds = table.options.data.map((i) => i.id as string);
|
|
128
|
+
const currentIndex = allItemIds.indexOf(currentId);
|
|
129
|
+
const lastSelectedIndex = allItemIds.indexOf(lastSelectedIdRef.current!);
|
|
130
|
+
const [start, end] = [
|
|
131
|
+
Math.min(currentIndex, lastSelectedIndex),
|
|
132
|
+
Math.max(currentIndex, lastSelectedIndex),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
setCheckedIds((prev) => {
|
|
136
|
+
const newCheckedIds = { ...prev };
|
|
137
|
+
for (let i = start; i <= end; i++) {
|
|
138
|
+
const id = allItemIds[i];
|
|
139
|
+
if (checked) {
|
|
140
|
+
newCheckedIds[id] = true;
|
|
141
|
+
} else {
|
|
142
|
+
delete newCheckedIds[id];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return newCheckedIds;
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const [searchFilters, setSearchFilters] = useState<SearchFilter[]>([]);
|
|
150
|
+
|
|
151
|
+
const { itemsRes, allCount } = useNamespacesQuery(
|
|
152
|
+
db,
|
|
153
|
+
selectedNamespace,
|
|
154
|
+
currentNav?.where,
|
|
155
|
+
currentNav?.filters || searchFilters,
|
|
156
|
+
limit,
|
|
157
|
+
offset,
|
|
158
|
+
sortAttr,
|
|
159
|
+
sortAsc,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const allItems =
|
|
163
|
+
itemsRes.data?.[selectedNamespace?.name ?? ''] ?? fallbackItems;
|
|
164
|
+
|
|
165
|
+
function getSelectedRows(
|
|
166
|
+
allItems: ({ id: string } & Record<string, any>)[],
|
|
167
|
+
checkedIds: Record<string, true | false>,
|
|
168
|
+
) {
|
|
169
|
+
return allItems.filter((item) => checkedIds[item.id]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const numPages = allCount ? Math.ceil(allCount / limit) : 1;
|
|
173
|
+
|
|
174
|
+
const currentPage = offset / limit + 1;
|
|
175
|
+
|
|
176
|
+
const [localDates, setLocalDates] = useLocalStorage('localDates', false);
|
|
177
|
+
|
|
178
|
+
const handleUploadFile = async () => {
|
|
179
|
+
try {
|
|
180
|
+
setUploadingFile(true);
|
|
181
|
+
if (selectedFiles.length === 0) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const [file] = selectedFiles;
|
|
186
|
+
const success = await upload(
|
|
187
|
+
explorerProps.adminToken,
|
|
188
|
+
explorerProps.appId,
|
|
189
|
+
file,
|
|
190
|
+
customPath,
|
|
191
|
+
explorerProps.apiURI,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (success) {
|
|
195
|
+
setSelectedFiles([]);
|
|
196
|
+
setCustomPath('');
|
|
197
|
+
fileInputRef.current && (fileInputRef.current.value = '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// await refreshFiles();
|
|
201
|
+
successToast('Successfully uploaded!');
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
console.error('Failed to upload:', err);
|
|
204
|
+
errorToast(`Failed to upload: ${err.body.message}`);
|
|
205
|
+
} finally {
|
|
206
|
+
setUploadingFile(false);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const tableItems = useMemo(() => {
|
|
211
|
+
return allItems;
|
|
212
|
+
}, [allItems]);
|
|
213
|
+
|
|
214
|
+
const tableRef = useRef<HTMLDivElement>(null);
|
|
215
|
+
const [leftShadowOpacity, setLeftShadowOpacity] = useState(0);
|
|
216
|
+
const [rightShadowOpacity, setRightShadowOpacity] = useState(1);
|
|
217
|
+
const [tableSmallerThanViewport, setTableSmallerThanViewport] =
|
|
218
|
+
useState(false);
|
|
219
|
+
|
|
220
|
+
const setMinViableColWidth = (columnId: string) => {
|
|
221
|
+
// for some reason the id column wants to resize bigger
|
|
222
|
+
if (table?.getColumn(columnId)?.columnDef.header === 'id') {
|
|
223
|
+
setColumnWidth(columnId, 285);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const size = getTableWidthSize(columnId, 800);
|
|
227
|
+
setColumnWidth(columnId, size);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const setColumnWidth = (columnId: string, width = 200) => {
|
|
231
|
+
if (!selectedNamespace) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const result: Record<string, number> = {};
|
|
235
|
+
selectedNamespace?.attrs.forEach((attr) => {
|
|
236
|
+
result[attr.id + attr.name] =
|
|
237
|
+
table.getColumn(attr.id + attr.name)?.getSize() || 0;
|
|
238
|
+
});
|
|
239
|
+
table.setColumnSizing({
|
|
240
|
+
...result,
|
|
241
|
+
[columnId]: width,
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const columns = useMemo(() => {
|
|
246
|
+
const result: ColumnDef<any>[] = [];
|
|
247
|
+
|
|
248
|
+
result.push({
|
|
249
|
+
id: 'select-col',
|
|
250
|
+
enableSorting: false,
|
|
251
|
+
accessorFn: () => null,
|
|
252
|
+
enableHiding: false,
|
|
253
|
+
enableResizing: false,
|
|
254
|
+
size: 52,
|
|
255
|
+
header: ({ table }) => {
|
|
256
|
+
return (
|
|
257
|
+
<Checkbox
|
|
258
|
+
className="relative z-10 text-[#2563EB] dark:checked:border-[#2563EB] dark:checked:bg-[#2563EB]"
|
|
259
|
+
style={{
|
|
260
|
+
pointerEvents: 'auto',
|
|
261
|
+
}}
|
|
262
|
+
checked={table.getIsAllRowsSelected()}
|
|
263
|
+
onChange={(checked) => {
|
|
264
|
+
if (checked) {
|
|
265
|
+
table.toggleAllRowsSelected();
|
|
266
|
+
// Use the first item as the last selected ID
|
|
267
|
+
if (allItems.length > 0) {
|
|
268
|
+
lastSelectedIdRef.current = allItems[0].id as string;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
setCheckedIds({});
|
|
272
|
+
lastSelectedIdRef.current = null;
|
|
273
|
+
}
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
cell: ({ row, column }) => {
|
|
279
|
+
return (
|
|
280
|
+
<div className="flex h-1 justify-around gap-2">
|
|
281
|
+
<Checkbox
|
|
282
|
+
className="relative z-10 text-[#2563EB] dark:checked:border-[#2563EB] dark:checked:bg-[#2563EB]"
|
|
283
|
+
checked={row.getIsSelected()}
|
|
284
|
+
onChange={(_, e) => {
|
|
285
|
+
const isShiftPressed = e.nativeEvent
|
|
286
|
+
? (e.nativeEvent as MouseEvent).shiftKey
|
|
287
|
+
: false;
|
|
288
|
+
|
|
289
|
+
if (isShiftPressed && lastSelectedIdRef.current) {
|
|
290
|
+
handleRangeSelection(row.id as string, e.target.checked);
|
|
291
|
+
} else {
|
|
292
|
+
// Regular single click selection
|
|
293
|
+
setCheckedIds((prev) => {
|
|
294
|
+
const newCheckedIds = { ...prev };
|
|
295
|
+
if (e.target.checked) {
|
|
296
|
+
newCheckedIds[row.id] = true;
|
|
297
|
+
} else {
|
|
298
|
+
delete newCheckedIds[row.id];
|
|
299
|
+
}
|
|
300
|
+
return newCheckedIds;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
lastSelectedIdRef.current = row.id;
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
{readOnlyNs ? null : (
|
|
308
|
+
<button
|
|
309
|
+
className="translate-y-0.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
310
|
+
onClick={() => setEditableRowId(row.id)}
|
|
311
|
+
>
|
|
312
|
+
<PencilSquareIcon className="h-4 w-4 text-neutral-500 dark:text-neutral-400" />
|
|
313
|
+
</button>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
selectedNamespace?.attrs?.forEach((attr) => {
|
|
321
|
+
result.push({
|
|
322
|
+
id: attr.id + attr.name,
|
|
323
|
+
header: attr.name,
|
|
324
|
+
enableSorting: true,
|
|
325
|
+
enableResizing: true,
|
|
326
|
+
accessorFn: (row) => row[attr.name],
|
|
327
|
+
meta: {
|
|
328
|
+
sortable: attr.sortable || attr.name === 'id',
|
|
329
|
+
copyable: true,
|
|
330
|
+
isLink: attr.type === 'ref',
|
|
331
|
+
attr,
|
|
332
|
+
disablePadding: attr.namespace === '$files' && attr.name === 'url',
|
|
333
|
+
} satisfies TableColMeta,
|
|
334
|
+
cell: (info) => {
|
|
335
|
+
if (
|
|
336
|
+
info.row.original[attr.name] === null ||
|
|
337
|
+
info.row.original[attr.name] === undefined
|
|
338
|
+
) {
|
|
339
|
+
return <div className="h-1">-</div>;
|
|
340
|
+
}
|
|
341
|
+
if (attr.type === 'ref') {
|
|
342
|
+
const linkCount = info.row.original[attr.name].length;
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
className={cn(
|
|
346
|
+
'h-1 translate-y-0.5',
|
|
347
|
+
linkCount < 1 && 'opacity-50',
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
350
|
+
{linkCount} link{linkCount === 1 ? '' : 's'}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (attr.namespace === '$files') {
|
|
356
|
+
if (attr.name === 'url') {
|
|
357
|
+
return (
|
|
358
|
+
<a
|
|
359
|
+
className="h-full w-full pl-2 align-middle text-xs font-bold underline hover:text-black dark:hover:text-white"
|
|
360
|
+
href={info.row.original['url'] as string}
|
|
361
|
+
target="_blank"
|
|
362
|
+
>
|
|
363
|
+
View File
|
|
364
|
+
</a>
|
|
365
|
+
);
|
|
366
|
+
} else if (attr.name === 'size') {
|
|
367
|
+
return formatBytes(info.row.original[attr.name]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (attr.checkedDataType === 'boolean') {
|
|
372
|
+
return info.row.original[attr.name] ? 'true' : 'false';
|
|
373
|
+
}
|
|
374
|
+
if (attr.checkedDataType === 'date') {
|
|
375
|
+
const coerced = coerceToDate(info.row.original[attr.name]);
|
|
376
|
+
|
|
377
|
+
if (localDates) {
|
|
378
|
+
return coerced?.toLocaleString() || info.row.original[attr.name];
|
|
379
|
+
} else {
|
|
380
|
+
return info.row.original[attr.name];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (isObject(info.row.original[attr.name])) {
|
|
384
|
+
return <Val data={info.row.original[attr.name]}></Val>;
|
|
385
|
+
}
|
|
386
|
+
return info.row.original[attr.name];
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return result;
|
|
392
|
+
}, [selectedNamespace?.attrs, localDates]);
|
|
393
|
+
|
|
394
|
+
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
|
|
395
|
+
|
|
396
|
+
const distributeRemainingWidth = () => {
|
|
397
|
+
const result: Record<string, number> = table.getState().columnSizing;
|
|
398
|
+
|
|
399
|
+
const fullWidth = tableRef.current?.clientWidth || -1;
|
|
400
|
+
|
|
401
|
+
const totalWidth = Object.values(result).reduce(
|
|
402
|
+
(acc, width) => acc + width,
|
|
403
|
+
0,
|
|
404
|
+
);
|
|
405
|
+
const remainingWidth = fullWidth - 52 - totalWidth;
|
|
406
|
+
|
|
407
|
+
if (remainingWidth > 0) {
|
|
408
|
+
const numColumns = Object.keys(result).length;
|
|
409
|
+
const extraWidth = remainingWidth / numColumns;
|
|
410
|
+
|
|
411
|
+
Object.keys(result).forEach((key) => {
|
|
412
|
+
result[key] += extraWidth;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
setTableSmallerThanViewport(false);
|
|
416
|
+
table.setColumnSizing(() => {
|
|
417
|
+
return { ...result };
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const columnResizeMode = 'onChange';
|
|
422
|
+
|
|
423
|
+
const columnResizeDirection = 'ltr';
|
|
424
|
+
|
|
425
|
+
const colVisiblity = useColumnVisibility({
|
|
426
|
+
appId: explorerProps.appId,
|
|
427
|
+
attrs: selectedNamespace?.attrs,
|
|
428
|
+
namespaceId: selectedNamespace?.id,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const [columnOrder, setColumnOrder] = useState<string[]>(() =>
|
|
432
|
+
columns.map((c) => c.id!),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Sync columnOrder when namespace changes
|
|
436
|
+
useLayoutEffect(() => {
|
|
437
|
+
if (selectedNamespace?.attrs) {
|
|
438
|
+
const savedOrder = localStorage.getItem(
|
|
439
|
+
`order-${selectedNamespace.id}-${explorerProps.appId}`,
|
|
440
|
+
);
|
|
441
|
+
if (savedOrder) {
|
|
442
|
+
setColumnOrder(JSON.parse(savedOrder));
|
|
443
|
+
} else {
|
|
444
|
+
const newOrder = selectedNamespace.attrs.map(
|
|
445
|
+
(attr) => attr.id + attr.name,
|
|
446
|
+
);
|
|
447
|
+
setColumnOrder(['select-col', ...newOrder]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}, [selectedNamespace?.attrs]);
|
|
451
|
+
|
|
452
|
+
// Persist columnOrder to localStorage when it changes
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (selectedNamespace?.id) {
|
|
455
|
+
localStorage.setItem(
|
|
456
|
+
`order-${selectedNamespace.id}-${explorerProps.appId}`,
|
|
457
|
+
JSON.stringify(columnOrder),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}, [columnOrder, selectedNamespace?.id]);
|
|
461
|
+
|
|
462
|
+
const [checkedIds, setCheckedIds] = useState<Record<string, true | false>>(
|
|
463
|
+
{},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// Clear selection when namespace changes
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
setCheckedIds({});
|
|
469
|
+
lastSelectedIdRef.current = null;
|
|
470
|
+
}, [selectedNamespace?.id]);
|
|
471
|
+
|
|
472
|
+
const numItemsSelected = Object.keys(checkedIds).length;
|
|
473
|
+
const rowText =
|
|
474
|
+
sanitizedNsName === '$files'
|
|
475
|
+
? numItemsSelected === 1
|
|
476
|
+
? 'file'
|
|
477
|
+
: 'files'
|
|
478
|
+
: numItemsSelected === 1
|
|
479
|
+
? 'row'
|
|
480
|
+
: 'rows';
|
|
481
|
+
|
|
482
|
+
const table = useReactTable({
|
|
483
|
+
columnResizeDirection,
|
|
484
|
+
columnResizeMode,
|
|
485
|
+
onColumnVisibilityChange: colVisiblity.setVisibility,
|
|
486
|
+
columns: columns,
|
|
487
|
+
data: tableItems,
|
|
488
|
+
enableColumnResizing: true,
|
|
489
|
+
enableRowSelection: true,
|
|
490
|
+
getCoreRowModel: getCoreRowModel(),
|
|
491
|
+
getRowId: (row) => row.id,
|
|
492
|
+
onColumnOrderChange: setColumnOrder,
|
|
493
|
+
onRowSelectionChange: setCheckedIds,
|
|
494
|
+
onColumnSizingChange: setColumnSizing,
|
|
495
|
+
state: {
|
|
496
|
+
columnSizing: columnSizing,
|
|
497
|
+
columnOrder,
|
|
498
|
+
columnVisibility: colVisiblity.visibility,
|
|
499
|
+
rowSelection: checkedIds,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const [isShiftPressed, setIsShiftPressed] = useState(false);
|
|
504
|
+
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
507
|
+
if (e.key === 'Shift') {
|
|
508
|
+
setIsShiftPressed(true);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
513
|
+
if (e.key === 'Shift') {
|
|
514
|
+
setIsShiftPressed(false);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const handleWindowBlur = () => {
|
|
519
|
+
setIsShiftPressed(false);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
523
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
524
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
525
|
+
|
|
526
|
+
return () => {
|
|
527
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
528
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
529
|
+
window.removeEventListener('blur-sm', handleWindowBlur);
|
|
530
|
+
};
|
|
531
|
+
}, []);
|
|
532
|
+
|
|
533
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
534
|
+
|
|
535
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
536
|
+
const { active, over } = event;
|
|
537
|
+
// Prevent dragging the select column or dragging over it
|
|
538
|
+
if (
|
|
539
|
+
active &&
|
|
540
|
+
over &&
|
|
541
|
+
active.id !== over.id &&
|
|
542
|
+
active.id !== 'select-col' &&
|
|
543
|
+
over.id !== 'select-col'
|
|
544
|
+
) {
|
|
545
|
+
setColumnOrder((columnOrder) => {
|
|
546
|
+
const oldIndex = columnOrder.indexOf(active.id as string);
|
|
547
|
+
const newIndex = columnOrder.indexOf(over.id as string);
|
|
548
|
+
return arrayMove(columnOrder, oldIndex, newIndex); //this is just a splice util
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const selectedEditableItem = useMemo(
|
|
554
|
+
() => allItems.find((i) => i.id === editableRowId),
|
|
555
|
+
[allItems.length, editableRowId],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Handle scroll to update shadow opacity
|
|
559
|
+
useEffect(() => {
|
|
560
|
+
const tableElement = tableRef.current;
|
|
561
|
+
if (!tableElement) return;
|
|
562
|
+
|
|
563
|
+
const handleScroll = () => {
|
|
564
|
+
const tableWidth = table.getCenterTotalSize();
|
|
565
|
+
const viewportWidth = tableElement.clientWidth;
|
|
566
|
+
|
|
567
|
+
setTableSmallerThanViewport(tableWidth < viewportWidth - 5);
|
|
568
|
+
|
|
569
|
+
const { scrollLeft, scrollWidth, clientWidth } = tableElement;
|
|
570
|
+
const maxScroll = scrollWidth - clientWidth;
|
|
571
|
+
if (maxScroll <= 0) {
|
|
572
|
+
setLeftShadowOpacity(0);
|
|
573
|
+
setRightShadowOpacity(0);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const leftOpacity = Math.min(scrollLeft / 30, 1);
|
|
577
|
+
setLeftShadowOpacity(leftOpacity);
|
|
578
|
+
|
|
579
|
+
const rightOpacity = Math.min((maxScroll - scrollLeft) / 30, 1);
|
|
580
|
+
setRightShadowOpacity(rightOpacity);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
handleScroll();
|
|
584
|
+
tableElement.addEventListener('scroll', handleScroll);
|
|
585
|
+
|
|
586
|
+
const resizeObserver = new ResizeObserver(handleScroll);
|
|
587
|
+
resizeObserver.observe(tableElement);
|
|
588
|
+
const tableContent = tableElement.firstElementChild;
|
|
589
|
+
if (tableContent) {
|
|
590
|
+
resizeObserver.observe(tableContent);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
window.addEventListener('resize', handleScroll);
|
|
594
|
+
|
|
595
|
+
return () => {
|
|
596
|
+
tableElement.removeEventListener('scroll', handleScroll);
|
|
597
|
+
resizeObserver.disconnect();
|
|
598
|
+
window.removeEventListener('resize', handleScroll);
|
|
599
|
+
};
|
|
600
|
+
}, [selectedNamespace, tableItems]);
|
|
601
|
+
|
|
602
|
+
const sensors = useSensors(
|
|
603
|
+
useSensor(MouseSensor, {}),
|
|
604
|
+
useSensor(TouchSensor, {}),
|
|
605
|
+
useSensor(KeyboardSensor, {}),
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// history.items is a list of PAST histories, not including current
|
|
609
|
+
const showBackButton = history.items.length >= 1;
|
|
610
|
+
|
|
611
|
+
const transformAttrNameToWidth = (name: string) => {
|
|
612
|
+
if (name === 'id') {
|
|
613
|
+
return 140;
|
|
614
|
+
}
|
|
615
|
+
if (name === 'url') {
|
|
616
|
+
return 120;
|
|
617
|
+
}
|
|
618
|
+
return name.length * 7.2 + 50;
|
|
619
|
+
};
|
|
620
|
+
// evenly space width of columns on first render
|
|
621
|
+
useLayoutEffect(() => {
|
|
622
|
+
if (selectedNamespace?.id) {
|
|
623
|
+
if (
|
|
624
|
+
localStorage.getItem(
|
|
625
|
+
`$sizing-${selectedNamespace.id}-${explorerProps.appId}`,
|
|
626
|
+
)
|
|
627
|
+
) {
|
|
628
|
+
const savedSizing = JSON.parse(
|
|
629
|
+
localStorage.getItem(
|
|
630
|
+
`sizing-${selectedNamespace.id}-${explorerProps.appId}`,
|
|
631
|
+
) || '{}',
|
|
632
|
+
);
|
|
633
|
+
table.setColumnSizing(() => {
|
|
634
|
+
return { ...savedSizing };
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const fullWidth = tableRef.current?.clientWidth || -1;
|
|
640
|
+
const result: Record<string, number> = {};
|
|
641
|
+
|
|
642
|
+
selectedNamespace?.attrs.forEach((attr) => {
|
|
643
|
+
result[attr.id + attr.name] = transformAttrNameToWidth(attr.name);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const totalWidth = Object.values(result).reduce(
|
|
647
|
+
(acc, width) => acc + width,
|
|
648
|
+
0,
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Distribute the remaining width equally
|
|
652
|
+
const remainingWidth = fullWidth - 52 - totalWidth;
|
|
653
|
+
if (remainingWidth > 0) {
|
|
654
|
+
const numColumns = Object.keys(result).length;
|
|
655
|
+
const extraWidth = remainingWidth / numColumns;
|
|
656
|
+
|
|
657
|
+
Object.keys(result).forEach((key) => {
|
|
658
|
+
result[key] += extraWidth;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
table.setColumnSizing(result);
|
|
663
|
+
}
|
|
664
|
+
}, [tableRef.current, selectedNamespace]);
|
|
665
|
+
|
|
666
|
+
if (!selectedNamespace) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const selectedNamespaceId = selectedNamespace.id;
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<>
|
|
674
|
+
<Dialog
|
|
675
|
+
open={deleteDataConfirmationOpen}
|
|
676
|
+
onClose={() => setDeleteDataConfirmationOpen(false)}
|
|
677
|
+
>
|
|
678
|
+
{selectedNamespace ? (
|
|
679
|
+
<ActionForm className="min flex flex-col gap-4">
|
|
680
|
+
<h5 className="flex text-lg font-bold">Delete {rowText}</h5>
|
|
681
|
+
|
|
682
|
+
<Content>
|
|
683
|
+
Deleting is an{' '}
|
|
684
|
+
<strong className="dark:text-white">
|
|
685
|
+
irreversible operation
|
|
686
|
+
</strong>{' '}
|
|
687
|
+
and will{' '}
|
|
688
|
+
<strong className="dark:text-white">
|
|
689
|
+
delete {numItemsSelected} {rowText}{' '}
|
|
690
|
+
</strong>
|
|
691
|
+
associated with{' '}
|
|
692
|
+
<strong className="dark:text-white">
|
|
693
|
+
{selectedNamespace.name}
|
|
694
|
+
</strong>
|
|
695
|
+
.
|
|
696
|
+
</Content>
|
|
697
|
+
|
|
698
|
+
<ActionButton
|
|
699
|
+
type="submit"
|
|
700
|
+
disabled={readOnlyNs}
|
|
701
|
+
label={`Delete ${rowText}`}
|
|
702
|
+
submitLabel={`Deleting ${rowText}...`}
|
|
703
|
+
errorMessage={`Failed to delete ${rowText}`}
|
|
704
|
+
className="border-red-500 text-red-500"
|
|
705
|
+
title={
|
|
706
|
+
readOnlyNs
|
|
707
|
+
? `The ${selectedNamespace?.name} namespace is read-only.`
|
|
708
|
+
: undefined
|
|
709
|
+
}
|
|
710
|
+
onClick={async () => {
|
|
711
|
+
try {
|
|
712
|
+
if (selectedNamespace.name === '$files') {
|
|
713
|
+
const filenames = allItems
|
|
714
|
+
.filter((i) => i.id in checkedIds)
|
|
715
|
+
.map((i) => i.path as string);
|
|
716
|
+
await bulkDeleteFiles(
|
|
717
|
+
explorerProps.adminToken,
|
|
718
|
+
explorerProps.appId,
|
|
719
|
+
filenames,
|
|
720
|
+
explorerProps.apiURI,
|
|
721
|
+
);
|
|
722
|
+
} else {
|
|
723
|
+
await db.transact(
|
|
724
|
+
Object.keys(checkedIds).map((id) =>
|
|
725
|
+
tx[selectedNamespace.name][id].delete(),
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
} catch (error: any) {
|
|
730
|
+
const errorMessage = error.message;
|
|
731
|
+
errorToast(
|
|
732
|
+
`Failed to delete ${rowText}${errorMessage ? `: ${errorMessage}` : ''}`,
|
|
733
|
+
);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
setCheckedIds({});
|
|
738
|
+
setDeleteDataConfirmationOpen(false);
|
|
739
|
+
}}
|
|
740
|
+
/>
|
|
741
|
+
</ActionForm>
|
|
742
|
+
) : null}
|
|
743
|
+
</Dialog>
|
|
744
|
+
<Dialog
|
|
745
|
+
open={addItemDialogOpen}
|
|
746
|
+
onClose={() => setAddItemDialogOpen(false)}
|
|
747
|
+
>
|
|
748
|
+
{selectedNamespace ? (
|
|
749
|
+
<EditRowDialog
|
|
750
|
+
db={db}
|
|
751
|
+
item={{}}
|
|
752
|
+
namespace={selectedNamespace}
|
|
753
|
+
onClose={() => setAddItemDialogOpen(false)}
|
|
754
|
+
/>
|
|
755
|
+
) : null}
|
|
756
|
+
</Dialog>
|
|
757
|
+
<Dialog
|
|
758
|
+
open={!!selectedEditableItem}
|
|
759
|
+
onClose={() => setEditableRowId(null)}
|
|
760
|
+
>
|
|
761
|
+
{!!selectedNamespace && !!selectedEditableItem ? (
|
|
762
|
+
<EditRowDialog
|
|
763
|
+
db={db}
|
|
764
|
+
namespace={selectedNamespace}
|
|
765
|
+
item={selectedEditableItem}
|
|
766
|
+
onClose={() => setEditableRowId(null)}
|
|
767
|
+
/>
|
|
768
|
+
) : null}
|
|
769
|
+
</Dialog>
|
|
770
|
+
<Dialog
|
|
771
|
+
stopFocusPropagation={true}
|
|
772
|
+
open={Boolean(editNs)}
|
|
773
|
+
onClose={() => setEditNs(null)}
|
|
774
|
+
>
|
|
775
|
+
{selectedNamespace ? (
|
|
776
|
+
<EditNamespaceDialog
|
|
777
|
+
readOnly={readOnlyNs}
|
|
778
|
+
isSystemCatalogNs={isSystemCatalogNs}
|
|
779
|
+
db={db}
|
|
780
|
+
namespace={selectedNamespace}
|
|
781
|
+
namespaces={namespaces ?? []}
|
|
782
|
+
onClose={(p) => {
|
|
783
|
+
setEditNs(null);
|
|
784
|
+
if (p?.ok) {
|
|
785
|
+
history.push({ namespace: namespaces?.[0].id });
|
|
786
|
+
}
|
|
787
|
+
}}
|
|
788
|
+
/>
|
|
789
|
+
) : null}
|
|
790
|
+
</Dialog>
|
|
791
|
+
<div className="flex flex-1 grow flex-col overflow-hidden bg-white dark:bg-neutral-800">
|
|
792
|
+
<div className="flex items-center overflow-hidden border-b border-b-gray-200 dark:border-neutral-700">
|
|
793
|
+
<div className="flex flex-1 flex-col justify-between py-2 md:flex-row md:items-center">
|
|
794
|
+
<div className="flex items-center overflow-hidden border-b px-2 py-1 pl-4 md:border-b-0 dark:border-neutral-700">
|
|
795
|
+
{showBackButton ? (
|
|
796
|
+
<ArrowLeftIcon
|
|
797
|
+
className="mr-4 inline cursor-pointer"
|
|
798
|
+
height="1rem"
|
|
799
|
+
onClick={() => history.pop()}
|
|
800
|
+
/>
|
|
801
|
+
) : null}
|
|
802
|
+
{currentNav.where ? (
|
|
803
|
+
<XMarkIcon
|
|
804
|
+
className="mr-4 inline cursor-pointer"
|
|
805
|
+
height="1rem"
|
|
806
|
+
onClick={() => {
|
|
807
|
+
history.push(
|
|
808
|
+
{
|
|
809
|
+
namespace: selectedNamespace.id,
|
|
810
|
+
},
|
|
811
|
+
true,
|
|
812
|
+
);
|
|
813
|
+
}}
|
|
814
|
+
/>
|
|
815
|
+
) : null}
|
|
816
|
+
<div className="text-ellipses shrink truncate overflow-hidden font-mono text-xs whitespace-nowrap dark:text-white">
|
|
817
|
+
<strong>{selectedNamespace.name}</strong>{' '}
|
|
818
|
+
{currentNav.where ? (
|
|
819
|
+
<>
|
|
820
|
+
{' '}
|
|
821
|
+
where <strong>{currentNav.where[0]}</strong> ={' '}
|
|
822
|
+
<em className="rounded-xs border bg-white px-1 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white">
|
|
823
|
+
{JSON.stringify(currentNav.where[1])}
|
|
824
|
+
</em>
|
|
825
|
+
</>
|
|
826
|
+
) : null}
|
|
827
|
+
{currentNav?.filters?.length ? (
|
|
828
|
+
<span
|
|
829
|
+
title={currentNav.filters
|
|
830
|
+
.map(([attr, op, search]) => `${attr} ${op} ${search}`)
|
|
831
|
+
.join(' || ')}
|
|
832
|
+
>
|
|
833
|
+
{currentNav.filters.map(([attr, op, search], i) => (
|
|
834
|
+
<span key={attr}>
|
|
835
|
+
<em className="rounded-xs border bg-white px-1 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white">
|
|
836
|
+
{attr} {op} {search}
|
|
837
|
+
</em>
|
|
838
|
+
{currentNav?.filters?.length &&
|
|
839
|
+
i < currentNav.filters.length - 1
|
|
840
|
+
? ' || '
|
|
841
|
+
: null}
|
|
842
|
+
</span>
|
|
843
|
+
))}
|
|
844
|
+
</span>
|
|
845
|
+
) : null}
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
<div className="flex justify-between gap-2 px-2 py-1 md:justify-start">
|
|
849
|
+
<Button
|
|
850
|
+
className="rounded-sm dark:bg-neutral-700/50"
|
|
851
|
+
variant="secondary"
|
|
852
|
+
size="mini"
|
|
853
|
+
onClick={() => {
|
|
854
|
+
setEditNs(selectedNamespace);
|
|
855
|
+
}}
|
|
856
|
+
>
|
|
857
|
+
Edit Schema
|
|
858
|
+
</Button>
|
|
859
|
+
<SearchInput
|
|
860
|
+
key={selectedNamespaceId}
|
|
861
|
+
onSearchChange={(filters) => setSearchFilters(filters)}
|
|
862
|
+
attrs={selectedNamespace?.attrs}
|
|
863
|
+
initialFilters={currentNav?.filters || []}
|
|
864
|
+
/>
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
{selectedNamespace.name === '$files' ? (
|
|
869
|
+
<div className="flex gap-2 px-2 py-2">
|
|
870
|
+
<div className="flex w-full gap-2">
|
|
871
|
+
<div className="flex shrink-0 gap-2">
|
|
872
|
+
<input
|
|
873
|
+
ref={fileInputRef}
|
|
874
|
+
type="file"
|
|
875
|
+
className="flex cursor-pointer rounded-sm border border-neutral-200 bg-transparent px-1 pt-1.5 text-sm shadow-xs transition-colors file:rounded-xs file:border-none file:border-neutral-200 file:bg-transparent file:p-2 file:pt-1 file:text-sm file:font-medium file:shadow-none placeholder:text-neutral-500 focus-visible:ring-1 focus-visible:ring-neutral-950 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white dark:file:border-neutral-700 dark:file:text-white dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-400"
|
|
876
|
+
onChange={(e: React.ChangeEvent<any>) => {
|
|
877
|
+
const files = e.target.files;
|
|
878
|
+
setSelectedFiles(files);
|
|
879
|
+
if (files?.[0]) {
|
|
880
|
+
setCustomPath(files[0].name);
|
|
881
|
+
}
|
|
882
|
+
}}
|
|
883
|
+
/>
|
|
884
|
+
<Button
|
|
885
|
+
variant="primary"
|
|
886
|
+
disabled={selectedFiles.length === 0}
|
|
887
|
+
size="mini"
|
|
888
|
+
loading={uploadingFile}
|
|
889
|
+
onClick={handleUploadFile}
|
|
890
|
+
className="rounded-sm"
|
|
891
|
+
>
|
|
892
|
+
{uploadingFile ? 'Uploading...' : 'Upload file'}
|
|
893
|
+
</Button>
|
|
894
|
+
</div>
|
|
895
|
+
<div className="relative flex max-w-[67vw] min-w-0 flex-1 rounded-sm border border-neutral-200 focus-within:ring-2 focus-within:ring-blue-700 dark:border-neutral-700 dark:focus-within:ring-blue-500">
|
|
896
|
+
<span className="absolute inset-y-0 left-0 flex items-center rounded-l bg-neutral-100 px-3 text-sm text-neutral-500 dark:bg-neutral-700 dark:text-neutral-400">
|
|
897
|
+
File Path:
|
|
898
|
+
</span>
|
|
899
|
+
<input
|
|
900
|
+
type="text"
|
|
901
|
+
placeholder="Enter a custom path (optional)"
|
|
902
|
+
value={customPath}
|
|
903
|
+
onChange={(e) => setCustomPath(e.target.value)}
|
|
904
|
+
className="h-9 w-full rounded-sm border-0 bg-transparent py-1 pr-3 pl-24 text-sm ring-0 placeholder:text-neutral-500 focus:outline-none dark:bg-neutral-800 dark:text-white dark:placeholder:text-neutral-400"
|
|
905
|
+
/>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
) : null}
|
|
910
|
+
<div className="flex items-center justify-start space-x-2 border-b border-b-gray-200 p-1 text-xs dark:border-neutral-700 dark:text-white">
|
|
911
|
+
{selectedNamespace.name !== '$files' ? (
|
|
912
|
+
<Button
|
|
913
|
+
disabled={readOnlyNs}
|
|
914
|
+
title={
|
|
915
|
+
readOnlyNs
|
|
916
|
+
? `The ${selectedNamespace?.name} namespace is read-only.`
|
|
917
|
+
: undefined
|
|
918
|
+
}
|
|
919
|
+
size="mini"
|
|
920
|
+
variant="secondary"
|
|
921
|
+
onClick={() => {
|
|
922
|
+
setAddItemDialogOpen(true);
|
|
923
|
+
}}
|
|
924
|
+
>
|
|
925
|
+
<PlusIcon width={12} />
|
|
926
|
+
Add row
|
|
927
|
+
</Button>
|
|
928
|
+
) : null}
|
|
929
|
+
<div
|
|
930
|
+
className={cn(
|
|
931
|
+
'px-1',
|
|
932
|
+
selectedNamespace.name === '$files' && 'pb-1',
|
|
933
|
+
)}
|
|
934
|
+
>
|
|
935
|
+
<Select
|
|
936
|
+
className="rounded-sm text-xs"
|
|
937
|
+
onChange={(opt) => {
|
|
938
|
+
if (!opt) return;
|
|
939
|
+
|
|
940
|
+
const newLimit = parseInt(opt.value, 10);
|
|
941
|
+
setLimit(newLimit);
|
|
942
|
+
history.push((prev) => ({
|
|
943
|
+
...prev,
|
|
944
|
+
limit: newLimit,
|
|
945
|
+
}));
|
|
946
|
+
}}
|
|
947
|
+
value={`${limit}`}
|
|
948
|
+
options={[
|
|
949
|
+
{ label: '25/page', value: '25' },
|
|
950
|
+
{ label: '50/page', value: '50' },
|
|
951
|
+
{ label: '100/page', value: '100' },
|
|
952
|
+
]}
|
|
953
|
+
/>
|
|
954
|
+
</div>
|
|
955
|
+
<div className="w-[62px]">
|
|
956
|
+
{allCount !== undefined &&
|
|
957
|
+
(allCount === 0 ? (
|
|
958
|
+
<>No Results</>
|
|
959
|
+
) : (
|
|
960
|
+
<>
|
|
961
|
+
{(currentPage - 1) * limit + 1} -{' '}
|
|
962
|
+
{Math.min(allCount, currentPage * limit)} of {allCount}
|
|
963
|
+
</>
|
|
964
|
+
))}
|
|
965
|
+
</div>
|
|
966
|
+
<button
|
|
967
|
+
className="flex items-center justify-center"
|
|
968
|
+
disabled={currentPage <= 1}
|
|
969
|
+
onClick={() => {
|
|
970
|
+
setOffsets({
|
|
971
|
+
...offsets,
|
|
972
|
+
[selectedNamespace.name]: Math.max(0, offset - limit),
|
|
973
|
+
});
|
|
974
|
+
history.push((prev) => ({
|
|
975
|
+
...prev,
|
|
976
|
+
page: Math.max(1, currentPage - 1),
|
|
977
|
+
}));
|
|
978
|
+
}}
|
|
979
|
+
>
|
|
980
|
+
<ArrowLeftIcon
|
|
981
|
+
className={cn('inline', {
|
|
982
|
+
'opacity-40': currentPage <= 1,
|
|
983
|
+
})}
|
|
984
|
+
height="1rem"
|
|
985
|
+
/>
|
|
986
|
+
</button>
|
|
987
|
+
<div className="flex items-center space-x-1 overflow-hidden">
|
|
988
|
+
{[...new Array(numPages)].map((_, i) => {
|
|
989
|
+
const page = i + 1;
|
|
990
|
+
if (
|
|
991
|
+
numPages > 6 &&
|
|
992
|
+
page !== 1 &&
|
|
993
|
+
page !== numPages &&
|
|
994
|
+
page !== currentPage &&
|
|
995
|
+
page !== currentPage - 1 &&
|
|
996
|
+
page !== currentPage + 1
|
|
997
|
+
) {
|
|
998
|
+
if (page === currentPage - 2 || page === currentPage + 2) {
|
|
999
|
+
return <div key={page}>...</div>;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
return (
|
|
1004
|
+
<button
|
|
1005
|
+
key={page}
|
|
1006
|
+
className={cn(
|
|
1007
|
+
'rounded-md px-3 py-1 text-neutral-600 dark:text-neutral-300',
|
|
1008
|
+
page === currentPage
|
|
1009
|
+
? 'bg-neutral-200 dark:bg-neutral-700'
|
|
1010
|
+
: 'hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
1011
|
+
)}
|
|
1012
|
+
onClick={() => {
|
|
1013
|
+
setOffsets({
|
|
1014
|
+
...offsets,
|
|
1015
|
+
[selectedNamespace.name]: i * limit,
|
|
1016
|
+
});
|
|
1017
|
+
history.push((prev) => ({
|
|
1018
|
+
...prev,
|
|
1019
|
+
page,
|
|
1020
|
+
}));
|
|
1021
|
+
}}
|
|
1022
|
+
disabled={page === currentPage}
|
|
1023
|
+
>
|
|
1024
|
+
{page}
|
|
1025
|
+
</button>
|
|
1026
|
+
);
|
|
1027
|
+
})}
|
|
1028
|
+
</div>
|
|
1029
|
+
<button
|
|
1030
|
+
className="flex items-center justify-center"
|
|
1031
|
+
disabled={currentPage >= numPages}
|
|
1032
|
+
onClick={() => {
|
|
1033
|
+
setOffsets({
|
|
1034
|
+
...offsets,
|
|
1035
|
+
[selectedNamespace.name]: offset + limit,
|
|
1036
|
+
});
|
|
1037
|
+
history.push((prev) => ({
|
|
1038
|
+
...prev,
|
|
1039
|
+
page: Math.min(numPages, currentPage + 1),
|
|
1040
|
+
}));
|
|
1041
|
+
}}
|
|
1042
|
+
>
|
|
1043
|
+
<ArrowRightIcon
|
|
1044
|
+
className={cn('inline', {
|
|
1045
|
+
'opacity-40': currentPage >= numPages,
|
|
1046
|
+
})}
|
|
1047
|
+
height="1rem"
|
|
1048
|
+
/>
|
|
1049
|
+
</button>
|
|
1050
|
+
{numItemsSelected > 0 && (
|
|
1051
|
+
<div className="flex items-center gap-2 pl-4">
|
|
1052
|
+
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
|
1053
|
+
<DropdownMenuTrigger>
|
|
1054
|
+
<Button
|
|
1055
|
+
onClick={() => {
|
|
1056
|
+
setDropdownOpen(true);
|
|
1057
|
+
}}
|
|
1058
|
+
variant="secondary"
|
|
1059
|
+
>
|
|
1060
|
+
<ArrowUpOnSquareIcon width={14} />
|
|
1061
|
+
Export ({numItemsSelected})
|
|
1062
|
+
</Button>
|
|
1063
|
+
</DropdownMenuTrigger>
|
|
1064
|
+
<DropdownMenuContent
|
|
1065
|
+
className="z-100"
|
|
1066
|
+
align="end"
|
|
1067
|
+
side="bottom"
|
|
1068
|
+
sideOffset={5}
|
|
1069
|
+
>
|
|
1070
|
+
<DropdownMenuItem
|
|
1071
|
+
onSelect={(e) => {
|
|
1072
|
+
e.preventDefault();
|
|
1073
|
+
const selectedRows = getSelectedRows(
|
|
1074
|
+
allItems,
|
|
1075
|
+
checkedIds,
|
|
1076
|
+
);
|
|
1077
|
+
// exportToCSV(
|
|
1078
|
+
// selectedRows,
|
|
1079
|
+
// columns,
|
|
1080
|
+
// selectedNamespace.name,
|
|
1081
|
+
// isShiftPressed,
|
|
1082
|
+
// );
|
|
1083
|
+
setDropdownOpen(false);
|
|
1084
|
+
}}
|
|
1085
|
+
className="flex items-center gap-2"
|
|
1086
|
+
>
|
|
1087
|
+
<Table width={12} />
|
|
1088
|
+
{isShiftPressed ? 'Download as CSV' : 'Copy as CSV'}
|
|
1089
|
+
</DropdownMenuItem>
|
|
1090
|
+
<DropdownMenuItem
|
|
1091
|
+
onSelect={(e) => {
|
|
1092
|
+
e.preventDefault();
|
|
1093
|
+
const selectedRows = getSelectedRows(
|
|
1094
|
+
allItems,
|
|
1095
|
+
checkedIds,
|
|
1096
|
+
);
|
|
1097
|
+
// exportToMarkdown(
|
|
1098
|
+
// selectedRows,
|
|
1099
|
+
// columns,
|
|
1100
|
+
// selectedNamespace.name,
|
|
1101
|
+
// isShiftPressed,
|
|
1102
|
+
// );
|
|
1103
|
+
setDropdownOpen(false);
|
|
1104
|
+
}}
|
|
1105
|
+
className="flex items-center gap-2"
|
|
1106
|
+
>
|
|
1107
|
+
<FileDown width={12} />
|
|
1108
|
+
{isShiftPressed
|
|
1109
|
+
? 'Download as Markdown'
|
|
1110
|
+
: 'Copy as Markdown'}
|
|
1111
|
+
</DropdownMenuItem>
|
|
1112
|
+
<DropdownMenuItem
|
|
1113
|
+
onSelect={(e) => {
|
|
1114
|
+
e.preventDefault();
|
|
1115
|
+
const selectedRows = getSelectedRows(
|
|
1116
|
+
allItems,
|
|
1117
|
+
checkedIds,
|
|
1118
|
+
);
|
|
1119
|
+
// exportToJSON(
|
|
1120
|
+
// selectedRows,
|
|
1121
|
+
// columns,
|
|
1122
|
+
// selectedNamespace.name,
|
|
1123
|
+
// isShiftPressed,
|
|
1124
|
+
// );
|
|
1125
|
+
setDropdownOpen(false);
|
|
1126
|
+
}}
|
|
1127
|
+
className="flex items-center gap-2"
|
|
1128
|
+
>
|
|
1129
|
+
<CurlyBraces width={12} />
|
|
1130
|
+
{isShiftPressed ? 'Download as JSON' : 'Copy as JSON'}
|
|
1131
|
+
</DropdownMenuItem>
|
|
1132
|
+
{!isShiftPressed && (
|
|
1133
|
+
<>
|
|
1134
|
+
<DropdownMenuSeparator />
|
|
1135
|
+
<DropdownMenuItem
|
|
1136
|
+
className="text-xs text-neutral-500 dark:text-neutral-400"
|
|
1137
|
+
disabled
|
|
1138
|
+
>
|
|
1139
|
+
Hold shift to download as file
|
|
1140
|
+
</DropdownMenuItem>
|
|
1141
|
+
</>
|
|
1142
|
+
)}
|
|
1143
|
+
</DropdownMenuContent>
|
|
1144
|
+
</DropdownMenu>
|
|
1145
|
+
<Button
|
|
1146
|
+
onClick={() => {
|
|
1147
|
+
setDeleteDataConfirmationOpen(true);
|
|
1148
|
+
}}
|
|
1149
|
+
className="px-2"
|
|
1150
|
+
variant="destructive"
|
|
1151
|
+
>
|
|
1152
|
+
<TrashIcon width={14} />
|
|
1153
|
+
Delete Selected Rows
|
|
1154
|
+
</Button>
|
|
1155
|
+
</div>
|
|
1156
|
+
)}
|
|
1157
|
+
<div className="grow" />
|
|
1158
|
+
<div className="px-2">
|
|
1159
|
+
<ViewSettings
|
|
1160
|
+
localDates={localDates}
|
|
1161
|
+
setLocalDates={setLocalDates}
|
|
1162
|
+
visiblity={colVisiblity}
|
|
1163
|
+
/>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
|
|
1167
|
+
<DndContext
|
|
1168
|
+
collisionDetection={closestCenter}
|
|
1169
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
1170
|
+
onDragEnd={handleDragEnd}
|
|
1171
|
+
sensors={sensors}
|
|
1172
|
+
>
|
|
1173
|
+
<div className="relative flex-1 overflow-hidden bg-neutral-100 dark:bg-neutral-900/50">
|
|
1174
|
+
{!tableSmallerThanViewport && (
|
|
1175
|
+
<div
|
|
1176
|
+
className="absolute top-0 right-0 bottom-0 z-50 w-[30px] bg-linear-to-l from-black/20 via-black/5 to-transparent transition-opacity duration-150"
|
|
1177
|
+
style={{
|
|
1178
|
+
pointerEvents: 'none',
|
|
1179
|
+
opacity: rightShadowOpacity,
|
|
1180
|
+
display: rightShadowOpacity == 0 ? 'none' : undefined,
|
|
1181
|
+
}}
|
|
1182
|
+
/>
|
|
1183
|
+
)}
|
|
1184
|
+
<div
|
|
1185
|
+
className="absolute top-0 bottom-0 left-0 z-50 w-[30px] bg-linear-to-r from-black/10 via-black/0 to-transparent transition-opacity duration-150"
|
|
1186
|
+
style={{
|
|
1187
|
+
pointerEvents: 'none',
|
|
1188
|
+
opacity: leftShadowOpacity,
|
|
1189
|
+
display: leftShadowOpacity == 0 ? 'none' : undefined,
|
|
1190
|
+
}}
|
|
1191
|
+
/>
|
|
1192
|
+
<div ref={tableRef} className="h-full w-full overflow-auto">
|
|
1193
|
+
<div
|
|
1194
|
+
style={{
|
|
1195
|
+
width: table.getCenterTotalSize(),
|
|
1196
|
+
}}
|
|
1197
|
+
className="z-0 inline-block text-left align-top font-mono text-xs text-neutral-500 dark:text-neutral-400"
|
|
1198
|
+
>
|
|
1199
|
+
<div className="sticky top-0 z-10 border-r border-b border-gray-200 border-r-gray-200 bg-white text-neutral-700 shadow-sm dark:border-r-neutral-700 dark:border-b-neutral-600 dark:bg-[#303030] dark:text-neutral-300">
|
|
1200
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
1201
|
+
<div className={'flex w-full'} key={headerGroup.id}>
|
|
1202
|
+
<SortableContext
|
|
1203
|
+
items={columnOrder}
|
|
1204
|
+
strategy={horizontalListSortingStrategy}
|
|
1205
|
+
>
|
|
1206
|
+
{headerGroup.headers.map((header, i) => (
|
|
1207
|
+
<TableHeader
|
|
1208
|
+
key={header.id}
|
|
1209
|
+
header={header}
|
|
1210
|
+
table={table}
|
|
1211
|
+
headerGroup={headerGroup}
|
|
1212
|
+
index={i}
|
|
1213
|
+
setMinViableColWidth={setMinViableColWidth}
|
|
1214
|
+
onSort={(attrName, currentAttr, currentAsc) => {
|
|
1215
|
+
history.push((prev) => ({
|
|
1216
|
+
...prev,
|
|
1217
|
+
sortAttr: attrName,
|
|
1218
|
+
sortAsc:
|
|
1219
|
+
currentAttr !== attrName ? true : !currentAsc,
|
|
1220
|
+
}));
|
|
1221
|
+
}}
|
|
1222
|
+
currentSortAttr={currentNav?.sortAttr}
|
|
1223
|
+
currentSortAsc={currentNav?.sortAsc}
|
|
1224
|
+
/>
|
|
1225
|
+
))}
|
|
1226
|
+
</SortableContext>
|
|
1227
|
+
</div>
|
|
1228
|
+
))}
|
|
1229
|
+
</div>
|
|
1230
|
+
<div>
|
|
1231
|
+
{table.getRowModel().rows.map((row) => (
|
|
1232
|
+
<div
|
|
1233
|
+
className="group flex border-r border-b border-r-gray-200 border-b-gray-200 bg-white dark:border-neutral-700 dark:border-r-neutral-700 dark:bg-neutral-800"
|
|
1234
|
+
key={row.id}
|
|
1235
|
+
>
|
|
1236
|
+
{row.getVisibleCells().map((cell) => (
|
|
1237
|
+
<SortableContext
|
|
1238
|
+
key={cell.id}
|
|
1239
|
+
items={columnOrder}
|
|
1240
|
+
strategy={horizontalListSortingStrategy}
|
|
1241
|
+
>
|
|
1242
|
+
<TableCell key={cell.id} cell={cell} />
|
|
1243
|
+
</SortableContext>
|
|
1244
|
+
))}
|
|
1245
|
+
</div>
|
|
1246
|
+
))}
|
|
1247
|
+
</div>
|
|
1248
|
+
</div>
|
|
1249
|
+
{tableSmallerThanViewport && (
|
|
1250
|
+
<div className="sticky top-0 inline-block align-top">
|
|
1251
|
+
<IconButton
|
|
1252
|
+
className="opacity-60"
|
|
1253
|
+
labelDirection="bottom"
|
|
1254
|
+
label="Fill Width"
|
|
1255
|
+
icon={<ArrowRightFromLine />}
|
|
1256
|
+
onClick={distributeRemainingWidth}
|
|
1257
|
+
/>
|
|
1258
|
+
</div>
|
|
1259
|
+
)}
|
|
1260
|
+
</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
</DndContext>
|
|
1263
|
+
</div>
|
|
1264
|
+
</>
|
|
1265
|
+
);
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
function formatVal(data: any, pretty?: boolean): string {
|
|
1269
|
+
if (isObject(data)) {
|
|
1270
|
+
return JSON.stringify(data, null, pretty ? 2 : undefined);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
return String(data);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function Val({ data, pretty }: { data: any; pretty?: boolean }) {
|
|
1277
|
+
const props = useExplorerProps();
|
|
1278
|
+
const sanitized = formatVal(data, pretty);
|
|
1279
|
+
|
|
1280
|
+
if (pretty && isObject(data)) {
|
|
1281
|
+
return <Fence darkMode={props.darkMode} code={sanitized} language="json" />;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
return <>{sanitized}</>;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Storage
|
|
1288
|
+
export async function jsonFetch(
|
|
1289
|
+
input: RequestInfo,
|
|
1290
|
+
init: RequestInit | undefined,
|
|
1291
|
+
): Promise<any> {
|
|
1292
|
+
const res = await fetch(input, init);
|
|
1293
|
+
const json = await res.json();
|
|
1294
|
+
return res.status === 200
|
|
1295
|
+
? Promise.resolve(json)
|
|
1296
|
+
: Promise.reject({ status: res.status, body: json });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
async function upload(
|
|
1300
|
+
token: string,
|
|
1301
|
+
appId: string,
|
|
1302
|
+
file: File,
|
|
1303
|
+
customFilename: string,
|
|
1304
|
+
apiUri: string,
|
|
1305
|
+
): Promise<boolean> {
|
|
1306
|
+
const headers = {
|
|
1307
|
+
app_id: appId,
|
|
1308
|
+
path: customFilename || file.name,
|
|
1309
|
+
authorization: `Bearer ${token}`,
|
|
1310
|
+
'content-type': file.type,
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const data = await jsonFetch(`${apiUri}/dash/apps/${appId}/storage/upload`, {
|
|
1314
|
+
method: 'PUT',
|
|
1315
|
+
headers,
|
|
1316
|
+
body: file,
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
return data;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
async function bulkDeleteFiles(
|
|
1323
|
+
token: string,
|
|
1324
|
+
appId: string,
|
|
1325
|
+
filenames: string[],
|
|
1326
|
+
apiUri: string,
|
|
1327
|
+
): Promise<any> {
|
|
1328
|
+
const { data } = await jsonFetch(
|
|
1329
|
+
`${apiUri}/dash/apps/${appId}/storage/files/delete`,
|
|
1330
|
+
{
|
|
1331
|
+
method: 'POST',
|
|
1332
|
+
headers: {
|
|
1333
|
+
'content-type': 'application/json',
|
|
1334
|
+
authorization: `Bearer ${token}`,
|
|
1335
|
+
},
|
|
1336
|
+
body: JSON.stringify({ filenames }),
|
|
1337
|
+
},
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
return data;
|
|
1341
|
+
}
|