@scality/data-browser-library 1.1.10 → 1.1.12

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 (47) hide show
  1. package/dist/components/__tests__/BucketCorsPage.test.js +14 -6
  2. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +129 -0
  3. package/dist/components/__tests__/BucketLifecycleList.test.js +78 -0
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +15 -3
  5. package/dist/components/buckets/BucketCorsPage.js +5 -7
  6. package/dist/components/buckets/BucketLifecycleFormPage.js +18 -12
  7. package/dist/components/buckets/BucketLifecycleList.js +5 -5
  8. package/dist/components/buckets/BucketOverview.js +1 -1
  9. package/dist/components/buckets/BucketPolicyPage.js +4 -7
  10. package/dist/components/buckets/BucketReplicationFormPage.js +12 -6
  11. package/dist/components/buckets/BucketVersioning.js +1 -1
  12. package/dist/components/buckets/notifications/BucketNotificationFormPage.js +2 -2
  13. package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
  14. package/dist/components/objects/DeleteObjectButton.js +1 -1
  15. package/dist/components/objects/GetPresignedUrlButton.js +1 -1
  16. package/dist/components/objects/ObjectDetails/ObjectMetadata.js +1 -1
  17. package/dist/components/objects/ObjectDetails/ObjectSummary.js +118 -7
  18. package/dist/components/objects/ObjectDetails/ObjectTags.js +1 -1
  19. package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +142 -3
  20. package/dist/components/objects/ObjectList.js +42 -16
  21. package/dist/components/objects/ObjectLock/ObjectLockSettings.js +1 -1
  22. package/dist/components/objects/__tests__/GetPresignedUrlButton.test.js +30 -1
  23. package/dist/components/providers/QueryProvider.js +2 -1
  24. package/dist/config/types.d.ts +4 -0
  25. package/dist/hooks/bucketConfiguration.js +15 -15
  26. package/dist/hooks/bucketOperations.js +1 -1
  27. package/dist/hooks/factories/__tests__/useCreateS3FunctionMutationHook.test.js +7 -35
  28. package/dist/hooks/factories/__tests__/useCreateS3InfiniteQueryHook.test.js +1 -1
  29. package/dist/hooks/factories/__tests__/useCreateS3MutationHook.test.js +1 -1
  30. package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +1 -1
  31. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.d.ts +1 -1
  32. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -2
  33. package/dist/hooks/factories/useCreateS3MutationHook.d.ts +3 -3
  34. package/dist/hooks/factories/useCreateS3MutationHook.js +20 -8
  35. package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -1
  36. package/dist/hooks/factories/useCreateS3QueryHook.js +2 -2
  37. package/dist/hooks/objectOperations.js +6 -6
  38. package/dist/hooks/presignedOperations.js +1 -1
  39. package/dist/hooks/useDeleteFolder.js +1 -1
  40. package/dist/test/utils/errorHandling.test.js +45 -33
  41. package/dist/utils/__tests__/coldStorage.test.d.ts +1 -0
  42. package/dist/utils/__tests__/coldStorage.test.js +24 -0
  43. package/dist/utils/coldStorage.d.ts +12 -0
  44. package/dist/utils/coldStorage.js +23 -0
  45. package/dist/utils/errorHandling.d.ts +2 -1
  46. package/dist/utils/errorHandling.js +8 -7
  47. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ConstrainedText, FormattedDateTime, Icon, Loader, PrettyBytes, Text, Toggle, Tooltip, spacing, useToast } from "@scality/core-ui";
3
3
  import { CopyButton } from "@scality/core-ui/dist/components/buttonv2/CopyButton.component";
4
- import { Box } from "@scality/core-ui/dist/next";
4
+ import { Box, Button } from "@scality/core-ui/dist/next";
5
5
  import { Fragment as external_react_Fragment, useCallback, useMemo } from "react";
6
6
  import { useDataBrowserUICustomization } from "../../../contexts/DataBrowserUICustomizationContext.js";
