@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.
- package/dist/components/__tests__/BucketCorsPage.test.js +14 -6
- package/dist/components/__tests__/BucketLifecycleFormPage.test.js +129 -0
- package/dist/components/__tests__/BucketLifecycleList.test.js +78 -0
- package/dist/components/__tests__/BucketNotificationFormPage.test.js +15 -3
- package/dist/components/buckets/BucketCorsPage.js +5 -7
- package/dist/components/buckets/BucketLifecycleFormPage.js +18 -12
- package/dist/components/buckets/BucketLifecycleList.js +5 -5
- package/dist/components/buckets/BucketOverview.js +1 -1
- package/dist/components/buckets/BucketPolicyPage.js +4 -7
- package/dist/components/buckets/BucketReplicationFormPage.js +12 -6
- package/dist/components/buckets/BucketVersioning.js +1 -1
- package/dist/components/buckets/notifications/BucketNotificationFormPage.js +2 -2
- package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
- package/dist/components/objects/DeleteObjectButton.js +1 -1
- package/dist/components/objects/GetPresignedUrlButton.js +1 -1
- package/dist/components/objects/ObjectDetails/ObjectMetadata.js +1 -1
- package/dist/components/objects/ObjectDetails/ObjectSummary.js +118 -7
- package/dist/components/objects/ObjectDetails/ObjectTags.js +1 -1
- package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +142 -3
- package/dist/components/objects/ObjectList.js +42 -16
- package/dist/components/objects/ObjectLock/ObjectLockSettings.js +1 -1
- package/dist/components/objects/__tests__/GetPresignedUrlButton.test.js +30 -1
- package/dist/components/providers/QueryProvider.js +2 -1
- package/dist/config/types.d.ts +4 -0
- package/dist/hooks/bucketConfiguration.js +15 -15
- package/dist/hooks/bucketOperations.js +1 -1
- package/dist/hooks/factories/__tests__/useCreateS3FunctionMutationHook.test.js +7 -35
- package/dist/hooks/factories/__tests__/useCreateS3InfiniteQueryHook.test.js +1 -1
- package/dist/hooks/factories/__tests__/useCreateS3MutationHook.test.js +1 -1
- package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +1 -1
- package/dist/hooks/factories/useCreateS3InfiniteQueryHook.d.ts +1 -1
- package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -2
- package/dist/hooks/factories/useCreateS3MutationHook.d.ts +3 -3
- package/dist/hooks/factories/useCreateS3MutationHook.js +20 -8
- package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -1
- package/dist/hooks/factories/useCreateS3QueryHook.js +2 -2
- package/dist/hooks/objectOperations.js +6 -6
- package/dist/hooks/presignedOperations.js +1 -1
- package/dist/hooks/useDeleteFolder.js +1 -1
- package/dist/test/utils/errorHandling.test.js +45 -33
- package/dist/utils/__tests__/coldStorage.test.d.ts +1 -0
- package/dist/utils/__tests__/coldStorage.test.js +24 -0
- package/dist/utils/coldStorage.d.ts +12 -0
- package/dist/utils/coldStorage.js +23 -0
- package/dist/utils/errorHandling.d.ts +2 -1
- package/dist/utils/errorHandling.js +8 -7
- 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
|
|
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
|
|
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:
|
|
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 : '
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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: '
|
|
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
|
});
|
package/dist/config/types.d.ts
CHANGED
|
@@ -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.
|