@scality/data-browser-library 1.0.0-preview.11 → 1.0.0-preview.13

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.
Files changed (40) hide show
  1. package/dist/components/DataBrowserUI.d.ts +20 -0
  2. package/dist/components/DataBrowserUI.js +64 -0
  3. package/dist/components/__tests__/BucketDetails.test.d.ts +1 -0
  4. package/dist/components/__tests__/BucketDetails.test.js +421 -0
  5. package/dist/components/__tests__/BucketList.test.js +389 -164
  6. package/dist/components/__tests__/BucketOverview.test.js +19 -63
  7. package/dist/components/__tests__/ObjectList.test.js +719 -219
  8. package/dist/components/buckets/BucketDetails.d.ts +40 -0
  9. package/dist/components/buckets/BucketDetails.js +194 -86
  10. package/dist/components/buckets/BucketList.d.ts +5 -6
  11. package/dist/components/buckets/BucketList.js +152 -97
  12. package/dist/components/buckets/BucketOverview.d.ts +6 -0
  13. package/dist/components/buckets/BucketOverview.js +363 -179
  14. package/dist/components/buckets/BucketPage.js +1 -5
  15. package/dist/components/buckets/BucketVersioning.js +3 -0
  16. package/dist/components/buckets/EmptyBucketButton.js +1 -1
  17. package/dist/components/index.d.ts +2 -1
  18. package/dist/components/index.js +2 -1
  19. package/dist/components/layouts/ArrowNavigation.js +20 -8
  20. package/dist/components/objects/CreateFolderButton.js +1 -1
  21. package/dist/components/objects/ObjectDetails/ObjectSummary.js +287 -157
  22. package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.d.ts +1 -0
  23. package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +516 -0
  24. package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.d.ts +1 -0
  25. package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +813 -0
  26. package/dist/components/objects/ObjectDetails/index.d.ts +16 -0
  27. package/dist/components/objects/ObjectDetails/index.js +132 -46
  28. package/dist/components/objects/ObjectList.d.ts +7 -5
  29. package/dist/components/objects/ObjectList.js +566 -286
  30. package/dist/components/objects/UploadButton.js +1 -1
  31. package/dist/config/types.d.ts +117 -0
  32. package/dist/contexts/DataBrowserUICustomizationContext.d.ts +27 -0
  33. package/dist/contexts/DataBrowserUICustomizationContext.js +13 -0
  34. package/dist/test/testUtils.d.ts +64 -0
  35. package/dist/test/testUtils.js +100 -1
  36. package/dist/types/index.d.ts +5 -3
  37. package/dist/utils/constants.d.ts +7 -0
  38. package/dist/utils/constants.js +8 -1
  39. package/dist/utils/useFeatures.js +1 -1
  40. package/package.json +2 -2
@@ -1,8 +1,11 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { ConstrainedText, FormattedDateTime, Icon, Link, PrettyBytes, Text, Toggle, Wrap, spacing } from "@scality/core-ui";
2
+ import { ConstrainedText, FormattedDateTime, Icon, Link, PrettyBytes, Text, Toggle, Wrap, spacing, useToast } from "@scality/core-ui";
3
3
  import { Box, Table } from "@scality/core-ui/dist/next";
4
- import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { useQueryClient } from "@tanstack/react-query";
6
+ import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
5
7
  import { useSearchObjects, useSearchObjectsVersions } from "../../hooks/index.js";
8
+ import { useGetPresignedDownload } from "../../hooks/presignedOperations.js";
6
9
  import { useBatchObjectLegalHold } from "../../hooks/useBatchObjectLegalHold.js";
7
10
  import { useQueryParams } from "../../utils/hooks.js";
8
11
  import { useFeatures } from "../../utils/useFeatures.js";
@@ -11,16 +14,305 @@ import MetadataSearch from "../search/MetadataSearch.js";
11
14
  import UploadButton from "./UploadButton.js";
12
15
  import DeleteObjectButton from "./DeleteObjectButton.js";
13
16
  const DEFAULT_PAGE_SIZE = 20;
17
+ const SEARCH_QUERY_PARAM = "search";
18
+ const NAME_COLUMN_FLEX = "2";
19
+ const VERSION_ID_COLUMN_FLEX = "1.5";
20
+ const LAST_MODIFIED_COLUMN_FLEX = "1";
21
+ const SIZE_COLUMN_FLEX = "1";
22
+ const STORAGE_CLASS_COLUMN_FLEX = "1";
23
+ const isObjectLike = (item)=>"folder" !== item.type;
14
24
  const getVersionTextColor = (row)=>{
15
25
  const isLatest = row.original.IsLatest;
16
26
  const isVersion = "version" === row.original.type;
17
27
  return !isLatest && isVersion ? "infoPrimary" : "textPrimary";
18
28
  };