7
- import { useGetPublicAccessBlock, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useSetObjectAcl, useSetObjectLegalHold } from "../../../hooks/index.js";
7
+ import { useGetPublicAccessBlock, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useRestoreObject, useSetObjectAcl, useSetObjectLegalHold } from "../../../hooks/index.js";
8
+ import { COLD_STORAGE_TOOLTIP, COLD_STORAGE_TOOLTIP_STYLE, parseRestoreStatus } from "../../../utils/coldStorage.js";
8
9
  import { useDataBrowserConfig } from "../../providers/DataBrowserProvider.js";
9
10
  import { Body, ExtraCell, Group, GroupContent, GroupName, GroupValues, Key, Row, Table, TableContainer, Value } from "../../ui/Table.elements.js";
10
11
  import { GetPresignedUrlButton } from "../GetPresignedUrlButton.js";
@@ -24,7 +25,7 @@ const buildPublicUrl = (endpoint, bucket, key, forcePathStyle)=>{
24
25
  };
25
26
  const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
26
27
  const s3Config = useDataBrowserConfig();
27
- const { extraObjectSummaryInformation, extraObjectSummaryDataProtection } = useDataBrowserUICustomization();
28
+ const { extraObjectSummaryInformation, extraObjectSummaryDataProtection, isLocationCold, coldStorageRestoreDays } = useDataBrowserUICustomization();
28
29
  const { data: metadata, status: metadataStatus } = useObjectMetadata({
29
30
  Bucket: bucketName,
30
31
  Key: objectKey,
@@ -48,6 +49,11 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
48
49
  });
49
50
  const { mutate: setLegalHold, isPending: isUpdatingLegalHold } = useSetObjectLegalHold();
50
51
  const { showToast } = useToast();
52
+ const storageClass = metadata?.StorageClass ?? 'STANDARD';
53
+ const isCold = isLocationCold?.(storageClass) ?? false;
54
+ const isColdMetadataLoading = isCold && 'pending' === metadataStatus;
55
+ const restoreStatus = isCold && 'success' === metadataStatus ? parseRestoreStatus(metadata?.Restore) : null;
56
+ const { mutate: restoreObject, isPending: isRestoring } = useRestoreObject();
51
57
  const { data: aclData, status: aclStatus } = useObjectAcl({
52
58
  Bucket: bucketName,
53
59
  Key: objectKey,
@@ -106,7 +112,7 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
106
112
  onError: (error)=>{
107
113
  showToast({
108
114
  open: true,
109
- message: error instanceof Error ? error.message : 'Failed to update legal hold',
115
+ message: error.message,
110
116
  status: 'error'
111
117
  });
112
118
  }
@@ -138,7 +144,7 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
138
144
  onError: (error)=>{
139
145
  showToast({
140
146
  open: true,
141
- message: error instanceof Error ? error.message : 'Failed to update object visibility',
147
+ message: error.message,
142
148
  status: 'error'
143
149
  });
144
150
  }
@@ -288,7 +294,7 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
288
294
  children: "Location"
289
295
  }),
290
296
  /*#__PURE__*/ jsx(Value, {
291
- children: "default"
297
+ children: 'pending' === metadataStatus ? /*#__PURE__*/ jsx(Loader, {}) : 'error' === metadataStatus ? 'Error' : metadata?.StorageClass !== 'STANDARD' && metadata?.StorageClass ? metadata.StorageClass : 'default'
292
298
  })
293
299
  ]
294
300
  }, "location")
@@ -316,6 +322,99 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
316
322
  if (defaultFields[fieldConfig.id]) defaultFields[fieldConfig.id] = fieldItem;
317
323
  else extraFields.push(fieldItem);
318
324
  });
