@scality/data-browser-library 1.1.11 → 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__/BucketLifecycleFormPage.test.js +104 -0
- package/dist/components/__tests__/BucketLifecycleList.test.js +78 -0
- package/dist/components/buckets/BucketLifecycleFormPage.js +5 -5
- package/dist/components/buckets/BucketLifecycleList.js +5 -5
- package/dist/components/objects/ObjectDetails/ObjectSummary.js +116 -5
- package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +105 -2
- package/dist/components/objects/ObjectList.js +42 -16
- package/dist/config/types.d.ts +4 -0
- 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/package.json +1 -1
|
@@ -709,6 +709,110 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
709
709
|
});
|
|
710
710
|
});
|
|
711
711
|
});
|
|
712
|
+
describe('editing immediate transition rules (0 days)', ()=>{
|
|
713
|
+
it('preserves 0 days when editing a current version transition', async ()=>{
|
|
714
|
+
mockUseGetBucketLifecycle.mockReturnValue({
|
|
715
|
+
data: {
|
|
716
|
+
Rules: [
|
|
717
|
+
{
|
|
718
|
+
ID: 'immediate-glacier',
|
|
719
|
+
Status: 'Enabled',
|
|
720
|
+
Filter: {},
|
|
721
|
+
Transitions: [
|
|
722
|
+
{
|
|
723
|
+
Days: 0,
|
|
724
|
+
StorageClass: 'GLACIER'
|
|
725
|
+
}
|
|
726
|
+
]
|
|
727
|
+
}
|
|
728
|
+
]
|
|
729
|
+
},
|
|
730
|
+
status: 'success'
|
|
731
|
+
});
|
|
732
|
+
renderBucketLifecycleFormPage('test-bucket', 'immediate-glacier');
|
|
733
|
+
await waitFor(()=>{
|
|
734
|
+
const transitionToggle = findToggleByLabel('Transition current version');
|
|
735
|
+
expect(transitionToggle).toBeChecked();
|
|
736
|
+
const daysInput = document.getElementById('transition-days-0');
|
|
737
|
+
expect(daysInput).toHaveValue(0);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
it('preserves 0 days when editing a noncurrent version transition', async ()=>{
|
|
741
|
+
mockUseGetBucketLifecycle.mockReturnValue({
|
|
742
|
+
data: {
|
|
743
|
+
Rules: [
|
|
744
|
+
{
|
|
745
|
+
ID: 'immediate-noncurrent',
|
|
746
|
+
Status: 'Enabled',
|
|
747
|
+
Filter: {},
|
|
748
|
+
NoncurrentVersionTransitions: [
|
|
749
|
+
{
|
|
750
|
+
NoncurrentDays: 0,
|
|
751
|
+
StorageClass: 'GLACIER'
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
},
|
|
757
|
+
status: 'success'
|
|
758
|
+
});
|
|
759
|
+
renderBucketLifecycleFormPage('test-bucket', 'immediate-noncurrent');
|
|
760
|
+
await waitFor(()=>{
|
|
761
|
+
const noncurrentToggle = findToggleByLabel('Transition noncurrent version');
|
|
762
|
+
expect(noncurrentToggle).toBeChecked();
|
|
763
|
+
const daysInput = document.getElementById('noncurrent-transition-days-0');
|
|
764
|
+
expect(daysInput).toHaveValue(0);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
it('preserves 0 days when editing a current version expiration', async ()=>{
|
|
768
|
+
mockUseGetBucketLifecycle.mockReturnValue({
|
|
769
|
+
data: {
|
|
770
|
+
Rules: [
|
|
771
|
+
{
|
|
772
|
+
ID: 'immediate-expire',
|
|
773
|
+
Status: 'Enabled',
|
|
774
|
+
Filter: {},
|
|
775
|
+
Expiration: {
|
|
776
|
+
Days: 0
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
status: 'success'
|
|
782
|
+
});
|
|
783
|
+
renderBucketLifecycleFormPage('test-bucket', 'immediate-expire');
|
|
784
|
+
await waitFor(()=>{
|
|
785
|
+
const expirationToggle = findToggleByLabel('Expiration current version');
|
|
786
|
+
expect(expirationToggle).toBeChecked();
|
|
787
|
+
const daysInput = screen.getByLabelText(/^days$/i);
|
|
788
|
+
expect(daysInput).toHaveValue(0);
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
it('preserves 0 days when editing a noncurrent version expiration', async ()=>{
|
|
792
|
+
mockUseGetBucketLifecycle.mockReturnValue({
|
|
793
|
+
data: {
|
|
794
|
+
Rules: [
|
|
795
|
+
{
|
|
796
|
+
ID: 'immediate-noncurrent-expire',
|
|
797
|
+
Status: 'Enabled',
|
|
798
|
+
Filter: {},
|
|
799
|
+
NoncurrentVersionExpiration: {
|
|
800
|
+
NoncurrentDays: 0
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
},
|
|
805
|
+
status: 'success'
|
|
806
|
+
});
|
|
807
|
+
renderBucketLifecycleFormPage('test-bucket', 'immediate-noncurrent-expire');
|
|
808
|
+
await waitFor(()=>{
|
|
809
|
+
const noncurrentExpirationToggle = findToggleByLabel('Expiration noncurrent version');
|
|
810
|
+
expect(noncurrentExpirationToggle).toBeChecked();
|
|
811
|
+
const noncurrentDaysInput = screen.getByLabelText(/noncurrent days/i);
|
|
812
|
+
expect(noncurrentDaysInput).toHaveValue(0);
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
});
|
|
712
816
|
describe('Transition Auto-Add in Create Mode', ()=>{
|
|
713
817
|
it('auto-adds a transition row when enabling transitions', async ()=>{
|
|
714
818
|
renderBucketLifecycleFormPage();
|
|
@@ -314,6 +314,84 @@ describe('BucketLifecycleList', ()=>{
|
|
|
314
314
|
});
|
|
315
315
|
expect(screen.getByText('Delete expired markers')).toBeInTheDocument();
|
|
316
316
|
});
|
|
317
|
+
describe('immediate transition rules (0 days)', ()=>{
|
|
318
|
+
it("shows current version transition description for immediate transition", ()=>{
|
|
319
|
+
renderBucketLifecycleList({
|
|
320
|
+
lifecycleRules: [
|
|
321
|
+
{
|
|
322
|
+
ID: 'immediate-glacier',
|
|
323
|
+
Status: 'Enabled',
|
|
324
|
+
Transitions: [
|
|
325
|
+
{
|
|
326
|
+
Days: 0,
|
|
327
|
+
StorageClass: 'GLACIER'
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
});
|
|
333
|
+
expect(screen.getByText(/Transition current \(0 days\)/)).toBeInTheDocument();
|
|
334
|
+
});
|
|
335
|
+
it("shows noncurrent version transition description for immediate transition", ()=>{
|
|
336
|
+
renderBucketLifecycleList({
|
|
337
|
+
lifecycleRules: [
|
|
338
|
+
{
|
|
339
|
+
ID: 'immediate-noncurrent',
|
|
340
|
+
Status: 'Enabled',
|
|
341
|
+
NoncurrentVersionTransitions: [
|
|
342
|
+
{
|
|
343
|
+
NoncurrentDays: 0,
|
|
344
|
+
StorageClass: 'GLACIER'
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
});
|
|
350
|
+
expect(screen.getByText(/Transition noncurrent \(0 days\)/)).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
it("shows expiration description for immediate expiration", ()=>{
|
|
353
|
+
renderBucketLifecycleList({
|
|
354
|
+
lifecycleRules: [
|
|
355
|
+
{
|
|
356
|
+
ID: 'immediate-expire',
|
|
357
|
+
Status: 'Enabled',
|
|
358
|
+
Expiration: {
|
|
359
|
+
Days: 0
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
expect(screen.getByText(/Expire current \(0 days\)/)).toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
it("shows noncurrent expiration description for immediate expiration", ()=>{
|
|
367
|
+
renderBucketLifecycleList({
|
|
368
|
+
lifecycleRules: [
|
|
369
|
+
{
|
|
370
|
+
ID: 'immediate-noncurrent-expire',
|
|
371
|
+
Status: 'Enabled',
|
|
372
|
+
NoncurrentVersionExpiration: {
|
|
373
|
+
NoncurrentDays: 0
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
});
|
|
378
|
+
expect(screen.getByText(/Expire noncurrent \(0 days\)/)).toBeInTheDocument();
|
|
379
|
+
});
|
|
380
|
+
it("shows abort MPU description for immediate abort", ()=>{
|
|
381
|
+
renderBucketLifecycleList({
|
|
382
|
+
lifecycleRules: [
|
|
383
|
+
{
|
|
384
|
+
ID: 'immediate-mpu',
|
|
385
|
+
Status: 'Enabled',
|
|
386
|
+
AbortIncompleteMultipartUpload: {
|
|
387
|
+
DaysAfterInitiation: 0
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
expect(screen.getByText(/Abort Incomplete MPU \(0 days\)/)).toBeInTheDocument();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
317
395
|
it('shows info icon for multiple actions', ()=>{
|
|
318
396
|
renderBucketLifecycleList({
|
|
319
397
|
lifecycleRules: mockLifecycleRules
|
|
@@ -226,13 +226,13 @@ const ruleToFormValues = (rule)=>{
|
|
|
226
226
|
formValues.transitionsEnabled = true;
|
|
227
227
|
formValues.transitions = rule.Transitions.map((t)=>({
|
|
228
228
|
timeType: void 0 !== t.Days ? 'days' : 'date',
|
|
229
|
-
days: t.Days
|
|
229
|
+
days: t.Days ?? 30,
|
|
230
230
|
date: t.Date ? new Date(t.Date).toISOString().split('T')[0] : '',
|
|
231
231
|
storageClass: t.StorageClass || 'GLACIER'
|
|
232
232
|
}));
|
|
233
233
|
}
|
|
234
234
|
if (rule.Expiration) {
|
|
235
|
-
if (rule.Expiration.Days) {
|
|
235
|
+
if (null != rule.Expiration.Days) {
|
|
236
236
|
formValues.expirationEnabled = true;
|
|
237
237
|
formValues.expirationType = 'days';
|
|
238
238
|
formValues.expirationDays = rule.Expiration.Days;
|
|
@@ -246,19 +246,19 @@ const ruleToFormValues = (rule)=>{
|
|
|
246
246
|
if (rule.NoncurrentVersionTransitions && rule.NoncurrentVersionTransitions.length > 0) {
|
|
247
247
|
formValues.noncurrentTransitionsEnabled = true;
|
|
248
248
|
formValues.noncurrentTransitions = rule.NoncurrentVersionTransitions.map((t)=>({
|
|
249
|
-
noncurrentDays: t.NoncurrentDays
|
|
249
|
+
noncurrentDays: t.NoncurrentDays ?? 30,
|
|
250
250
|
storageClass: t.StorageClass || 'GLACIER',
|
|
251
251
|
newerNoncurrentVersions: t.NewerNoncurrentVersions || 0
|
|
252
252
|
}));
|
|
253
253
|
}
|
|
254
254
|
if (rule.NoncurrentVersionExpiration) {
|
|
255
255
|
formValues.noncurrentExpirationEnabled = true;
|
|
256
|
-
formValues.noncurrentExpirationDays = rule.NoncurrentVersionExpiration.NoncurrentDays
|
|
256
|
+
formValues.noncurrentExpirationDays = rule.NoncurrentVersionExpiration.NoncurrentDays ?? 30;
|
|
257
257
|
formValues.noncurrentNewerVersions = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions || 0;
|
|
258
258
|
}
|
|
259
259
|
if (rule.AbortIncompleteMultipartUpload) {
|
|
260
260
|
formValues.abortMpuEnabled = true;
|
|
261
|
-
formValues.abortMpuDays = rule.AbortIncompleteMultipartUpload.DaysAfterInitiation
|
|
261
|
+
formValues.abortMpuDays = rule.AbortIncompleteMultipartUpload.DaysAfterInitiation ?? 7;
|
|
262
262
|
}
|
|
263
263
|
return formValues;
|
|
264
264
|
};
|
|
@@ -9,7 +9,7 @@ const pluralizeDays = (days)=>1 === days ? `${days} day` : `${days} days`;
|
|
|
9
9
|
const formatActions = (rule)=>{
|
|
10
10
|
const actions = [];
|
|
11
11
|
if (rule.Expiration) {
|
|
12
|
-
if (rule.Expiration.Days) actions.push({
|
|
12
|
+
if (null != rule.Expiration.Days) actions.push({
|
|
13
13
|
text: `Expire current (${pluralizeDays(rule.Expiration.Days)})`,
|
|
14
14
|
description: `Expire current versions of objects after ${pluralizeDays(rule.Expiration.Days)}`
|
|
15
15
|
});
|
|
@@ -23,7 +23,7 @@ const formatActions = (rule)=>{
|
|
|
23
23
|
description: 'Delete expired object delete markers'
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
if (rule.NoncurrentVersionExpiration?.NoncurrentDays) {
|
|
26
|
+
if (rule.NoncurrentVersionExpiration?.NoncurrentDays != null) {
|
|
27
27
|
const keepText = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions ? `, keep ${rule.NoncurrentVersionExpiration.NewerNoncurrentVersions}` : '';
|
|
28
28
|
const keepDescription = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions ? `, keep ${rule.NoncurrentVersionExpiration.NewerNoncurrentVersions} newest versions` : '';
|
|
29
29
|
actions.push({
|
|
@@ -32,7 +32,7 @@ const formatActions = (rule)=>{
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
if (rule.Transitions && rule.Transitions.length > 0) rule.Transitions.forEach((transition)=>{
|
|
35
|
-
if (transition.Days) actions.push({
|
|
35
|
+
if (null != transition.Days) actions.push({
|
|
36
36
|
text: `Transition current (${pluralizeDays(transition.Days)})`,
|
|
37
37
|
description: `Transition current versions of objects after ${pluralizeDays(transition.Days)}`
|
|
38
38
|
});
|
|
@@ -43,7 +43,7 @@ const formatActions = (rule)=>{
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
if (rule.NoncurrentVersionTransitions && rule.NoncurrentVersionTransitions.length > 0) rule.NoncurrentVersionTransitions.forEach((transition)=>{
|
|
46
|
-
if (transition.NoncurrentDays) {
|
|
46
|
+
if (null != transition.NoncurrentDays) {
|
|
47
47
|
const keepText = transition.NewerNoncurrentVersions ? `, keep ${transition.NewerNoncurrentVersions}` : '';
|
|
48
48
|
const keepDescription = transition.NewerNoncurrentVersions ? `, keep ${transition.NewerNoncurrentVersions} newest versions` : '';
|
|
49
49
|
actions.push({
|
|
@@ -52,7 +52,7 @@ const formatActions = (rule)=>{
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
if (rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation) actions.push({
|
|
55
|
+
if (rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation != null) actions.push({
|
|
56
56
|
text: `Abort Incomplete MPU (${pluralizeDays(rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)})`,
|
|
57
57
|
description: `Abort incomplete multipart uploads after ${pluralizeDays(rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)}`
|
|
58
58
|
});
|
|
@@ -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,
|
|
@@ -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 = [];
|
|
@@ -4,7 +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 { 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
9
|
import { EnhancedS3Error, ErrorCategory } from "../../../../utils/errorHandling.js";
|
|
10
10
|
import { ObjectSummary } from "../ObjectSummary.js";
|
|
@@ -16,6 +16,7 @@ jest.mock('@scality/core-ui', ()=>{
|
|
|
16
16
|
useToast: jest.fn()
|
|
17
17
|
};
|
|
18
18
|
});
|
|
19
|
+
const mockIsLocationCold = jest.fn(()=>false);
|
|
19
20
|
jest.mock('../../../providers/DataBrowserProvider', ()=>({
|
|
20
21
|
...jest.requireActual('../../../providers/DataBrowserProvider'),
|
|
21
22
|
useDataBrowserConfig: jest.fn(()=>({
|
|
@@ -31,6 +32,7 @@ const mockUseObjectAcl = jest.mocked(useObjectAcl);
|
|
|
31
32
|
const mockUseSetObjectAcl = jest.mocked(useSetObjectAcl);
|
|
32
33
|
const mockUseGetPublicAccessBlock = jest.mocked(useGetPublicAccessBlock);
|
|
33
34
|
const mockUseGetPresignedDownload = jest.mocked(useGetPresignedDownload);
|
|
35
|
+
const mockUseRestoreObject = jest.mocked(useRestoreObject);
|
|
34
36
|
const mockUseToast = jest.mocked(useToast);
|
|
35
37
|
const mockObjectMetadata = {
|
|
36
38
|
VersionId: 'test-version-id',
|
|
@@ -99,6 +101,11 @@ const setupMockDefaults = ()=>{
|
|
|
99
101
|
mutateAsync: jest.fn(),
|
|
100
102
|
isPending: false
|
|
101
103
|
});
|
|
104
|
+
mockUseRestoreObject.mockReturnValue({
|
|
105
|
+
mutate: jest.fn(),
|
|
106
|
+
isPending: false
|
|
107
|
+
});
|
|
108
|
+
mockIsLocationCold.mockReturnValue(false);
|
|
102
109
|
mockUseToast.mockReturnValue({
|
|
103
110
|
showToast: jest.fn()
|
|
104
111
|
});
|
|
@@ -126,7 +133,10 @@ const renderWithProviders = (ui, { customizationConfig = {} } = {})=>{
|
|
|
126
133
|
return render(/*#__PURE__*/ jsx(QueryClientProvider, {
|
|
127
134
|
client: queryClient,
|
|
128
135
|
children: /*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
|
|
129
|
-
config:
|
|
136
|
+
config: {
|
|
137
|
+
isLocationCold: mockIsLocationCold,
|
|
138
|
+
...customizationConfig
|
|
139
|
+
},
|
|
130
140
|
children: ui
|
|
131
141
|
})
|
|
132
142
|
}));
|
|
@@ -1097,4 +1107,97 @@ describe('ObjectSummary', ()=>{
|
|
|
1097
1107
|
await expectTooltipOnHover(toggle, 'Unable to check public access block settings');
|
|
1098
1108
|
});
|
|
1099
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
|
+
});
|
|
1100
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, {
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseRestoreStatus } from "../coldStorage.js";
|
|
2
|
+
describe('parseRestoreStatus', ()=>{
|
|
3
|
+
it('returns "cold" when restore header is undefined', ()=>{
|
|
4
|
+
expect(parseRestoreStatus(void 0)).toEqual({
|
|
5
|
+
status: 'cold'
|
|
6
|
+
});
|
|
7
|
+
});
|
|
8
|
+
it('returns "restoring" when ongoing-request="true"', ()=>{
|
|
9
|
+
expect(parseRestoreStatus('ongoing-request="true"')).toEqual({
|
|
10
|
+
status: 'restoring'
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it('returns "restored" with expiry date when ongoing-request="false"', ()=>{
|
|
14
|
+
const header = 'ongoing-request="false", expiry-date="Fri, 02 May 2026 00:00:00 GMT"';
|
|
15
|
+
const result = parseRestoreStatus(header);
|
|
16
|
+
expect(result.status).toBe('restored');
|
|
17
|
+
if ('restored' === result.status) expect(result.expiryDate).toEqual(new Date('Fri, 02 May 2026 00:00:00 GMT'));
|
|
18
|
+
});
|
|
19
|
+
it('returns "restored" without expiry if date is missing', ()=>{
|
|
20
|
+
expect(parseRestoreStatus('ongoing-request="false"')).toEqual({
|
|
21
|
+
status: 'restored'
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export declare const COLD_STORAGE_TOOLTIP = "The Temperature of this Location is Cold.\n\nYou can move your data in this Location through a Transition rule.\n\nOnce your data are in this Location, you can only trigger a request for restoration to get a temporary access to the object.";
|
|
3
|
+
export declare const COLD_STORAGE_TOOLTIP_STYLE: React.CSSProperties;
|
|
4
|
+
export type ColdStorageStatus = {
|
|
5
|
+
status: 'cold';
|
|
6
|
+
} | {
|
|
7
|
+
status: 'restoring';
|
|
8
|
+
} | {
|
|
9
|
+
status: 'restored';
|
|
10
|
+
expiryDate?: Date;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseRestoreStatus(restoreHeader: string | undefined): ColdStorageStatus;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const COLD_STORAGE_TOOLTIP = 'The Temperature of this Location is Cold.\n\nYou can move your data in this Location through a Transition rule.\n\nOnce your data are in this Location, you can only trigger a request for restoration to get a temporary access to the object.';
|
|
2
|
+
const COLD_STORAGE_TOOLTIP_STYLE = {
|
|
3
|
+
textWrap: 'wrap',
|
|
4
|
+
width: '24rem',
|
|
5
|
+
textAlign: 'left'
|
|
6
|
+
};
|
|
7
|
+
function parseRestoreStatus(restoreHeader) {
|
|
8
|
+
if (!restoreHeader) return {
|
|
9
|
+
status: 'cold'
|
|
10
|
+
};
|
|
11
|
+
if (restoreHeader.includes('ongoing-request="true"')) return {
|
|
12
|
+
status: 'restoring'
|
|
13
|
+
};
|
|
14
|
+
const expiryMatch = restoreHeader.match(/expiry-date="([^"]+)"/);
|
|
15
|
+
if (expiryMatch) return {
|
|
16
|
+
status: 'restored',
|
|
17
|
+
expiryDate: new Date(expiryMatch[1])
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
status: 'restored'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export { COLD_STORAGE_TOOLTIP, COLD_STORAGE_TOOLTIP_STYLE, parseRestoreStatus };
|