29
+ const removePrefix = (path, prefix)=>{
30
+ if (!prefix) return path;
31
+ if (path.startsWith(prefix)) return path.slice(prefix.length);
32
+ return path;
33
+ };
34
+ const createLegalHoldKey = (key, versionId)=>`${key}:${versionId ?? "null"}`;
35
+ const downloadFile = (url, filename, onCleanup)=>{
36
+ try {
37
+ new URL(url);
38
+ const link = document.createElement("a");
39
+ link.href = url;
40
+ link.download = filename;
41
+ link.style.display = "none";
42
+ link.rel = "noopener noreferrer";
43
+ document.body.appendChild(link);
44
+ link.click();
45
+ const timeoutId = setTimeout(()=>{
46
+ if (document.body.contains(link)) document.body.removeChild(link);
47
+ }, 100);
48
+ onCleanup?.(timeoutId);
49
+ } catch (error) {
50
+ console.error("Invalid download URL:", url, error);
51
+ throw new Error("Failed to initiate download: Invalid URL");
52
+ }
53
+ };
54
+ const createNameColumn = (onPrefixChange, onDownload)=>({
55
+ Header: "Name",
56
+ accessor: "displayName",
57
+ id: "name",
58
+ sortType: (rowA, rowB)=>{
59
+ const aIsFolder = "folder" === rowA.original.type;
60
+ const bIsFolder = "folder" === rowB.original.type;
61
+ if (aIsFolder && !bIsFolder) return -1;
62
+ if (!aIsFolder && bIsFolder) return 1;
63
+ const aName = String(rowA.values.displayName || "");
64
+ const bName = String(rowB.values.displayName || "");
65
+ return aName.localeCompare(bName, "en", {
66
+ sensitivity: "base"
67
+ });
68
+ },
69
+ Cell: ({ value, row })=>{
70
+ const isFolder = "folder" === row.original.type;
71
+ const isVersion = "version" === row.original.type;
72
+ const isDeleteMarker = "deleteMarker" === row.original.type;
73
+ const isLatest = row.original.IsLatest;
74
+ let iconName;
75
+ iconName = isFolder ? "Folder" : isDeleteMarker ? "Deletion-marker" : "File";
76
+ const shouldIndent = isVersion && !isLatest;
77
+ const isLegalHoldEnabled = isObjectLike(row.original) && Boolean(row.original.isLegalHoldEnabled);
78
+ return /*#__PURE__*/ jsx(ConstrainedText, {
79
+ text: /*#__PURE__*/ jsxs(Box, {
80
+ display: "flex",
81
+ alignItems: "center",
82
+ gap: spacing.r8,
83
+ paddingLeft: shouldIndent ? spacing.r24 : 0,
84
+ children: [
85
+ /*#__PURE__*/ jsx(Icon, {
86
+ name: iconName,
87
+ size: "sm",
88
+ color: getVersionTextColor(row)
89
+ }),
90
+ isLegalHoldEnabled && /*#__PURE__*/ jsx(Icon, {
91
+ name: "Rebalance",
92
+ size: "sm",
93
+ color: getVersionTextColor(row)
94
+ }),
95
+ isDeleteMarker ? /*#__PURE__*/ jsx(Text, {
96
+ color: getVersionTextColor(row),
97
+ children: value
98
+ }) : /*#__PURE__*/ jsx(Text, {
99
+ color: getVersionTextColor(row),
100
+ children: /*#__PURE__*/ jsx(Link, {
101
+ onClick: (e)=>{
102
+ e.stopPropagation();
103
+ if (isFolder) onPrefixChange?.(row.original.Key ?? "");
104
+ else onDownload?.(row.original);
105
+ },
106
+ children: value
107
+ })
108
+ })
109
+ ]
110
+ }),
111
+ lineClamp: 2
112
+ });
113
+ },
114
+ cellStyle: {
115
+ flex: NAME_COLUMN_FLEX,
116
+ width: "unset"
117
+ }
118
+ });
119
+ const createVersionIdColumn = ()=>({
120
+ Header: "Version ID",
121
+ accessor: "VersionId",
122
+ id: "versionId",
123
+ Cell: ({ row })=>{
124
+ const isFolder = "folder" === row.original.type;
125
+ const isDeleteMarker = "deleteMarker" === row.original.type;
126
+ const isLatest = row.original.IsLatest;
127
+ if (isFolder || isDeleteMarker) return /*#__PURE__*/ jsx(Text, {
128
+ children: "-"
129
+ });
130
+ const versionId = "version" === row.original.type ? row.original.VersionId ?? "-" : "-";
131
+ const textColor = isLatest ? void 0 : "infoPrimary";
132
+ return /*#__PURE__*/ jsx(ConstrainedText, {
133
+ text: versionId,
134
+ lineClamp: 1,
135
+ color: textColor
136
+ });
137
+ },
138
+ cellStyle: {
139
+ flex: VERSION_ID_COLUMN_FLEX,
140
+ textAlign: "left",
141
+ paddingRight: spacing.r16,
142
+ width: "unset",
143
+ maxWidth: "8rem"
144
+ }
145
+ });
146
+ const createLastModifiedColumn = ()=>({
147
+ Header: "Modified on",
148
+ accessor: "LastModified",
149
+ id: "lastModified",
150
+ Cell: ({ value, row })=>{
151
+ if ("folder" === row.original.type || null == value) return /*#__PURE__*/ jsx(Text, {
152
+ children: "-"
153
+ });
154
+ return /*#__PURE__*/ jsx(Text, {
155
+ color: getVersionTextColor(row),
156
+ children: /*#__PURE__*/ jsx(FormattedDateTime, {
157
+ format: "date-time-second",
158
+ value: new Date(value)
159
+ })
160
+ });
161
+ },
162
+ cellStyle: {
163
+ flex: LAST_MODIFIED_COLUMN_FLEX,
164
+ textAlign: "right",
165
+ paddingRight: spacing.r16,
166
+ width: "unset",
167
+ minWidth: "10rem"
168
+ }
169
+ });
170
+ const createSizeColumn = ()=>({
171
+ Header: "Size",
172
+ accessor: "Size",
173
+ id: "size",
174
+ Cell: ({ value, row })=>{
175
+ if ("folder" === row.original.type || null == value) return /*#__PURE__*/ jsx(Text, {
176
+ children: "-"
177
+ });
178
+ return /*#__PURE__*/ jsx(Box, {
179
+ display: "flex",
180
+ justifyContent: "flex-end",
181
+ children: /*#__PURE__*/ jsx(Text, {
182
+ color: getVersionTextColor(row),
183
+ children: /*#__PURE__*/ jsx(PrettyBytes, {
184
+ bytes: value,
185
+ decimals: 2
186
+ })
187
+ })
188
+ });
189
+ },
190
+ cellStyle: {
191
+ flex: SIZE_COLUMN_FLEX,
192
+ textAlign: "right",
193
+ paddingRight: spacing.r16,
194
+ width: "unset"
195
+ }
196
+ });
197
+ const createStorageClassColumn = ()=>({
198
+ Header: "Storage Location",
199
+ accessor: "StorageClass",
200
+ id: "storageClass",
201
+ Cell: ({ value, row })=>{
202
+ if (null == value) return /*#__PURE__*/ jsx(Text, {
203
+ children: "-"
204
+ });
205
+ return /*#__PURE__*/ jsx(Box, {
206
+ display: "flex",
207
+ justifyContent: "flex-end",
208
+ children: /*#__PURE__*/ jsx(Text, {
209
+ color: getVersionTextColor(row),
210
+ children: "STANDARD" === value ? "default" : value
211
+ })
212
+ });
213
+ },
214
+ cellStyle: {
215
+ flex: STORAGE_CLASS_COLUMN_FLEX,
216
+ textAlign: "right",
217
+ paddingRight: spacing.r16,
218
+ width: "unset"
219
+ }
220
+ });
221
+ const buildCustomColumn = (columnConfig)=>({
222
+ Header: columnConfig.header,
223
+ id: String(columnConfig.id),
224
+ Cell: ({ row })=>{
225
+ const RenderComponent = columnConfig.render;
226
+ return /*#__PURE__*/ jsx(RenderComponent, {
227
+ data: row.original
228
+ });
229
+ },
230
+ cellStyle: {
231
+ width: columnConfig.width ?? "unset",
232
+ flex: columnConfig.width ? void 0 : "1"
233
+ }
234
+ });
235
+ function createOverrideMap(customItems) {
236
+ return new Map(customItems.filter((item)=>item.id).map((item)=>[
237
+ String(item.id),
238
+ item
239
+ ]));
240
+ }
19
241
  const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