325
+ const temperatureField = isCold && (restoreStatus || isColdMetadataLoading) ? {
326
+ id: 'temperature',
327
+ label: 'Temperature',
328
+ node: /*#__PURE__*/ jsxs(Row, {
329
+ children: [
330
+ /*#__PURE__*/ jsx(Key, {
331
+ children: "Temperature"
332
+ }),
333
+ /*#__PURE__*/ jsx(Value, {
334
+ children: isColdMetadataLoading ? /*#__PURE__*/ jsx(Loader, {}) : restoreStatus ? /*#__PURE__*/ jsxs(GroupValues, {
335
+ children: [
336
+ /*#__PURE__*/ jsx(Box, {
337
+ display: "flex",
338
+ alignItems: "center",
339
+ gap: spacing.r8,
340
+ children: 'cold' === restoreStatus.status ? /*#__PURE__*/ jsxs(Fragment, {
341
+ children: [
342
+ /*#__PURE__*/ jsx(Icon, {
343
+ name: "Snowflake"
344
+ }),
345
+ /*#__PURE__*/ jsx(Text, {
346
+ children: "Cold"
347
+ }),
348
+ /*#__PURE__*/ jsx(Tooltip, {
349
+ overlay: COLD_STORAGE_TOOLTIP,
350
+ overlayStyle: COLD_STORAGE_TOOLTIP_STYLE,
351
+ children: /*#__PURE__*/ jsx(Icon, {
352
+ name: "Info",
353
+ color: "infoPrimary"
354
+ })
355
+ })
356
+ ]
357
+ }) : 'restoring' === restoreStatus.status ? /*#__PURE__*/ jsxs(Fragment, {
358
+ children: [
359
+ /*#__PURE__*/ jsx(Loader, {
360
+ size: "smaller"
361
+ }),
362
+ /*#__PURE__*/ jsx(Text, {
363
+ children: "Restoring..."
364
+ })
365
+ ]
366
+ }) : /*#__PURE__*/ jsxs(Text, {
367
+ children: [
368
+ "Available until",
369
+ ' ',
370
+ restoreStatus.expiryDate ? /*#__PURE__*/ jsx(FormattedDateTime, {
371
+ format: "date-time-second",
372
+ value: restoreStatus.expiryDate
373
+ }) : 'N/A'
374
+ ]
375
+ })
376
+ }),
377
+ 'cold' === restoreStatus.status && /*#__PURE__*/ jsx(Button, {
378
+ variant: "outline",
379
+ label: "Restore",
380
+ icon: /*#__PURE__*/ jsx(Icon, {
381
+ name: "Redo"
382
+ }),
383
+ disabled: isRestoring,
384
+ onClick: ()=>{
385
+ restoreObject({
386
+ Bucket: bucketName,
387
+ Key: objectKey,
388
+ ...versionId && {
389
+ VersionId: versionId
390
+ },
391
+ RestoreRequest: {
392
+ Days: coldStorageRestoreDays ?? 5
393
+ }
394
+ }, {
395
+ onSuccess: ()=>{
396
+ showToast({
397
+ open: true,
398
+ message: 'Restore initiated',
399
+ status: 'success'
400
+ });
401
+ },
402
+ onError: (error)=>{
403
+ showToast({
404
+ open: true,
405
+ message: error.message,
406
+ status: 'error'
407
+ });
408
+ }
409
+ });
410
+ }
411
+ })
412
+ ]
413
+ }) : null
414
+ })
415
+ ]
416
+ }, "temperature")
417
+ } : null;
319
418
  return [
320
419
  defaultFields.name,
321
420
  defaultFields.versionId,
@@ -323,6 +422,9 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
323
422
  defaultFields.lastModified,
324
423
  defaultFields.etag,
325
424
  defaultFields.location,
425
+ ...temperatureField ? [
426
+ temperatureField
427
+ ] : [],
326
428
  ...extraFields
327
429
  ].filter((field)=>null !== field.node);
328
430
  }, [
@@ -332,8 +434,17 @@ const ObjectSummary = ({ bucketName, objectKey, versionId })=>{
332
434
  metadata?.ContentLength,
333
435
  metadata?.LastModified,
334
436
  metadata?.ETag,
437
+ metadata?.StorageClass,
335
438
  metadataStatus,
336
- extraObjectSummaryInformation
439
+ extraObjectSummaryInformation,
440
+ isCold,
441
+ isColdMetadataLoading,
442
+ restoreStatus,
443
+ isRestoring,
444
+ bucketName,
445
+ restoreObject,
446
+ showToast,
447
+ coldStorageRestoreDays
337
448
  ]);
338
449
  const accessFields = useMemo(()=>{
339
450
  const fields = [];
@@ -108,7 +108,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
108
108
  status: 'success'
109
109
  });
