@scality/data-browser-library 1.0.1 → 1.0.4

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.
@@ -143,7 +143,7 @@ const BucketCorsPage = ()=>{
143
143
  },
144
144
  children: [
145
145
  /*#__PURE__*/ jsx(Icon, {
146
- name: "Exclamation-triangle",
146
+ name: "Exclamation-circle",
147
147
  size: "2x",
148
148
  color: "statusWarning"
149
149
  }),
@@ -80,6 +80,8 @@ const BucketCreate = ({ subTitle, validationSchema, validationContext, defaultVa
80
80
  const { register, handleSubmit, formState, watch } = useFormMethods;
81
81
  const { isValid, errors } = formState;
82
82
  const isObjectLockEnabled = watch('isObjectLockEnabled');
83
+ const isEncryptionEnabled = watch('isEncryptionEnabled');
84
+ const isVersioning = watch('isVersioning');
83
85
  const { s3Capabilities } = useDataBrowserConfig();
84
86
  const { mutate: createBucket, isPending: isCreatingBucket } = useCreateBucket();
85
87
  const { mutate: setBucketVersioning } = useSetBucketVersioning();
@@ -242,6 +244,7 @@ const BucketCreate = ({ subTitle, validationSchema, validationContext, defaultVa
242
244
  helpErrorPosition: "bottom",
243
245
  content: /*#__PURE__*/ jsx(Checkbox, {
244
246
  id: "isEncryptionEnabled",
247
+ label: isEncryptionEnabled ? 'Enabled' : 'Disabled',
245
248
  ...register('isEncryptionEnabled')
246
249
  })
247
250
  }),
@@ -263,6 +266,7 @@ const BucketCreate = ({ subTitle, validationSchema, validationContext, defaultVa
263
266
  helpErrorPosition: "bottom",
264
267
  content: /*#__PURE__*/ jsx(Checkbox, {
265
268
  id: "isVersioning",
269
+ label: isVersioning ? 'Enabled' : 'Disabled',
266
270
  disabled: isObjectLockEnabled,
267
271
  ...register('isVersioning')
268
272
  })
@@ -257,9 +257,27 @@ const generateStorageClassHelpText = ()=>{
257
257
  const hints = [];
258
258
  storageClassOptions.forEach((option)=>{
259
259
  const minDays = STORAGE_CLASS_MIN_DAYS[option.value];
260
- if (minDays > 0) hints.push(`<li>${option.label}: min ${minDays} days</li>`);
260
+ if (minDays > 0) hints.push({
261
+ label: option.label,
262
+ minDays
263
+ });
264
+ });
265
+ if (0 === hints.length) return;
266
+ return /*#__PURE__*/ jsxs(Fragment, {
267
+ children: [
268
+ "Storage class requirements:",
269
+ /*#__PURE__*/ jsx("ul", {
270
+ children: hints.map((hint)=>/*#__PURE__*/ jsxs("li", {
271
+ children: [
272
+ hint.label,
273
+ ": min ",
274
+ hint.minDays,
275
+ " days"
276
+ ]
277
+ }, hint.label))
278
+ })
279
+ ]
261
280
  });
262
- return hints.length > 0 ? `Storage class requirements:<ul>${hints.join('')}</ul>` : void 0;
263
281
  };
264
282
  function BucketLifecycleFormPage() {
265
283
  const { bucketName, ruleId } = useParams();
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { ConstrainedText, FormattedDateTime, Icon, Stack, Tooltip, Wrap, spacing } from "@scality/core-ui";
2
+ import { ConstrainedText, FormattedDateTime, Icon, Stack, Tooltip, spacing } from "@scality/core-ui";
3
3
  import { Box, Button, Table } from "@scality/core-ui/dist/next";
4
4
  import { useMemo } from "react";
5
5
  import { useDeleteBucketLifecycle, useSetBucketLifecycle } from "../../hooks/bucketConfiguration.js";
@@ -242,20 +242,18 @@ function BucketLifecycleList({ bucketName, lifecycleRules, lifecycleStatus, onCr
242
242
  }
243
243
  },
244
244
  children: [
245
- /*#__PURE__*/ jsx(Wrap, {
245
+ /*#__PURE__*/ jsx(Box, {
246
+ display: "flex",
247
+ justifyContent: "flex-end",
246
248
  padding: spacing.r16,
247
- children: /*#__PURE__*/ jsx(Box, {
248
- display: "flex",
249
- justifyContent: "flex-end",
250
- children: /*#__PURE__*/ jsx(Button, {
251
- icon: /*#__PURE__*/ jsx(Icon, {
252
- name: "Create-add"
253
- }),
254
- label: "Create Rule",
255
- variant: "primary",
256
- onClick: onCreateRule,
257
- type: "button"
258
- })
249
+ children: /*#__PURE__*/ jsx(Button, {
250
+ icon: /*#__PURE__*/ jsx(Icon, {
251
+ name: "Create-add"
252
+ }),
253
+ label: "Create Rule",
254
+ variant: "primary",
255
+ onClick: onCreateRule,
256
+ type: "button"
259
257
  })
260
258
  }),
261
259
  /*#__PURE__*/ jsx(Table.SingleSelectableContent, {
@@ -110,7 +110,7 @@ const BucketPolicyPage = ()=>{
110
110
  },
111
111
  children: [
112
112
  /*#__PURE__*/ jsx(Icon, {
113
- name: "Exclamation-triangle",
113
+ name: "Exclamation-circle",
114
114
  size: "2x",
115
115
  color: "statusWarning"
116
116
  }),
@@ -158,7 +158,7 @@ const BucketPolicyPage = ()=>{
158
158
  variant: "warning",
159
159
  icon: /*#__PURE__*/ jsx(Icon, {
160
160
  color: "statusWarning",
161
- name: "Exclamation-triangle"
161
+ name: "Exclamation-circle"
162
162
  }),
163
163
  children: /*#__PURE__*/ jsxs(Text, {
164
164
  children: [
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { ConstrainedText, Icon, Wrap, spacing } from "@scality/core-ui";
2
+ import { ConstrainedText, Icon, spacing } from "@scality/core-ui";
3
3
  import { Box, Button, Table } from "@scality/core-ui/dist/next";
4
4
  import { useMemo } from "react";
5
5
  import { useDeleteBucketReplication, useSetBucketReplication } from "../../hooks/bucketConfiguration.js";
@@ -166,7 +166,9 @@ function BucketReplicationList({ bucketName, replicationRules, replicationRole,
166
166
  }
167
167
  },
168
168
  children: [
169
- /*#__PURE__*/ jsx(Wrap, {
169
+ /*#__PURE__*/ jsx(Box, {
170
+ display: "flex",
171
+ justifyContent: "flex-end",
170
172
  padding: spacing.r16,
171
173
  children: /*#__PURE__*/ jsx(Button, {
172
174
  icon: /*#__PURE__*/ jsx(Icon, {
@@ -174,7 +176,8 @@ function BucketReplicationList({ bucketName, replicationRules, replicationRole,
174
176
  }),
175
177
  label: "Create Rule",
176
178
  variant: "primary",
177
- onClick: onCreateRule
179
+ onClick: onCreateRule,
180
+ type: "button"
178
181
  })
179
182
  }),
180
183
  /*#__PURE__*/ jsx(Table.SingleSelectableContent, {
@@ -8,6 +8,7 @@ import { useCopyObject, useObjectMetadata } from "../../../hooks/index.js";
8
8
  import { useInvalidateQueries } from "../../providers/DataBrowserProvider.js";
9
9
  import { ArrayFieldActions } from "../../ui/ArrayFieldActions.js";
10
10
  import { TableContainer } from "../../ui/Table.elements.js";
11
+ import { hasFormDataChanged } from "./formUtils.js";
11
12
  const METADATA_KEYS = [
12
13
  {
13
14
  key: 'CacheControl',
@@ -47,6 +48,7 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
47
48
  const { showToast } = useToast();
48
49
  const invalidateQueries = useInvalidateQueries();
49
50
  const [isInitialized, setIsInitialized] = useState(false);
51
+ const [originalMetadata, setOriginalMetadata] = useState([]);
50
52
  const { control, handleSubmit, setValue, watch, formState: { errors, isValid, isSubmitting } } = useForm({
51
53
  mode: 'onChange',
52
54
  defaultValues: {
@@ -57,6 +59,14 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
57
59
  control,
58
60
  name: 'metadata'
59
61
  });
62
+ const normalizeMetadata = (metadata)=>metadata.filter((row)=>{
63
+ const hasKey = 'x-amz-meta' === row.keyType ? row.customKey.trim() : row.keyType;
64
+ return hasKey && row.value.trim();
65
+ }).map((row)=>({
66
+ key: 'x-amz-meta' === row.keyType ? `x-amz-meta-${row.customKey.trim()}` : row.keyType,
67
+ value: row.value.trim()
68
+ })).sort((a, b)=>a.key.localeCompare(b.key));
69
+ const hasChanges = ()=>hasFormDataChanged(watch('metadata'), originalMetadata, normalizeMetadata);
60
70
  useEffect(()=>{
61
71
  setIsInitialized(false);
62
72
  copyObjectMutation.reset();
@@ -97,6 +107,7 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
97
107
  value: ''
98
108
  });
99
109
  setValue('metadata', rows);
110
+ setOriginalMetadata(rows);
100
111
  setIsInitialized(true);
101
112
  }
102
113
  }, [
@@ -113,7 +124,10 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
113
124
  const onSubmit = async (data)=>{
114
125
  if (!metadata) return;
115
126
  try {
116
- const validMetadata = data.metadata.filter((row)=>row.keyType && row.value.trim());
127
+ const validMetadata = data.metadata.filter((row)=>{
128
+ const hasKey = 'x-amz-meta' === row.keyType ? row.customKey.trim() : row.keyType;
129
+ return hasKey && row.value.trim();
130
+ });
117
131
  const encodedKey = encodeURIComponent(objectKey);
118
132
  const copySource = versionId ? `${bucketName}/${encodedKey}?versionId=${versionId}` : `${bucketName}/${encodedKey}`;
119
133
  const copyParams = {
@@ -189,7 +203,7 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
189
203
  /*#__PURE__*/ jsx(Button, {
190
204
  variant: "primary",
191
205
  label: "Save",
192
- disabled: !isValid || isSubmitting,
206
+ disabled: !isValid || isSubmitting || !hasChanges(),
193
207
  type: "submit",
194
208
  icon: /*#__PURE__*/ jsx(Icon, {
195
209
  name: "Save"
@@ -321,8 +335,20 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
321
335
  customKey: '',
322
336
  value: ''
323
337
  }),
324
- canRemove: true,
325
- canAdd: !!watch(`metadata.${index}.keyType`) && !!watch(`metadata.${index}.value`),
338
+ canRemove: (()=>{
339
+ const keyType = watch(`metadata.${index}.keyType`);
340
+ const customKey = watch(`metadata.${index}.customKey`);
341
+ const value = watch(`metadata.${index}.value`);
342
+ const hasKey = 'x-amz-meta' === keyType ? customKey : keyType;
343
+ return !!hasKey && !!value;
344
+ })(),
345
+ canAdd: (()=>{
346
+ const keyType = watch(`metadata.${index}.keyType`);
347
+ const customKey = watch(`metadata.${index}.customKey`);
348
+ const value = watch(`metadata.${index}.value`);
349
+ const hasKey = 'x-amz-meta' === keyType ? customKey : keyType;
350
+ return !!hasKey && !!value;
351
+ })(),
326
352
  removeLabel: "Remove metadata",
327
353
  addLabel: "Add metadata"
328
354
  })
@@ -7,6 +7,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
7
7
  import { useDeleteObjectTagging, useObjectTagging, useSetObjectTagging } from "../../../hooks/index.js";
8
8
  import { useInvalidateQueries } from "../../providers/DataBrowserProvider.js";
9
9
  import { ArrayFieldActions } from "../../ui/ArrayFieldActions.js";
10
+ import { hasFormDataChanged } from "./formUtils.js";
10
11
  const MAX_TAGS_PER_OBJECT = 10;
11
12
  const MAX_TAG_KEY_LENGTH = 128;
12
13
  const MAX_TAG_VALUE_LENGTH = 256;
@@ -24,6 +25,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
24
25
  const { showToast } = useToast();
25
26
  const invalidateQueries = useInvalidateQueries();
26
27
  const [isInitialized, setIsInitialized] = useState(false);
28
+ const [originalTags, setOriginalTags] = useState([]);
27
29
  const { control, handleSubmit, setValue, watch, formState: { errors, isValid, isSubmitting } } = useForm({
28
30
  mode: 'onChange',
29
31
  defaultValues: {
@@ -34,6 +36,11 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
34
36
  control,
35
37
  name: 'tags'
36
38
  });
39
+ const normalizeTags = (tags)=>tags.filter((tag)=>tag.key.trim() && tag.value.trim()).map((tag)=>({
40
+ key: tag.key.trim(),
41
+ value: tag.value.trim()
42
+ })).sort((a, b)=>a.key.localeCompare(b.key));
43
+ const hasChanges = ()=>hasFormDataChanged(watch('tags'), originalTags, normalizeTags);
37
44
  useEffect(()=>{
38
45
  setIsInitialized(false);
39
46
  setTaggingMutation.reset();
@@ -55,6 +62,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
55
62
  value: ''
56
63
  });
57
64
  setValue('tags', rows);
65
+ setOriginalTags(rows);
58
66
  setIsInitialized(true);
59
67
  }
60
68
  }, [
@@ -138,7 +146,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
138
146
  /*#__PURE__*/ jsx(Button, {
139
147
  variant: "primary",
140
148
  label: "Save",
141
- disabled: !isValid || isSubmitting,
149
+ disabled: !isValid || isSubmitting || !hasChanges(),
142
150
  type: "submit",
143
151
  icon: /*#__PURE__*/ jsx(Icon, {
144
152
  name: "Save"
@@ -241,7 +249,7 @@ const ObjectTags = ({ bucketName, objectKey, versionId })=>{
241
249
  key: '',
242
250
  value: ''
243
251
  }),
244
- canRemove: true,
252
+ canRemove: !!watch(`tags.${index}.key`) && !!watch(`tags.${index}.value`),
245
253
  canAdd: fields.length < MAX_TAGS_PER_OBJECT && !!watch(`tags.${index}.key`) && !!watch(`tags.${index}.value`),
246
254
  removeLabel: "Remove tag",
247
255
  addLabel: "Add tag"
@@ -5,7 +5,7 @@ import { cleanup, render, screen } from "@testing-library/react";
5
5
  import { MemoryRouter, Route, Routes } from "react-router";
6
6
  import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
7
7
  var __webpack_modules__ = {
8
- ".." (module) {
8
+ "../index" (module) {
9
9
  module.exports = __rspack_external__index_js_95fdb65a;
10
10
  }
11
11
  };
@@ -19,7 +19,7 @@ function __webpack_require__(moduleId) {
19
19
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
20
20
  return module.exports;
21
21
  }
22
- var external_index_js_ = __webpack_require__("..");
22
+ var external_index_js_ = __webpack_require__("../index");
23
23
  jest.mock('../ObjectSummary', ()=>({
24
24
  ObjectSummary: ()=>/*#__PURE__*/ jsx("div", {
25
25
  "data-testid": "object-summary",
@@ -398,7 +398,7 @@ describe('ObjectDetails', ()=>{
398
398
  expect(screen.getByTestId('object-summary')).toBeInTheDocument();
399
399
  });
400
400
  it('should throw error when useObjectDetailsContext is used outside provider', ()=>{
401
- const { useObjectDetailsContext } = __webpack_require__("..");
401
+ const { useObjectDetailsContext } = __webpack_require__("../index");
402
402
  const TestComponent = ()=>{
403
403
  useObjectDetailsContext();
404
404
  return null;
@@ -0,0 +1,15 @@
1
+ interface NormalizedItem {
2
+ key: string;
3
+ value: string;
4
+ }
5
+ /**
6
+ * Detects if form data has actually changed by comparing normalized values.
7
+ * Filters out empty entries and compares trimmed, sorted values.
8
+ *
9
+ * @param currentData - current form values
10
+ * @param originalData - original form values
11
+ * @param normalizer - function to normalize field values for comparison
12
+ * @returns true if data has changed, false otherwise
13
+ */
14
+ export declare function hasFormDataChanged<T>(currentData: T[], originalData: T[], normalizer: (data: T[]) => NormalizedItem[]): boolean;
15
+ export {};
@@ -0,0 +1,7 @@
1
+ function hasFormDataChanged(currentData, originalData, normalizer) {
2
+ const normalizedCurrent = normalizer(currentData);
3
+ const normalizedOriginal = normalizer(originalData);
4
+ if (normalizedCurrent.length !== normalizedOriginal.length) return true;
5
+ return normalizedCurrent.some((item, index)=>item.key !== normalizedOriginal[index]?.key || item.value !== normalizedOriginal[index]?.value);
6
+ }
7
+ export { hasFormDataChanged };
@@ -1,6 +1,6 @@
1
- import type { S3ClientConfig } from "@aws-sdk/client-s3";
2
- import type { AwsCredentialIdentity } from "@aws-sdk/types";
3
- import type { ProxyConfiguration, S3EventType } from "../config/types";
1
+ import type { S3ClientConfig } from '@aws-sdk/client-s3';
2
+ import type { AwsCredentialIdentity } from '@aws-sdk/types';
3
+ import type { ProxyConfiguration, S3EventType } from '../config/types';
4
4
  export type { _Object, Bucket, BucketCannedACL, BucketLocationConstraint, CopyObjectCommandInput, CopyObjectCommandOutput, CreateBucketCommandInput, CreateBucketCommandOutput, DeleteBucketCommandInput, DeleteBucketCommandOutput, DeleteBucketCorsCommandOutput, DeleteBucketLifecycleCommandOutput, DeleteBucketPolicyCommandOutput, DeleteBucketReplicationCommandOutput, DeleteBucketTaggingCommandOutput, DeleteMarkerEntry, DeleteObjectCommandInput, DeleteObjectCommandOutput, DeleteObjectsCommandInput, DeleteObjectsCommandOutput, DeleteObjectTaggingCommandOutput, GetBucketAclCommandOutput, GetBucketCorsCommandOutput, GetBucketEncryptionCommandOutput, GetBucketLifecycleConfigurationCommandOutput, GetBucketLocationCommandInput, GetBucketLocationCommandOutput, GetBucketNotificationConfigurationCommandOutput, GetBucketPolicyCommandOutput, GetBucketReplicationCommandOutput, GetBucketTaggingCommandOutput, GetBucketVersioningCommandOutput, GetObjectAclCommandOutput, GetObjectAttributesCommandInput, GetObjectAttributesCommandOutput, GetObjectCommandInput, GetObjectCommandOutput, GetObjectLegalHoldCommandOutput, GetObjectLockConfigurationCommandOutput, GetObjectRetentionCommandOutput, GetObjectTaggingCommandOutput, GetObjectTorrentCommandOutput, HeadObjectCommandInput, HeadObjectCommandOutput, ListBucketsCommandOutput, ListMultipartUploadsCommandInput, ListMultipartUploadsCommandOutput, ListObjectsV2CommandInput, ListObjectsV2CommandOutput, ListObjectVersionsCommandInput, ListObjectVersionsCommandOutput, ObjectCannedACL, ObjectVersion, Owner, PutBucketAclCommandInput, PutBucketCorsCommandInput, PutBucketEncryptionCommandInput, PutBucketLifecycleConfigurationCommandInput, PutBucketNotificationConfigurationCommandInput, PutBucketPolicyCommandInput, PutBucketReplicationCommandInput, PutBucketTaggingCommandInput, PutBucketTaggingCommandOutput, PutBucketVersioningCommandInput, PutObjectAclCommandInput, PutObjectCommandInput, PutObjectCommandOutput, PutObjectLegalHoldCommandInput, PutObjectLockConfigurationCommandInput, PutObjectRetentionCommandInput, PutObjectTaggingCommandInput, RestoreObjectCommandInput, RestoreObjectCommandOutput, SelectObjectContentCommandInput, SelectObjectContentCommandOutput, Tag, } from '@aws-sdk/client-s3';
5
5
  export type { MonacoJsonDefaults, MonacoLanguagesJson } from './monaco';
6
6
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/data-browser-library",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "A modular React component library for browsing S3 buckets and objects",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -25,10 +25,10 @@
25
25
  "clean": "rm -rf dist"
26
26
  },
27
27
  "dependencies": {
28
- "@aws-sdk/client-s3": "^3.478.0",
28
+ "@aws-sdk/client-s3": "^3.983.0",
29
29
  "@aws-sdk/protocol-http": "^3.370.0",
30
- "@aws-sdk/s3-presigned-post": "^3.888.0",
31
- "@aws-sdk/s3-request-presigner": "^3.478.0",
30
+ "@aws-sdk/s3-presigned-post": "^3.983.0",
31
+ "@aws-sdk/s3-request-presigner": "^3.983.0",
32
32
  "@hookform/resolvers": "^5.2.2",
33
33
  "@monaco-editor/react": "^4.7.0",
34
34
  "@scality/zenkoclient": "^2.0.0-preview.1",