242
+ const { extraObjectListColumns, extraObjectListActions } = useDataBrowserUICustomization();
243
+ const queryClient = useQueryClient();
20
244
  const [showVersions, setShowVersions] = useState(false);
21
245
  const isMetadataSearchEnabled = useFeatures("metadatasearch");
22
246
  const metadataSearchQuery = useQueryParams().get("metadatasearch");
23
247
  const [selectedObjects, setSelectedObjects] = useState([]);
248
+ const { mutateAsync: getPresignedDownload } = useGetPresignedDownload();
249
+ const downloadingRef = useRef(new Set());
250
+ const downloadTimeoutsRef = useRef(new Map());
251
+ const { showToast } = useToast();
252
+ const onObjectSelectRef = useRef(onObjectSelect);
253
+ const onPrefixChangeRef = useRef(onPrefixChange);
254
+ useEffect(()=>{
255
+ onObjectSelectRef.current = onObjectSelect;
256
+ }, [
257
+ onObjectSelect
258
+ ]);
259
+ useEffect(()=>{
260
+ onPrefixChangeRef.current = onPrefixChange;
261
+ }, [
262
+ onPrefixChange
263
+ ]);
264
+ useEffect(()=>()=>{
265
+ downloadTimeoutsRef.current.forEach((timeout)=>clearTimeout(timeout));
266
+ downloadTimeoutsRef.current.clear();
267
+ }, []);
268
+ const handleObjectSelect = useCallback((object)=>{
269
+ onObjectSelectRef.current(object);
270
+ }, []);
271
+ const handlePrefixChange = useCallback((newPrefix)=>{
272
+ onPrefixChangeRef.current(newPrefix);
273
+ }, []);
274
+ const handleDownload = useCallback(async (object)=>{
275
+ if (!object.Key) return void console.warn("Cannot download object: missing Key");
276
+ const versionId = isObjectLike(object) ? object.VersionId : void 0;
277
+ const downloadKey = versionId ? `${object.Key}:${versionId}` : object.Key;
278
+ if (downloadingRef.current.has(downloadKey)) return void console.debug(`Download already in progress for: ${object.Key}`);
279
+ downloadingRef.current.add(downloadKey);
280
+ try {
281
+ const rawFilename = object.displayName || object.Key.split("/").pop() || "download";
282
+ const sanitized = rawFilename.replace(/["\r\n\t]/g, "");
283
+ const encoded = encodeURIComponent(sanitized);
284
+ const result = await getPresignedDownload({
285
+ Bucket: bucketName,
286
+ Key: object.Key,
287
+ ResponseContentDisposition: `attachment; filename="${sanitized}"; filename*=UTF-8''${encoded}`,
288
+ ...versionId ? {
289
+ VersionId: versionId
290
+ } : {}
291
+ });
292
+ if (!result?.Url) throw new Error("Failed to generate presigned URL: No URL returned");
293
+ downloadFile(result.Url, sanitized, (timeoutId)=>{
294
+ downloadTimeoutsRef.current.set(`${downloadKey}_link`, timeoutId);
295
+ });
296
+ } catch (error) {
297
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
298
+ showToast({
299
+ open: true,
300
+ message: errorMessage,
301
+ status: "error"
302
+ });
303
+ } finally{
304
+ const timeoutId = setTimeout(()=>{
305
+ downloadingRef.current.delete(downloadKey);
306
+ downloadTimeoutsRef.current.delete(downloadKey);
307
+ downloadTimeoutsRef.current.delete(`${downloadKey}_link`);
308
+ }, 1000);
309
+ downloadTimeoutsRef.current.set(downloadKey, timeoutId);
310
+ }
311
+ }, [
312
+ bucketName,
313
+ getPresignedDownload,
314
+ showToast
315
+ ]);
24
316
  const searchParams = useMemo(()=>{
25
317
  const baseParams = {
26
318
  Bucket: bucketName,
@@ -40,72 +332,121 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
40
332
  metadataSearchQuery
41
333
  ]);
42
334
  const listObjectsQuery = useSearchObjects(searchParams, {
43
- enabled: !showVersions
335
+ enabled: Boolean(bucketName)
44
336
  });
45
- const listVersionsQuery = useSearchObjectsVersions(searchParams, {
46
- enabled: showVersions
337
+ const versionSearchParams = useMemo(()=>({
338
+ Bucket: bucketName,
339
+ Prefix: prefix,
340
+ MaxKeys: DEFAULT_PAGE_SIZE
341
+ }), [
342
+ bucketName,
343
+ prefix
344
+ ]);
345
+ const listVersionsQuery = useSearchObjectsVersions(versionSearchParams, {
346
+ enabled: showVersions && Boolean(bucketName)
47
347
  });
48
- const activeQuery = showVersions ? listVersionsQuery : listObjectsQuery;
49
348
  useEffect(()=>{
50
349
  setSelectedObjects([]);
51
350
  }, [
52
351
  prefix,
53
352
  showVersions
54
353
  ]);
55
- const { data, error, hasNextPage, fetchNextPage, isFetchingNextPage, isLoading } = activeQuery;
354
+ const data = showVersions ? listVersionsQuery.data : listObjectsQuery.data;
355
+ const error = showVersions ? listVersionsQuery.error : listObjectsQuery.error;
356
+ const hasNextPage = showVersions ? listVersionsQuery.hasNextPage : listObjectsQuery.hasNextPage;
357
+ const fetchNextPage = showVersions ? listVersionsQuery.fetchNextPage : listObjectsQuery.fetchNextPage;
358
+ const isFetchingNextPage = showVersions ? listVersionsQuery.isFetchingNextPage : listObjectsQuery.isFetchingNextPage;
359
+ const isLoading = showVersions ? listVersionsQuery.isLoading : listObjectsQuery.isLoading;
56
360
  const objectsForLegalHold = useMemo(()=>{
57
361
  const objects = [];
58
- if (showVersions && listVersionsQuery.data?.pages) listVersionsQuery.data.pages.forEach((page)=>{
59
- if (page.Versions) page.Versions.forEach((version)=>{
60
- if (version.Key) objects.push({
61
- Key: version.Key,
62
- VersionId: version.VersionId
63
- });
64
- });
65
- });
66
- else if (!showVersions && data?.pages) data.pages.forEach((page)=>{
67
- if (page.Contents) page.Contents.forEach((obj)=>{
68
- if (obj.Key) objects.push({
69
- Key: obj.Key
70
- });
71
- });
72
- });
362
+ const currentData = showVersions ? listVersionsQuery.data : listObjectsQuery.data;
363
+ if (!currentData?.pages) return objects;
364
+ let count = 0;
365
+ if (showVersions) for (const page of currentData.pages){
366
+ if (count >= 100) break;
367
+ if (page.Versions) for (const version of page.Versions){
368
+ if (count >= 100) break;
369
+ if (version.Key) {
370
+ objects.push({
371
+ Key: version.Key,
372
+ VersionId: version.VersionId
373
+ });
374
+ count++;
375
+ }
376
+ }
377
+ }
378
+ else for (const page of currentData.pages){
379
+ if (count >= 100) break;
380
+ if (page.Contents) for (const obj of page.Contents){
381
+ if (count >= 100) break;
382
+ if (obj.Key) {
383
+ objects.push({
384
+ Key: obj.Key
385
+ });
386
+ count++;
387
+ }
388
+ }
389
+ }
73
390
  return objects;
74
391
  }, [
75
- data,
76
392
  showVersions,
77
- listVersionsQuery.data
393
+ listVersionsQuery.data,
394
+ listObjectsQuery.data
78
395
  ]);
79
- const { data: legalHoldData } = useBatchObjectLegalHold(bucketName, objectsForLegalHold, true);
80
- const tableData = useMemo(()=>{
396
+ const shouldFetchLegalHold = useMemo(()=>Boolean(bucketName) && objectsForLegalHold.length > 0, [
397
+ bucketName,
398
+ objectsForLegalHold.length
399
+ ]);
400
+ const { data: legalHoldData } = useBatchObjectLegalHold(bucketName, objectsForLegalHold, shouldFetchLegalHold);
401
+ const processFolders = useCallback((pages)=>{
81
402
  const folders = [];
403
+ pages.forEach((page)=>{
404
+ if (page.CommonPrefixes) page.CommonPrefixes.forEach((cp)=>{
405
+ if (cp.Prefix) {
406
+ const folderName = removePrefix(cp.Prefix, prefix).replace(/\/$/, "");
407
+ folders.push({
408
+ Key: cp.Prefix,
409
+ displayName: folderName + "/",
410
+ type: "folder"
411
+ });
412
+ }
413
+ });
414
+ });
415
+ return folders;
416
+ }, [
417
+ prefix
418
+ ]);
419
+ const createObjectItem = useCallback((obj, legalHoldData)=>{
420
+ const legalHoldKey = createLegalHoldKey(obj.Key, null);
421
+ const isLegalHoldEnabled = legalHoldData?.[legalHoldKey]?.isLegalHoldEnabled ?? false;
422
+ return {
423
+ ...obj,
424
+ displayName: removePrefix(obj.Key, prefix),
425
+ type: "object",
426
+ isLegalHoldEnabled
427
+ };
428
+ }, [
429
+ prefix
430
+ ]);
431
+ const tableData = useMemo(()=>{
432
+ let folders = [];
82
433
  const items = [];
83
434
  if (showVersions) {
84
- if (listObjectsQuery.data?.pages) listObjectsQuery.data.pages.forEach((page)=>{
85
- if (page.CommonPrefixes) page.CommonPrefixes.forEach((cp)=>{
86
- if (cp.Prefix) {
87
- const folderName = cp.Prefix.replace(prefix, "").replace(/\/$/, "");
88
- folders.push({
89
- Key: cp.Prefix,
90
- displayName: folderName + "/",
91
- type: "folder",
92
- LastModified: void 0,
93
- Size: void 0
94
- });
95
- }
96
- });
97
- });
435
+ folders = listObjectsQuery.data?.pages ? processFolders(listObjectsQuery.data.pages) : [];
436
+ const objectsWithVersions = new Set();
98
437
  if (listVersionsQuery.data?.pages) {
99
438
  const itemsByKey = {};
100
439
  listVersionsQuery.data.pages.forEach((page)=>{
101
440
  if (page.Versions) page.Versions.forEach((version)=>{
102
- if (version.Key) {
441
+ if (version.Key && !version.Key.endsWith("/")) {
442
+ objectsWithVersions.add(version.Key);
103
443
  if (!itemsByKey[version.Key]) itemsByKey[version.Key] = [];
104
444
  itemsByKey[version.Key].push(version);
105
445
  }
106
446
  });
107
447
  if (page.DeleteMarkers) page.DeleteMarkers.forEach((marker)=>{
108
- if (marker.Key) {
448
+ if (marker.Key && !marker.Key.endsWith("/")) {
449
+ objectsWithVersions.add(marker.Key);
109
450
  if (!itemsByKey[marker.Key]) itemsByKey[marker.Key] = [];
110
451
  itemsByKey[marker.Key].push({
111
452
  ...marker,
@@ -114,21 +455,25 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
114
455
  }
115
456
  });
116
457
  });
117
- Object.keys(itemsByKey).sort().forEach((key)=>{
458
+ Object.keys(itemsByKey).sort((a, b)=>a.localeCompare(b, "en", {
459
+ sensitivity: "base"
460
+ })).forEach((key)=>{
118
461
  const allItems = itemsByKey[key];
119
462
  allItems.sort((a, b)=>{
120
463
  if (a.IsLatest) return -1;
121
464
  if (b.IsLatest) return 1;
122
- const aDate = a.LastModified ? new Date(a.LastModified).getTime() : 0;
123
- const bDate = b.LastModified ? new Date(b.LastModified).getTime() : 0;
124
- return bDate - aDate;
465
+ const aTime = a.LastModified?.getTime() ?? 0;
466
+ const bTime = b.LastModified?.getTime() ?? 0;
467
+ return bTime - aTime;
125
468
  });
126
469
  allItems.forEach((item)=>{
127
- const baseName = item.Key.replace(prefix, "");
470
+ if (!item.Key) return;
471
+ if (item.Key.endsWith("/")) return;
472
+ const baseName = removePrefix(item.Key, prefix);
128
473
  const isLatest = item.IsLatest;
129
474
  const isDeleteMarker = "isDeleteMarker" in item && item.isDeleteMarker;
130
- const legalHoldKey = `${item.Key}:${item.VersionId || "null"}`;
131
- const isLegalHoldEnabled = legalHoldData?.[legalHoldKey]?.isLegalHoldEnabled || false;
475
+ const legalHoldKey = createLegalHoldKey(item.Key, item.VersionId);
476
+ const isLegalHoldEnabled = legalHoldData?.[legalHoldKey]?.isLegalHoldEnabled ?? false;
132
477
  if (isDeleteMarker) items.push({
133
478
  ...item,
134
479
  displayName: baseName,
@@ -147,224 +492,164 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
147
492
  });
148
493
  });
149
494
  }
150
- } else if (data?.pages) data.pages.forEach((page)=>{
151
- if (page.CommonPrefixes) page.CommonPrefixes.forEach((cp)=>{
152
- if (cp.Prefix) {
153
- const folderName = cp.Prefix.replace(prefix, "").replace(/\/$/, "");
154
- folders.push({
155
- Key: cp.Prefix,
156
- displayName: folderName + "/",
157
- type: "folder",
158
- LastModified: void 0,
159
- Size: void 0
160
- });
161
- }
495
+ if (listObjectsQuery.data?.pages) listObjectsQuery.data.pages.forEach((page)=>{
496
+ if (page.Contents) page.Contents.forEach((obj)=>{
497
+ if (obj.Key && !obj.Key.endsWith("/") && !objectsWithVersions.has(obj.Key)) items.push(createObjectItem(obj, legalHoldData));
498
+ });
162
499
  });
163
- if (page.Contents) page.Contents.forEach((obj)=>{
164
- if (obj.Key && !obj.Key.endsWith("/")) {
165
- const legalHoldKey = `${obj.Key}:null`;
166
- const isLegalHoldEnabled = legalHoldData?.[legalHoldKey]?.isLegalHoldEnabled || false;
167
- items.push({
168
- ...obj,
169
- displayName: obj.Key.replace(prefix, ""),
170
- type: "object",
171
- isLegalHoldEnabled
172
- });
173
- }
500
+ } else {
501
+ folders = data?.pages ? processFolders(data.pages) : [];
502
+ if (data?.pages) data.pages.forEach((page)=>{
503
+ if (page.Contents) page.Contents.forEach((obj)=>{
504
+ if (obj.Key && !obj.Key.endsWith("/")) items.push(createObjectItem(obj, legalHoldData));
505
+ });
174
506
  });
175
- });
507
+ }
176
508
  const allItems = [
177
509
  ...folders,
178
510
  ...items
179
511
  ];
180
512
  return allItems;
181
513
  }, [
182
- data,
183
- prefix,
184
514
  showVersions,
185
515
  listObjectsQuery.data,
186
516
  listVersionsQuery.data,
187
- legalHoldData
517
+ legalHoldData,
518
+ processFolders,
519
+ createObjectItem
520
+ ]);
521
+ const versionIdColumn = useMemo(()=>createVersionIdColumn(), []);
522
+ const lastModifiedColumn = useMemo(()=>createLastModifiedColumn(), []);
523
+ const sizeColumn = useMemo(()=>createSizeColumn(), []);
524
+ const storageClassColumn = useMemo(()=>createStorageClassColumn(), []);
525
+ const nameColumn = useMemo(()=>createNameColumn(handlePrefixChange, handleDownload), [
526
+ handlePrefixChange,
527
+ handleDownload
188
528
  ]);
189
529
  const columns = useMemo(()=>{
190
- const cols = [
191
- {
192
- Header: "Name",
193
- accessor: "displayName",
194
- id: "name",
195
- sortType: (rowA, rowB)=>{
196
- const aIsFolder = "folder" === rowA.original.type;
197
- const bIsFolder = "folder" === rowB.original.type;
198
- if (aIsFolder && !bIsFolder) return -1;
199
- if (!aIsFolder && bIsFolder) return 1;
200
- const aName = String(rowA.values.displayName || "");
201
- const bName = String(rowB.values.displayName || "");
202
- return aName.localeCompare(bName);
203
- },
204
- Cell: ({ value, row })=>{
205
- const displayValue = value;
206
- const isFolder = "folder" === row.original.type;
207
- const isVersion = "version" === row.original.type;
208
- const isDeleteMarker = "deleteMarker" === row.original.type;
209
- const isLatest = row.original.IsLatest;
210
- let iconName;
211
- iconName = isFolder ? "Folder" : isDeleteMarker ? "Deletion-marker" : "File";
212
- const shouldIndent = isVersion && !isLatest;
213
- const isLegalHoldEnabled = Boolean(row.original.isLegalHoldEnabled);
214
- return /*#__PURE__*/ jsx(ConstrainedText, {
215
- text: /*#__PURE__*/ jsxs("div", {
216
- style: {
217
- display: "flex",
218
- alignItems: "center",
219
- gap: spacing.r8,
220
- paddingLeft: shouldIndent ? spacing.r24 : 0
221
- },
222
- children: [
223
- /*#__PURE__*/ jsx(Icon, {
224
- name: iconName,
225
- size: "sm",
226
- color: getVersionTextColor(row)
227
- }),
228
- isLegalHoldEnabled && /*#__PURE__*/ jsx(Icon, {
229
- name: "Rebalance",
230
- size: "sm",
231
- color: getVersionTextColor(row)
232
- }),
233
- isDeleteMarker ? /*#__PURE__*/ jsx(Text, {
234
- color: getVersionTextColor(row),
235
- children: displayValue
236
- }) : /*#__PURE__*/ jsx(Link, {
237
- onClick: (e)=>{
238
- e.stopPropagation();
239
- if (isFolder) onPrefixChange?.(row.original.Key || "");
240
- else onObjectSelect(row.original);
241
- },
242
- children: /*#__PURE__*/ jsx(Text, {
243
- color: getVersionTextColor(row),
244
- children: displayValue
245
- })
246
- })
247
- ]
248
- }),
249
- lineClamp: 2
250
- });
251
- },
252
- cellStyle: {
253
- flex: "2",
254
- width: "unset"
255
- }
256
- }
530
+ const defaultColumnsMap = {
531
+ name: nameColumn,
532
+ versionId: versionIdColumn,
533
+ lastModified: lastModifiedColumn,
534
+ size: sizeColumn,
535
+ storageClass: storageClassColumn
536
+ };
537
+ const customColumns = (extraObjectListColumns || []).map((config)=>buildCustomColumn(config));
538
+ const customColumnsMap = createOverrideMap(customColumns);
539
+ const getColumn = (id)=>customColumnsMap.get(id) || defaultColumnsMap[id];
540
+ const extraColumns = customColumns.filter((col)=>col.id && !(col.id in defaultColumnsMap));
541
+ const finalColumns = [
542
+ getColumn("name"),
543
+ ...showVersions ? [
544
+ getColumn("versionId")
545
+ ] : [],
546
+ ...extraColumns,
547
+ getColumn("lastModified"),
548
+ getColumn("size"),
549
+ getColumn("storageClass")
257
550
  ];
258
- if (showVersions) cols.push({
259
- Header: "Version ID",
260
- accessor: "VersionId",
261
- id: "versionId",
262
- Cell: ({ row })=>{
263
- const isFolder = "folder" === row.original.type;
264
- const isDeleteMarker = "deleteMarker" === row.original.type;
265
- const isLatest = row.original.IsLatest;
266
- if (isFolder || isDeleteMarker) return /*#__PURE__*/ jsx(Text, {
267
- children: "-"
268
- });
269
- const versionId = "version" === row.original.type ? row.original.VersionId || "-" : "-";
270
- const textColor = isLatest ? void 0 : "infoPrimary";
271
- return /*#__PURE__*/ jsx(ConstrainedText, {
272
- text: versionId,
273
- lineClamp: 1,
274
- color: textColor
275
- });
276
- },
277
- cellStyle: {
278
- flex: "1.5",
279
- textAlign: "left",
280
- paddingRight: spacing.r16,
281
- width: "unset",
282
- maxWidth: "8rem"
283
- }
551
+ return finalColumns;
552
+ }, [
553
+ showVersions,
554
+ extraObjectListColumns,
555
+ nameColumn,
556
+ versionIdColumn,
557
+ lastModifiedColumn,
558
+ sizeColumn,
559
+ storageClassColumn
560
+ ]);
561
+ const handleUploadSuccess = useCallback(()=>{
562
+ queryClient.invalidateQueries({
563
+ queryKey: [
564
+ "ListObjects"
565
+ ]
284
566
  });
285
- cols.push({
286
- Header: "Modified on",
287
- accessor: "LastModified",
288
- id: "lastModified",
289
- Cell: ({ value, row })=>{
290
- if ("folder" === row.original.type || !value) return /*#__PURE__*/ jsx(Text, {
291
- children: "-"
292
- });
293
- const dateValue = value;
294
- return /*#__PURE__*/ jsx(Text, {
295
- color: getVersionTextColor(row),
296
- children: /*#__PURE__*/ jsx(FormattedDateTime, {
297
- format: "date-time-second",
298
- value: new Date(dateValue)
299
- })
300
- });
301
- },
302
- cellStyle: {
303
- flex: "1",
304
- textAlign: "right",
305
- paddingRight: spacing.r16,
306
- width: "unset",
307
- minWidth: "10rem"
308
- }
309
- }, {
310
- Header: "Size",
311
- accessor: "Size",
312
- id: "size",
313
- Cell: ({ value, row })=>{
314
- if ("folder" === row.original.type || null == value) return /*#__PURE__*/ jsx(Text, {
315
- children: "-"
316
- });
317
- const sizeValue = value;
318
- return /*#__PURE__*/ jsx(Wrap, {
319
- style: {
320
- justifyContent: "flex-end"
321
- },
322
- children: /*#__PURE__*/ jsx(Text, {
323
- color: getVersionTextColor(row),
324
- children: /*#__PURE__*/ jsx(PrettyBytes, {
325
- bytes: sizeValue,
326
- decimals: 2
327
- })
328
- })
329
- });
567
+ queryClient.invalidateQueries({
568
+ queryKey: [
569
+ "ListObjectVersions"
570
+ ]
571
+ });
572
+ }, [
573
+ queryClient
574
+ ]);
575
+ const handleFolderSuccess = useCallback(()=>{
576
+ queryClient.invalidateQueries({
577
+ queryKey: [
578
+ "ListObjects"
579
+ ]
580
+ });
581
+ queryClient.invalidateQueries({
582
+ queryKey: [
583
+ "ListObjectVersions"
584
+ ]
585
+ });
586
+ }, [
587
+ queryClient
588
+ ]);
589
+ const handleUploadError = useCallback(()=>{}, []);
590
+ const handleFolderError = useCallback(()=>{}, []);
591
+ const renderUploadAction = useCallback(()=>/*#__PURE__*/ jsx(UploadButton, {
592
+ bucket: bucketName,
593
+ prefix: prefix,
594
+ onUploadSuccess: handleUploadSuccess,
595
+ onUploadError: handleUploadError
596
+ }), [
597
+ bucketName,
598
+ prefix,
599
+ handleUploadSuccess,
600
+ handleUploadError
601
+ ]);
602
+ const renderCreateFolderAction = useCallback(()=>/*#__PURE__*/ jsx(CreateFolderButton, {
603
+ bucket: bucketName,
604
+ prefix: prefix,
605
+ onFolderSuccess: handleFolderSuccess,
606
+ onFolderError: handleFolderError
607
+ }), [
608
+ bucketName,
609
+ prefix,
610
+ handleFolderSuccess,
611
+ handleFolderError
612
+ ]);
613
+ const renderDeleteAction = useCallback(()=>/*#__PURE__*/ jsx(DeleteObjectButton, {
614
+ objects: selectedObjects,
615
+ bucketName: bucketName
616
+ }), [
617
+ selectedObjects,
618
+ bucketName
619
+ ]);
620
+ const actions = useMemo(()=>{
621
+ const defaultActionsMap = {
622
+ upload: {
623
+ id: "upload",
624
+ render: renderUploadAction
330
625
  },
331
- cellStyle: {
332
- flex: "1",
333
- textAlign: "right",
334
- paddingRight: spacing.r16,
335
- width: "unset"
336
- }
337
- }, {
338
- Header: "Storage Location",
339
- accessor: "StorageClass",
340
- id: "storageClass",
341
- Cell: ({ value, row })=>{
342
- if (!value) return /*#__PURE__*/ jsx(Text, {
343
- children: "-"
344
- });
345
- const storageClass = value;
346
- return /*#__PURE__*/ jsx(Wrap, {
347
- style: {
348
- justifyContent: "flex-end"
349
- },
350
- children: /*#__PURE__*/ jsx(Text, {
351
- color: getVersionTextColor(row),
352
- children: "STANDARD" === storageClass ? "default" : storageClass
353
- })
354
- });
626
+ createFolder: {
627
+ id: "createFolder",
628
+ render: renderCreateFolderAction
355
629
  },
356
- cellStyle: {
357
- flex: "1",
358
- textAlign: "right",
359
- paddingRight: spacing.r16,
360
- width: "unset"
630
+ delete: {
631
+ id: "delete",
632
+ render: renderDeleteAction
361
633
  }
362
- });
363
- return cols;
634
+ };
635
+ const customActions = (extraObjectListActions || []).map((config)=>({
636
+ id: config.id,
637
+ render: config.render
638
+ }));
639
+ const customActionsMap = createOverrideMap(customActions);
640
+ const getAction = (id)=>customActionsMap.get(id) || defaultActionsMap[id];
641
+ const extraActions = customActions.filter((action)=>!(action.id in defaultActionsMap));
642
+ return [
643
+ getAction("upload"),
644
+ getAction("createFolder"),
645
+ getAction("delete"),
646
+ ...extraActions
647
+ ];
364
648
  }, [
365
- onObjectSelect,
366
- onPrefixChange,
367
- showVersions
649
+ renderUploadAction,
650
+ renderCreateFolderAction,
651
+ renderDeleteAction,
652
+ extraObjectListActions
368
653
  ]);
369
654
  const handleReachBottom = useCallback(()=>{
370
655
  if (hasNextPage && !isFetchingNextPage) fetchNextPage();
@@ -373,18 +658,33 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
373
658
  isFetchingNextPage,
374
659
  fetchNextPage
375
660
  ]);
661
+ const handleMultiSelectionChanged = useCallback((rows)=>{
662
+ const objects = rows.map((row)=>row.original);
663
+ setSelectedObjects(objects);
664
+ }, []);
665
+ const handleSingleRowSelected = useCallback((row)=>{
666
+ handleObjectSelect(row.original);
667
+ }, [
668
+ handleObjectSelect
669
+ ]);
670
+ const handleToggleAll = useCallback((selected)=>{
671
+ selected ? setSelectedObjects(tableData) : setSelectedObjects([]);
672
+ }, [
673
+ tableData
674
+ ]);
376
675
  const tableStatus = isLoading ? "loading" : error ? "error" : "success";
676
+ const entityName = useMemo(()=>({
677
+ en: {
678
+ singular: "object",
679
+ plural: "objects"
680
+ }
681
+ }), []);
377
682
  return /*#__PURE__*/ jsxs(Table, {
378
683
  columns: columns,
379
684
  data: tableData,
380
685
  status: tableStatus,
381
686
  onBottom: handleReachBottom,
382
- entityName: {
383
- en: {
384
- singular: "object",
385
- plural: "objects"
386
- }
387
- },
687
+ entityName: entityName,
388
688
  children: [
389
689
  /*#__PURE__*/ jsxs(Wrap, {
390
690
  padding: spacing.r16,
@@ -392,7 +692,7 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
392
692
  isMetadataSearchEnabled ? /*#__PURE__*/ jsx(MetadataSearch, {
393
693
  isError: !!error
394
694
  }) : /*#__PURE__*/ jsx(Table.SearchWithQueryParams, {
395
- queryParams: "search"
695
+ queryParams: SEARCH_QUERY_PARAM
396
696
  }),
397
697
  /*#__PURE__*/ jsxs(Box, {
398
698
  display: "flex",
@@ -400,33 +700,20 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
400
700
  alignItems: "center",
401
701
  gap: spacing.r8,
402
702
  children: [
403
- /*#__PURE__*/ jsxs(Box, {
703
+ /*#__PURE__*/ jsx(Box, {
404
704
  display: "flex",
405
705
  alignItems: "center",
406
706
  gap: spacing.r8,
407
- children: [
408
- /*#__PURE__*/ jsx(UploadButton, {
409
- bucket: bucketName,
410
- prefix: prefix,
411
- onUploadSuccess: ()=>{},
412
- onUploadError: ()=>{}
413
- }),
414
- /*#__PURE__*/ jsx(CreateFolderButton, {
415
- bucket: bucketName,
416
- prefix: prefix,
417
- onFolderSuccess: ()=>{},
418
- onFolderError: ()=>{}
419
- }),
420
- /*#__PURE__*/ jsx(DeleteObjectButton, {
421
- objects: selectedObjects,
422
- bucketName: bucketName
423
- })
424
- ]
707
+ children: actions.map((action)=>/*#__PURE__*/ jsx(Fragment, {
708
+ children: action.render()
709
+ }, action.id))
425
710
  }),
426
711
  /*#__PURE__*/ jsx(Toggle, {
427
712
  toggle: showVersions,
428
713
  onChange: (e)=>setShowVersions(e.target.checked),
429
- label: "List Versions"
714
+ label: "List Versions",
715
+ "aria-label": showVersions ? "Hide object versions" : "Show object versions",
716
+ "aria-pressed": showVersions
430
717
  })
431
718
  ]
432
719
  })
@@ -434,20 +721,13 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange })=>{
434
721
  }),
435
722
  /*#__PURE__*/ jsx(Table.MultiSelectableContent, {
436
723
  rowHeight: "h40",
437
- onMultiSelectionChanged: (rows)=>{
438
- const objects = rows.map((row)=>row.original);
439
- setSelectedObjects(objects);
440
- },
441
- onSingleRowSelected: (row)=>{
442
- onObjectSelect(row.original);
443
- },
444
- onToggleAll: (selected)=>{
445
- console.log("Toggle all", selected);
446
- },
724
+ onMultiSelectionChanged: handleMultiSelectionChanged,
725
+ onSingleRowSelected: handleSingleRowSelected,
726
+ onToggleAll: handleToggleAll,
447
727
  separationLineVariant: "backgroundLevel1",
448
728
  isLoadingMoreItems: isFetchingNextPage
449
729
  })
450
730
  ]
451
731
  });
452
732
  };
453
- export { ObjectList };
733
+ export { ObjectList, isObjectLike };