110
110
  } catch (error) {
111
- const errorMessage = error instanceof Error ? error.message : 'Failed to save tags';
111
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
112
112
  showToast({
113
113
  open: true,
114
114
  message: errorMessage,
@@ -4,8 +4,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
4
  import { cleanup, render, screen, waitFor } from "@testing-library/react";
5
5
  import user_event from "@testing-library/user-event";
6
6
  import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
7
- import { useGetPresignedDownload, useGetPublicAccessBlock, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useSetObjectAcl, useSetObjectLegalHold } from "../../../../hooks/index.js";
7
+ import { useGetPresignedDownload, useGetPublicAccessBlock, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useRestoreObject, useSetObjectAcl, useSetObjectLegalHold } from "../../../../hooks/index.js";
8
8
  import { findToggleByLabel } from "../../../../test/testUtils.js";
9
+ import { EnhancedS3Error, ErrorCategory } from "../../../../utils/errorHandling.js";
9
10
  import { ObjectSummary } from "../ObjectSummary.js";
10
11
  jest.mock('../../../../hooks');
11
12
  jest.mock('@scality/core-ui', ()=>{
@@ -15,6 +16,7 @@ jest.mock('@scality/core-ui', ()=>{
15
16
  useToast: jest.fn()
16
17
  };
17
18
  });
19
+ const mockIsLocationCold = jest.fn(()=>false);
18
20
  jest.mock('../../../providers/DataBrowserProvider', ()=>({
19
21
  ...jest.requireActual('../../../providers/DataBrowserProvider'),
20
22
  useDataBrowserConfig: jest.fn(()=>({
@@ -30,6 +32,7 @@ const mockUseObjectAcl = jest.mocked(useObjectAcl);
30
32
  const mockUseSetObjectAcl = jest.mocked(useSetObjectAcl);
31
33
  const mockUseGetPublicAccessBlock = jest.mocked(useGetPublicAccessBlock);
32
34
  const mockUseGetPresignedDownload = jest.mocked(useGetPresignedDownload);
35
+ const mockUseRestoreObject = jest.mocked(useRestoreObject);
33
36
  const mockUseToast = jest.mocked(useToast);
34
37
  const mockObjectMetadata = {
35
38
  VersionId: 'test-version-id',
@@ -98,6 +101,11 @@ const setupMockDefaults = ()=>{
98
101
  mutateAsync: jest.fn(),
99
102
  isPending: false
100
103
  });
104
+ mockUseRestoreObject.mockReturnValue({
105
+ mutate: jest.fn(),
106
+ isPending: false
107
+ });
108
+ mockIsLocationCold.mockReturnValue(false);
101
109
  mockUseToast.mockReturnValue({
102
110
  showToast: jest.fn()
103
111
  });
@@ -125,7 +133,10 @@ const renderWithProviders = (ui, { customizationConfig = {} } = {})=>{
125
133
  return render(/*#__PURE__*/ jsx(QueryClientProvider, {
126
134
  client: queryClient,
127
135
  children: /*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
128
- config: customizationConfig,
136
+ config: {
137
+ isLocationCold: mockIsLocationCold,
138
+ ...customizationConfig
139
+ },
129
140
  children: ui
130
141
  })
131
142
  }));
@@ -663,6 +674,41 @@ describe('ObjectSummary', ()=>{
663
674
  });
664
675
  });
665
676
  });
677
+ it('should show permission message when mutation fails with 403 error', async ()=>{
678
+ const user = user_event.setup();
679
+ const mockShowToast = jest.fn();
680
+ const authError = new EnhancedS3Error("You don't have permission to update legal hold. Contact your administrator.", 'AccessDenied', ErrorCategory.AUTHORIZATION, new Error('Access Denied'), 403);
681
+ const mockMutate = jest.fn((_params, callbacks)=>{
682
+ callbacks.onError(authError);
683
+ });
684
+ mockUseToast.mockReturnValue({
685
+ showToast: mockShowToast
686
+ });
687
+ mockUseObjectRetention.mockReturnValueOnce({
688
+ data: mockRetentionData,
689
+ status: 'success'
690
+ });
691
+ mockUseObjectLegalHold.mockReturnValueOnce({
692
+ data: mockLegalHoldData,
693
+ status: 'success'
694
+ });
695
+ mockUseSetObjectLegalHold.mockReturnValueOnce({
696
+ mutate: mockMutate,
697
+ isPending: false
698
+ });
699
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
700
+ ...defaultProps
701
+ }));
702
+ const toggle = getLegalHoldToggle();
703
+ await user.click(toggle);
704
+ await waitFor(()=>{
705
+ expect(mockShowToast).toHaveBeenCalledWith({
706
+ open: true,
707
+ message: "You don't have permission to update legal hold. Contact your administrator.",
708
+ status: 'error'
709
+ });
710
+ });
711
+ });
666
712
  it('should show generic error message when mutation fails with non-Error object', async ()=>{
667
713
  const user = user_event.setup();
668
714
  const mockShowToast = jest.fn();
@@ -694,7 +740,7 @@ describe('ObjectSummary', ()=>{
694
740
  await waitFor(()=>{
695
741
  expect(mockShowToast).toHaveBeenCalledWith({
696
742
  open: true,
697
- message: 'Failed to update legal hold',
743
+ message: 'Custom error',
698
744
  status: 'error'
699
745
  });
700
746
  });
@@ -1061,4 +1107,97 @@ describe('ObjectSummary', ()=>{
1061
1107
  await expectTooltipOnHover(toggle, 'Unable to check public access block settings');
1062
1108
  });
1063
1109
  });
1110
+ describe('Cold Storage - Temperature Row', ()=>{
1111
+ it('should not show Temperature row for non-cold objects', ()=>{
1112
+ mockIsLocationCold.mockReturnValue(false);
1113
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
1114
+ ...defaultProps
1115
+ }));
1116
+ expect(screen.queryByText('Cold')).not.toBeInTheDocument();
1117
+ });
1118
+ it('should show Temperature row with "Cold" status and restore button for cold objects', ()=>{
1119
+ mockIsLocationCold.mockReturnValue(true);
1120
+ mockUseObjectMetadata.mockReturnValue({
1121
+ data: {
1122
+ ...mockObjectMetadata,
1123
+ StorageClass: 'cold-location',
1124
+ Restore: void 0
1125
+ },
1126
+ status: 'success'
1127
+ });
1128
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
1129
+ ...defaultProps
1130
+ }));
1131
+ expect(screen.getByText('Cold')).toBeInTheDocument();
1132
+ expect(screen.getByRole('button', {
1133
+ name: /restore/i
1134
+ })).toBeInTheDocument();
1135
+ });
1136
+ it('should show "Restoring..." status without restore button', ()=>{
1137
+ mockIsLocationCold.mockReturnValue(true);
1138
+ mockUseObjectMetadata.mockReturnValue({
1139
+ data: {
1140
+ ...mockObjectMetadata,
1141
+ StorageClass: 'cold-location',
1142
+ Restore: 'ongoing-request="true"'
1143
+ },
1144
+ status: 'success'
1145
+ });
1146
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
1147
+ ...defaultProps
1148
+ }));
1149
+ expect(screen.getByText('Restoring...')).toBeInTheDocument();
1150
+ expect(screen.queryByRole('button', {
1151
+ name: /restore/i
1152
+ })).not.toBeInTheDocument();
1153
+ });
1154
+ it('should show expiry date when object is restored', ()=>{
1155
+ mockIsLocationCold.mockReturnValue(true);
1156
+ mockUseObjectMetadata.mockReturnValue({
1157
+ data: {
1158
+ ...mockObjectMetadata,
1159
+ StorageClass: 'cold-location',
1160
+ Restore: 'ongoing-request="false", expiry-date="Fri, 02 May 2026 00:00:00 GMT"'
1161
+ },
1162
+ status: 'success'
1163
+ });
1164
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
1165
+ ...defaultProps
1166
+ }));
1167
+ expect(screen.getByText(/Available until/)).toBeInTheDocument();
1168
+ expect(screen.queryByRole('button', {
1169
+ name: /restore/i
1170
+ })).not.toBeInTheDocument();
1171
+ });
1172
+ it('should call restoreObject when restore button is clicked', async ()=>{
1173
+ const mockRestore = jest.fn();
1174
+ mockIsLocationCold.mockReturnValue(true);
1175
+ mockUseObjectMetadata.mockReturnValue({
1176
+ data: {
1177
+ ...mockObjectMetadata,
1178
+ StorageClass: 'cold-location',
1179
+ Restore: void 0
1180
+ },
1181
+ status: 'success'
1182
+ });
1183
+ mockUseRestoreObject.mockReturnValue({
1184
+ mutate: mockRestore,
1185
+ isPending: false
1186
+ });
1187
+ renderWithProviders(/*#__PURE__*/ jsx(ObjectSummary, {
1188
+ ...defaultProps
1189
+ }));
1190
+ const user = user_event.setup();
1191
+ await user.click(screen.getByRole('button', {
1192
+ name: /restore/i
1193
+ }));
1194
+ expect(mockRestore).toHaveBeenCalledWith(expect.objectContaining({
1195
+ Bucket: 'test-bucket',
1196
+ Key: 'test-object.txt',
1197
+ RestoreRequest: {
1198
+ Days: 5
1199
+ }
1200
+ }), expect.any(Object));
1201
+ });
1202
+ });
1064
1203
  });
@@ -1,7 +1,7 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Banner, ConstrainedText, FormattedDateTime, Icon, Link, PrettyBytes, SearchInput, Text, Toggle, spacing, useToast } from "@scality/core-ui";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { Banner, ConstrainedText, FormattedDateTime, Icon, Link, PrettyBytes, SearchInput, Text, Toggle, Tooltip, spacing, useToast } from "@scality/core-ui";
3
3
  import { Box, Table } from "@scality/core-ui/dist/next";
4
- import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as external_react_Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { useLocation, useNavigate } from "react-router";
6
6
  import styled_components from "styled-components";
7
7
  import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
@@ -9,6 +9,7 @@ import { useListObjectVersions, useListObjects, useSearchObjects, useSearchObjec
9
9
  import { useDownloadObject } from "../../hooks/useDownloadObject.js";
10
10
  import { useBatchObjectLegalHold } from "../../hooks/useBatchObjectLegalHold.js";
11
11
  import { useFeatures } from "../../hooks/useFeatures.js";
12
+ import { COLD_STORAGE_TOOLTIP, COLD_STORAGE_TOOLTIP_STYLE } from "../../utils/coldStorage.js";
12
13
  import { useQueryParams } from "../../utils/hooks.js";
13
14
  import { useInvalidateQueries } from "../providers/DataBrowserProvider.js";
14
15
  import MetadataSearch from "../search/MetadataSearch.js";
@@ -62,7 +63,7 @@ const removePrefix = (path, prefix)=>{
62
63
  return path;
63
64
  };
64
65
  const createLegalHoldKey = (key, versionId)=>`${key}:${versionId ?? 'null'}`;
65
- const createNameColumn = (onPrefixChange, onDownload)=>({
66
+ const createNameColumn = (onPrefixChange, onDownload, isLocationCold)=>({
66
67
  Header: 'Name',
67
68
  accessor: 'displayName',
68
69
  id: 'name',
@@ -90,6 +91,7 @@ const createNameColumn = (onPrefixChange, onDownload)=>({
90
91
  iconName = isFolder ? 'Folder' : isDeleteMarker ? 'Deletion-marker' : 'File';
91
92
  const shouldIndent = isVersion && !isLatest;
92
93
  const isLegalHoldEnabled = isObjectLike(row.original) && Boolean(row.original.isLegalHoldEnabled);
94
+ const isCold = !isFolder && !isDeleteMarker && 'StorageClass' in row.original && null != row.original.StorageClass && isLocationCold?.(row.original.StorageClass);
93
95
  return /*#__PURE__*/ jsxs(Box, {
94
96
  display: "flex",
95
97
  alignItems: "center",
@@ -108,7 +110,7 @@ const createNameColumn = (onPrefixChange, onDownload)=>({
108
110
  color: getVersionTextColor(row)
109
111
  }),
110
112
  /*#__PURE__*/ jsx(TruncatedName, {
111
- children: isDeleteMarker ? /*#__PURE__*/ jsx(Text, {
113
+ children: isDeleteMarker || isCold ? /*#__PURE__*/ jsx(Text, {
112
114
  color: getVersionTextColor(row),
113
115
  children: value
114
116
  }) : /*#__PURE__*/ jsx(Text, {
@@ -227,7 +229,7 @@ const createSizeColumn = ()=>({
227
229
  width: 'unset'
228
230
  }
229
231
  });
230
- const createStorageClassColumn = ()=>({
232
+ const createStorageClassColumn = (isLocationCold)=>({
231
233
  Header: 'Storage Location',
232
234
  accessor: 'StorageClass',
233
235
  id: 'storageClass',
@@ -235,13 +237,34 @@ const createStorageClassColumn = ()=>({
235
237
  if (null == value) return /*#__PURE__*/ jsx(Text, {
236
238
  children: "-"
237
239
  });
238
- return /*#__PURE__*/ jsx(Box, {
240
+ const isCold = isObjectLike(row.original) && 'deleteMarker' !== row.original.type && null != value && isLocationCold?.(value);
241
+ const displayValue = 'STANDARD' === value ? 'default' : value;
242
+ return /*#__PURE__*/ jsxs(Box, {
239
243
  display: "flex",
240
244
  justifyContent: "flex-end",
241
- children: /*#__PURE__*/ jsx(Text, {
242
- color: getVersionTextColor(row),
243
- children: 'STANDARD' === value ? 'default' : value
244
- })
245
+ alignItems: "center",
246
+ gap: spacing.r4,
247
+ children: [
248
+ isCold && /*#__PURE__*/ jsxs(Fragment, {
249
+ children: [
250
+ /*#__PURE__*/ jsx(Icon, {
251
+ name: "Snowflake"
252
+ }),
253
+ /*#__PURE__*/ jsx(Tooltip, {
254
+ overlay: COLD_STORAGE_TOOLTIP,
255
+ overlayStyle: COLD_STORAGE_TOOLTIP_STYLE,
256
+ children: /*#__PURE__*/ jsx(Icon, {
257
+ name: "Info",
258
+ color: "infoPrimary"
259
+ })
260
+ })
261
+ ]
262
+ }),
263
+ /*#__PURE__*/ jsx(Text, {
264
+ color: getVersionTextColor(row),
265
+ children: displayValue
266
+ })
267
+ ]
245
268
  });
246
269
  },
247
270
  cellStyle: {
@@ -273,7 +296,7 @@ function createOverrideMap(customItems) {
273
296
  ]));
274
297
  }
275
298
  const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSelectedObjectsChange })=>{
276
- const { extraObjectListColumns, extraObjectListActions } = useDataBrowserUICustomization();
299
+ const { extraObjectListColumns, extraObjectListActions, isLocationCold } = useDataBrowserUICustomization();
277
300
  const invalidateQueries = useInvalidateQueries();
278
301
  const versionCheck = useListObjectVersions({
279
302
  Bucket: bucketName,
@@ -579,10 +602,13 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
579
602
  versionGroupLatestModified
580
603
  ]);
581
604
  const sizeColumn = useMemo(()=>createSizeColumn(), []);
582
- const storageClassColumn = useMemo(()=>createStorageClassColumn(), []);
583
- const nameColumn = useMemo(()=>createNameColumn(handlePrefixChange, handleDownload), [
605
+ const storageClassColumn = useMemo(()=>createStorageClassColumn(isLocationCold), [
606
+ isLocationCold
607
+ ]);
608
+ const nameColumn = useMemo(()=>createNameColumn(handlePrefixChange, handleDownload, isLocationCold), [
584
609
  handlePrefixChange,
585
- handleDownload
610
+ handleDownload,
611
+ isLocationCold
586
612
  ]);
587
613
  const columns = useMemo(()=>{
588
614
  const defaultColumnsMap = {
@@ -800,7 +826,7 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
800
826
  alignItems: "center",
801
827
  gap: spacing.r8,
802
828
  children: [
803
- actions.map((action)=>/*#__PURE__*/ jsx(Fragment, {
829
+ actions.map((action)=>/*#__PURE__*/ jsx(external_react_Fragment, {
804
830
  children: action.render()
805
831
  }, action.id)),
806
832
  /*#__PURE__*/ jsx(Toggle, {
@@ -102,7 +102,7 @@ function ObjectLockSettings() {
102
102
  navigate(`/buckets/${bucketName}`);
103
103
  },
104
104
  onError: (error)=>{
105
- const errorMessage = error instanceof Error ? error.message : 'Failed to save Object Lock settings';
105
+ const errorMessage = error.message;
106
106
  showToast({
107
107
  open: true,
108
108
  message: errorMessage,
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
4
  import { cleanup, render, screen, waitFor } from "@testing-library/react";
5
5
  import user_event from "@testing-library/user-event";
6
6
  import { useGetPresignedDownload } from "../../../hooks/index.js";
7
+ import { EnhancedS3Error, ErrorCategory } from "../../../utils/errorHandling.js";
7
8
  import { GetPresignedUrlButton } from "../GetPresignedUrlButton.js";
8
9
  jest.mock('../../../hooks');
9
10
  jest.mock('@scality/core-ui', ()=>{
@@ -462,6 +463,34 @@ describe('GetPresignedUrlButton', ()=>{
462
463
  });
463
464
  });
464
465
  });
466
+ it('should show permission message for 403 errors', async ()=>{
467
+ const mockShowToast = jest.fn();
468
+ mockUseToast.mockReturnValue({
469
+ showToast: mockShowToast
470
+ });
471
+ const authError = new EnhancedS3Error("You don't have permission to generate the presigned URL. Contact your administrator.", 'AccessDenied', ErrorCategory.AUTHORIZATION, new Error('Access Denied'), 403);
472
+ mockUseGetPresignedDownload.mockReturnValue({
473
+ mutateAsync: jest.fn().mockRejectedValue(authError),
474
+ isPending: false
475
+ });
476
+ const user = user_event.setup();
477
+ renderWithProviders(/*#__PURE__*/ jsx(GetPresignedUrlButton, {
478
+ ...defaultProps
479
+ }));
480
+ await user.click(screen.getByRole('button', {
481
+ name: /get pre-signed url/i
482
+ }));
483
+ await user.click(screen.getByRole('button', {
484
+ name: /generate a pre-signed url/i
485
+ }));
486
+ await waitFor(()=>{
487
+ expect(mockShowToast).toHaveBeenCalledWith({
488
+ open: true,
489
+ message: "You don't have permission to generate the presigned URL. Contact your administrator.",
490
+ status: 'error'
491
+ });
492
+ });
493
+ });
465
494
  it('should show generic toast for non-Error failures', async ()=>{
466
495
  const mockShowToast = jest.fn();
467
496
  mockUseToast.mockReturnValue({
@@ -484,7 +513,7 @@ describe('GetPresignedUrlButton', ()=>{
484
513
  await waitFor(()=>{
485
514
  expect(mockShowToast).toHaveBeenCalledWith({
486
515
  open: true,
487
- message: 'Failed to generate presigned URL',
516
+ message: 'Unknown error',
488
517
  status: 'error'
489
518
  });
490
519
  });
@@ -1,11 +1,12 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
4
+ import { shouldRetryError } from "../../utils/errorHandling.js";
4
5
  const defaultQueryClient = new QueryClient({
5
6
  defaultOptions: {
6
7
  queries: {
7
8
  staleTime: 300000,
8
- retry: 2
9
+ retry: (failureCount, error)=>shouldRetryError(error, failureCount, 2)
9
10
  }
10
11
  }
11
12
  });
@@ -184,6 +184,10 @@ export interface BucketCreateVersioningProps {
184
184
  */
185
185
  export interface DataBrowserUIProps {
186
186
  basePath?: string;
187
+ /** Returns true if the given location name (from StorageClass) is a cold storage location. */
188
+ isLocationCold?: (locationName: string) => boolean;
189
+ /** Number of days to keep a restored cold object accessible. Defaults to 5. */
190
+ coldStorageRestoreDays?: number;
187
191
  /**
188
192
  * Custom header component to render above the main content.
189
193
  * Typically used for breadcrumbs or navigation.