@scality/data-browser-library 1.0.4 → 1.0.5
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/DataBrowserUI.js +18 -8
- package/dist/components/__tests__/BucketList.test.js +74 -1
- package/dist/components/__tests__/ObjectList.test.js +94 -2
- package/dist/components/buckets/BucketCreate.d.ts +1 -0
- package/dist/components/buckets/BucketCreate.js +57 -7
- package/dist/components/buckets/BucketDetails.js +0 -1
- package/dist/components/buckets/BucketLifecycleFormPage.js +209 -213
- package/dist/components/buckets/BucketList.js +25 -4
- package/dist/components/buckets/BucketReplicationFormPage.js +9 -3
- package/dist/components/buckets/DeleteBucketConfigRuleButton.js +1 -1
- package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
- package/dist/components/objects/DeleteObjectButton.d.ts +1 -0
- package/dist/components/objects/DeleteObjectButton.js +11 -5
- package/dist/components/objects/ObjectDetails/FormComponents.d.ts +9 -0
- package/dist/components/objects/ObjectDetails/FormComponents.js +37 -0
- package/dist/components/objects/ObjectDetails/ObjectMetadata.js +182 -204
- package/dist/components/objects/ObjectDetails/ObjectSummary.js +22 -5
- package/dist/components/objects/ObjectDetails/ObjectTags.js +109 -154
- package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
- package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.d.ts +1 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.js +230 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.d.ts +1 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.js +342 -0
- package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.d.ts +1 -0
- package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.js +202 -0
- package/dist/components/objects/ObjectDetails/index.d.ts +2 -1
- package/dist/components/objects/ObjectDetails/index.js +12 -16
- package/dist/components/objects/ObjectList.d.ts +3 -2
- package/dist/components/objects/ObjectList.js +204 -104
- package/dist/components/objects/ObjectPage.js +22 -5
- package/dist/components/ui/ArrayFieldActions.js +0 -2
- package/dist/components/ui/FilterFormSection.js +17 -36
- package/dist/components/ui/FormGrid.d.ts +7 -0
- package/dist/components/ui/FormGrid.js +37 -0
- package/dist/components/ui/Table.elements.js +1 -0
- package/dist/config/types.d.ts +45 -2
- package/dist/hooks/__tests__/usePresigningS3Client.test.d.ts +1 -0
- package/dist/hooks/__tests__/usePresigningS3Client.test.js +104 -0
- package/dist/hooks/factories/index.d.ts +1 -1
- package/dist/hooks/factories/index.js +2 -2
- package/dist/hooks/factories/useCreateS3MutationHook.d.ts +2 -1
- package/dist/hooks/factories/useCreateS3MutationHook.js +10 -3
- package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -0
- package/dist/hooks/factories/useCreateS3QueryHook.js +9 -6
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/presignedOperations.js +4 -4
- package/dist/hooks/useBucketLocations.d.ts +6 -0
- package/dist/hooks/useBucketLocations.js +45 -0
- package/dist/hooks/usePresigningS3Client.d.ts +13 -0
- package/dist/hooks/usePresigningS3Client.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useToast } from "@scality/core-ui";
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import user_event from "@testing-library/user-event";
|
|
6
|
+
import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
|
|
7
|
+
import { useDeleteObjectTagging, useObjectTagging, useSetObjectTagging } from "../../../../hooks/index.js";
|
|
8
|
+
import { ObjectTags } from "../ObjectTags.js";
|
|
9
|
+
jest.mock('../../../../hooks');
|
|
10
|
+
jest.mock('@scality/core-ui', ()=>{
|
|
11
|
+
const actual = jest.requireActual('@scality/core-ui');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
useToast: jest.fn()
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
jest.mock('../../../providers/DataBrowserProvider', ()=>({
|
|
18
|
+
...jest.requireActual('../../../providers/DataBrowserProvider'),
|
|
19
|
+
useInvalidateQueries: jest.fn(()=>jest.fn().mockResolvedValue(void 0))
|
|
20
|
+
}));
|
|
21
|
+
const mockUseObjectTagging = jest.mocked(useObjectTagging);
|
|
22
|
+
const mockUseSetObjectTagging = jest.mocked(useSetObjectTagging);
|
|
23
|
+
const mockUseDeleteObjectTagging = jest.mocked(useDeleteObjectTagging);
|
|
24
|
+
const mockUseToast = jest.mocked(useToast);
|
|
25
|
+
const mockShowToast = jest.fn();
|
|
26
|
+
const setupMockDefaults = (tagsData = {
|
|
27
|
+
TagSet: []
|
|
28
|
+
})=>{
|
|
29
|
+
mockUseObjectTagging.mockReturnValue({
|
|
30
|
+
data: tagsData,
|
|
31
|
+
status: 'success'
|
|
32
|
+
});
|
|
33
|
+
mockUseSetObjectTagging.mockReturnValue({
|
|
34
|
+
mutateAsync: jest.fn().mockResolvedValue({}),
|
|
35
|
+
reset: jest.fn()
|
|
36
|
+
});
|
|
37
|
+
mockUseDeleteObjectTagging.mockReturnValue({
|
|
38
|
+
mutateAsync: jest.fn().mockResolvedValue({}),
|
|
39
|
+
reset: jest.fn()
|
|
40
|
+
});
|
|
41
|
+
mockUseToast.mockReturnValue({
|
|
42
|
+
showToast: mockShowToast
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const renderWithProviders = (ui)=>{
|
|
46
|
+
const queryClient = new QueryClient({
|
|
47
|
+
defaultOptions: {
|
|
48
|
+
queries: {
|
|
49
|
+
retry: false
|
|
50
|
+
},
|
|
51
|
+
mutations: {
|
|
52
|
+
retry: false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return render(/*#__PURE__*/ jsx(QueryClientProvider, {
|
|
57
|
+
client: queryClient,
|
|
58
|
+
children: /*#__PURE__*/ jsx(DataBrowserUICustomizationProvider, {
|
|
59
|
+
config: {},
|
|
60
|
+
children: ui
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
const defaultProps = {
|
|
65
|
+
bucketName: 'test-bucket',
|
|
66
|
+
objectKey: 'test-object.txt'
|
|
67
|
+
};
|
|
68
|
+
const getSaveButton = ()=>screen.getByRole('button', {
|
|
69
|
+
name: /save/i
|
|
70
|
+
});
|
|
71
|
+
describe('ObjectTags', ()=>{
|
|
72
|
+
beforeEach(()=>{
|
|
73
|
+
jest.clearAllMocks();
|
|
74
|
+
});
|
|
75
|
+
describe('Loading and Error States', ()=>{
|
|
76
|
+
it('should show loader when tags are loading', ()=>{
|
|
77
|
+
mockUseObjectTagging.mockReturnValue({
|
|
78
|
+
data: void 0,
|
|
79
|
+
status: 'pending'
|
|
80
|
+
});
|
|
81
|
+
mockUseSetObjectTagging.mockReturnValue({
|
|
82
|
+
reset: jest.fn()
|
|
83
|
+
});
|
|
84
|
+
mockUseDeleteObjectTagging.mockReturnValue({
|
|
85
|
+
reset: jest.fn()
|
|
86
|
+
});
|
|
87
|
+
mockUseToast.mockReturnValue({
|
|
88
|
+
showToast: mockShowToast
|
|
89
|
+
});
|
|
90
|
+
const { container } = renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
91
|
+
...defaultProps
|
|
92
|
+
}));
|
|
93
|
+
expect(container.querySelectorAll('svg').length).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
it('should show error message when tags fail to load', ()=>{
|
|
96
|
+
mockUseObjectTagging.mockReturnValue({
|
|
97
|
+
data: void 0,
|
|
98
|
+
status: 'error'
|
|
99
|
+
});
|
|
100
|
+
mockUseSetObjectTagging.mockReturnValue({
|
|
101
|
+
reset: jest.fn()
|
|
102
|
+
});
|
|
103
|
+
mockUseDeleteObjectTagging.mockReturnValue({
|
|
104
|
+
reset: jest.fn()
|
|
105
|
+
});
|
|
106
|
+
mockUseToast.mockReturnValue({
|
|
107
|
+
showToast: mockShowToast
|
|
108
|
+
});
|
|
109
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
110
|
+
...defaultProps
|
|
111
|
+
}));
|
|
112
|
+
expect(screen.getByText('Error loading tags')).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('Empty Row Validation', ()=>{
|
|
116
|
+
it('should keep Save disabled when no tags exist and nothing is changed', async ()=>{
|
|
117
|
+
setupMockDefaults({
|
|
118
|
+
TagSet: []
|
|
119
|
+
});
|
|
120
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
121
|
+
...defaultProps
|
|
122
|
+
}));
|
|
123
|
+
await waitFor(()=>{
|
|
124
|
+
expect(screen.getByLabelText('Tag 1 key')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
expect(getSaveButton()).toBeDisabled();
|
|
127
|
+
});
|
|
128
|
+
it('should not block Save when a filled tag has a trailing empty row', async ()=>{
|
|
129
|
+
setupMockDefaults({
|
|
130
|
+
TagSet: []
|
|
131
|
+
});
|
|
132
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
133
|
+
...defaultProps
|
|
134
|
+
}));
|
|
135
|
+
const user = user_event.setup();
|
|
136
|
+
await waitFor(()=>{
|
|
137
|
+
expect(screen.getByLabelText('Tag 1 key')).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
await user.type(screen.getByLabelText('Tag 1 key'), 'env');
|
|
140
|
+
await user.type(screen.getByLabelText('Tag 1 value'), 'prod');
|
|
141
|
+
await waitFor(()=>{
|
|
142
|
+
expect(screen.getByRole('button', {
|
|
143
|
+
name: /add tag/i
|
|
144
|
+
})).toBeEnabled();
|
|
145
|
+
});
|
|
146
|
+
await user.click(screen.getByRole('button', {
|
|
147
|
+
name: /add tag/i
|
|
148
|
+
}));
|
|
149
|
+
await waitFor(()=>{
|
|
150
|
+
expect(screen.getByLabelText('Tag 2 key')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
await waitFor(()=>{
|
|
153
|
+
expect(getSaveButton()).toBeEnabled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it('should show validation error when only key is filled (value empty)', async ()=>{
|
|
157
|
+
setupMockDefaults({
|
|
158
|
+
TagSet: []
|
|
159
|
+
});
|
|
160
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
161
|
+
...defaultProps
|
|
162
|
+
}));
|
|
163
|
+
const user = user_event.setup();
|
|
164
|
+
await waitFor(()=>{
|
|
165
|
+
expect(screen.getByLabelText('Tag 1 key')).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
await user.type(screen.getByLabelText('Tag 1 key'), 'env');
|
|
168
|
+
await user.type(screen.getByLabelText('Tag 1 value'), 'x');
|
|
169
|
+
await user.clear(screen.getByLabelText('Tag 1 value'));
|
|
170
|
+
await waitFor(()=>{
|
|
171
|
+
expect(screen.getByText('Value cannot be empty or whitespace only')).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('Remove Button (canRemove)', ()=>{
|
|
176
|
+
it('should disable remove button for a single empty placeholder row', async ()=>{
|
|
177
|
+
setupMockDefaults({
|
|
178
|
+
TagSet: []
|
|
179
|
+
});
|
|
180
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
181
|
+
...defaultProps
|
|
182
|
+
}));
|
|
183
|
+
await waitFor(()=>{
|
|
184
|
+
expect(screen.getByLabelText('Tag 1 key')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
expect(screen.getByRole('button', {
|
|
187
|
+
name: /remove tag/i
|
|
188
|
+
})).toBeDisabled();
|
|
189
|
+
});
|
|
190
|
+
it('should enable remove button when there are multiple rows', async ()=>{
|
|
191
|
+
setupMockDefaults({
|
|
192
|
+
TagSet: [
|
|
193
|
+
{
|
|
194
|
+
Key: 'env',
|
|
195
|
+
Value: 'prod'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
Key: 'team',
|
|
199
|
+
Value: 'platform'
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
204
|
+
...defaultProps
|
|
205
|
+
}));
|
|
206
|
+
await waitFor(()=>{
|
|
207
|
+
expect(screen.getByLabelText('Tag 1 key')).toHaveValue('env');
|
|
208
|
+
});
|
|
209
|
+
const removeButtons = screen.getAllByRole('button', {
|
|
210
|
+
name: /remove tag/i
|
|
211
|
+
});
|
|
212
|
+
expect(removeButtons[0]).toBeEnabled();
|
|
213
|
+
expect(removeButtons[1]).toBeEnabled();
|
|
214
|
+
});
|
|
215
|
+
it('should enable Save after removing one of two tags', async ()=>{
|
|
216
|
+
setupMockDefaults({
|
|
217
|
+
TagSet: [
|
|
218
|
+
{
|
|
219
|
+
Key: 'env',
|
|
220
|
+
Value: 'prod'
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
Key: 'team',
|
|
224
|
+
Value: 'platform'
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
});
|
|
228
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
229
|
+
...defaultProps
|
|
230
|
+
}));
|
|
231
|
+
const user = user_event.setup();
|
|
232
|
+
await waitFor(()=>{
|
|
233
|
+
expect(screen.getByLabelText('Tag 1 key')).toHaveValue('env');
|
|
234
|
+
});
|
|
235
|
+
const removeButtons = screen.getAllByRole('button', {
|
|
236
|
+
name: /remove tag/i
|
|
237
|
+
});
|
|
238
|
+
await user.click(removeButtons[0]);
|
|
239
|
+
await waitFor(()=>{
|
|
240
|
+
expect(getSaveButton()).toBeEnabled();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('Duplicate Key Validation', ()=>{
|
|
245
|
+
it('should not count empty rows as duplicates', async ()=>{
|
|
246
|
+
setupMockDefaults({
|
|
247
|
+
TagSet: []
|
|
248
|
+
});
|
|
249
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
250
|
+
...defaultProps
|
|
251
|
+
}));
|
|
252
|
+
const user = user_event.setup();
|
|
253
|
+
await waitFor(()=>{
|
|
254
|
+
expect(screen.getByLabelText('Tag 1 key')).toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
await user.type(screen.getByLabelText('Tag 1 key'), 'env');
|
|
257
|
+
await user.type(screen.getByLabelText('Tag 1 value'), 'prod');
|
|
258
|
+
await waitFor(()=>{
|
|
259
|
+
expect(screen.getByRole('button', {
|
|
260
|
+
name: /add tag/i
|
|
261
|
+
})).toBeEnabled();
|
|
262
|
+
});
|
|
263
|
+
await user.click(screen.getByRole('button', {
|
|
264
|
+
name: /add tag/i
|
|
265
|
+
}));
|
|
266
|
+
await waitFor(()=>{
|
|
267
|
+
expect(screen.getByLabelText('Tag 2 key')).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
expect(screen.queryByText(/duplicate key/i)).not.toBeInTheDocument();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('Save Button Change Detection', ()=>{
|
|
273
|
+
it('should disable Save when no actual changes are made', async ()=>{
|
|
274
|
+
setupMockDefaults({
|
|
275
|
+
TagSet: [
|
|
276
|
+
{
|
|
277
|
+
Key: 'env',
|
|
278
|
+
Value: 'prod'
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
});
|
|
282
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
283
|
+
...defaultProps
|
|
284
|
+
}));
|
|
285
|
+
await waitFor(()=>{
|
|
286
|
+
expect(screen.getByLabelText('Tag 1 key')).toHaveValue('env');
|
|
287
|
+
});
|
|
288
|
+
expect(getSaveButton()).toBeDisabled();
|
|
289
|
+
});
|
|
290
|
+
it('should enable Save when a tag value is modified', async ()=>{
|
|
291
|
+
setupMockDefaults({
|
|
292
|
+
TagSet: [
|
|
293
|
+
{
|
|
294
|
+
Key: 'env',
|
|
295
|
+
Value: 'prod'
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
300
|
+
...defaultProps
|
|
301
|
+
}));
|
|
302
|
+
const user = user_event.setup();
|
|
303
|
+
await waitFor(()=>{
|
|
304
|
+
expect(screen.getByLabelText('Tag 1 value')).toHaveValue('prod');
|
|
305
|
+
});
|
|
306
|
+
await user.clear(screen.getByLabelText('Tag 1 value'));
|
|
307
|
+
await user.type(screen.getByLabelText('Tag 1 value'), 'staging');
|
|
308
|
+
await waitFor(()=>{
|
|
309
|
+
expect(getSaveButton()).toBeEnabled();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
it('should keep Save disabled when adding only an empty trailing row to existing tags', async ()=>{
|
|
313
|
+
setupMockDefaults({
|
|
314
|
+
TagSet: [
|
|
315
|
+
{
|
|
316
|
+
Key: 'env',
|
|
317
|
+
Value: 'prod'
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
renderWithProviders(/*#__PURE__*/ jsx(ObjectTags, {
|
|
322
|
+
...defaultProps
|
|
323
|
+
}));
|
|
324
|
+
const user = user_event.setup();
|
|
325
|
+
await waitFor(()=>{
|
|
326
|
+
expect(screen.getByLabelText('Tag 1 key')).toHaveValue('env');
|
|
327
|
+
});
|
|
328
|
+
await waitFor(()=>{
|
|
329
|
+
expect(screen.getByRole('button', {
|
|
330
|
+
name: /add tag/i
|
|
331
|
+
})).toBeEnabled();
|
|
332
|
+
});
|
|
333
|
+
await user.click(screen.getByRole('button', {
|
|
334
|
+
name: /add tag/i
|
|
335
|
+
}));
|
|
336
|
+
await waitFor(()=>{
|
|
337
|
+
expect(screen.getByLabelText('Tag 2 key')).toBeInTheDocument();
|
|
338
|
+
});
|
|
339
|
+
expect(getSaveButton()).toBeDisabled();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { hasFormDataChanged } from "../formUtils.js";
|
|
2
|
+
const identity = (data)=>data;
|
|
3
|
+
const normalizeTags = (tags)=>tags.filter((tag)=>tag.key.trim() && tag.value.trim()).map((tag)=>({
|
|
4
|
+
key: tag.key.trim(),
|
|
5
|
+
value: tag.value.trim()
|
|
6
|
+
})).sort((a, b)=>a.key.localeCompare(b.key));
|
|
7
|
+
describe('hasFormDataChanged', ()=>{
|
|
8
|
+
describe('basic comparison', ()=>{
|
|
9
|
+
it('should return false when both arrays are empty', ()=>{
|
|
10
|
+
expect(hasFormDataChanged([], [], identity)).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it('should return false when data is identical', ()=>{
|
|
13
|
+
const data = [
|
|
14
|
+
{
|
|
15
|
+
key: 'a',
|
|
16
|
+
value: '1'
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
expect(hasFormDataChanged(data, data, identity)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('should return true when lengths differ', ()=>{
|
|
22
|
+
const current = [
|
|
23
|
+
{
|
|
24
|
+
key: 'a',
|
|
25
|
+
value: '1'
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
const original = [
|
|
29
|
+
{
|
|
30
|
+
key: 'a',
|
|
31
|
+
value: '1'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'b',
|
|
35
|
+
value: '2'
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
expect(hasFormDataChanged(current, original, identity)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('should return true when values differ', ()=>{
|
|
41
|
+
const current = [
|
|
42
|
+
{
|
|
43
|
+
key: 'a',
|
|
44
|
+
value: 'changed'
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
const original = [
|
|
48
|
+
{
|
|
49
|
+
key: 'a',
|
|
50
|
+
value: '1'
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
expect(hasFormDataChanged(current, original, identity)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('should return true when keys differ', ()=>{
|
|
56
|
+
const current = [
|
|
57
|
+
{
|
|
58
|
+
key: 'b',
|
|
59
|
+
value: '1'
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
const original = [
|
|
63
|
+
{
|
|
64
|
+
key: 'a',
|
|
65
|
+
value: '1'
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
expect(hasFormDataChanged(current, original, identity)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('with normalizeTags (filters empty rows, trims, sorts)', ()=>{
|
|
72
|
+
it('should return false when only difference is empty trailing rows', ()=>{
|
|
73
|
+
const current = [
|
|
74
|
+
{
|
|
75
|
+
key: 'env',
|
|
76
|
+
value: 'prod'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: '',
|
|
80
|
+
value: ''
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
const original = [
|
|
84
|
+
{
|
|
85
|
+
key: 'env',
|
|
86
|
+
value: 'prod'
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('should return false when only difference is whitespace', ()=>{
|
|
92
|
+
const current = [
|
|
93
|
+
{
|
|
94
|
+
key: ' env ',
|
|
95
|
+
value: ' prod '
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
const original = [
|
|
99
|
+
{
|
|
100
|
+
key: 'env',
|
|
101
|
+
value: 'prod'
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
it('should return false when both have empty rows only', ()=>{
|
|
107
|
+
const current = [
|
|
108
|
+
{
|
|
109
|
+
key: '',
|
|
110
|
+
value: ''
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
const original = [
|
|
114
|
+
{
|
|
115
|
+
key: '',
|
|
116
|
+
value: ''
|
|
117
|
+
}
|
|
118
|
+
];
|
|
119
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
it('should return true when a new tag is added', ()=>{
|
|
122
|
+
const current = [
|
|
123
|
+
{
|
|
124
|
+
key: 'env',
|
|
125
|
+
value: 'prod'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: 'team',
|
|
129
|
+
value: 'platform'
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
const original = [
|
|
133
|
+
{
|
|
134
|
+
key: 'env',
|
|
135
|
+
value: 'prod'
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('should return true when a tag is removed (plus empty placeholder)', ()=>{
|
|
141
|
+
const current = [
|
|
142
|
+
{
|
|
143
|
+
key: '',
|
|
144
|
+
value: ''
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
const original = [
|
|
148
|
+
{
|
|
149
|
+
key: 'env',
|
|
150
|
+
value: 'prod'
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('should return false regardless of order', ()=>{
|
|
156
|
+
const current = [
|
|
157
|
+
{
|
|
158
|
+
key: 'team',
|
|
159
|
+
value: 'platform'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: 'env',
|
|
163
|
+
value: 'prod'
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
const original = [
|
|
167
|
+
{
|
|
168
|
+
key: 'env',
|
|
169
|
+
value: 'prod'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: 'team',
|
|
173
|
+
value: 'platform'
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
it('should return false when multiple empty rows exist alongside unchanged data', ()=>{
|
|
179
|
+
const current = [
|
|
180
|
+
{
|
|
181
|
+
key: 'env',
|
|
182
|
+
value: 'prod'
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: '',
|
|
186
|
+
value: ''
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: '',
|
|
190
|
+
value: ''
|
|
191
|
+
}
|
|
192
|
+
];
|
|
193
|
+
const original = [
|
|
194
|
+
{
|
|
195
|
+
key: 'env',
|
|
196
|
+
value: 'prod'
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
expect(hasFormDataChanged(current, original, normalizeTags)).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -19,7 +19,8 @@ interface ObjectDetailsContextValue {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare const ObjectDetailsContext: import("react").Context<ObjectDetailsContextValue | null>;
|
|
21
21
|
export declare function useObjectDetailsContext(): ObjectDetailsContextValue;
|
|
22
|
-
export declare const ObjectDetails: ({ item }: {
|
|
22
|
+
export declare const ObjectDetails: ({ item, multipleSelected }: {
|
|
23
23
|
item: TableItem | null;
|
|
24
|
+
multipleSelected?: boolean;
|
|
24
25
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
25
26
|
export {};
|
|
@@ -44,21 +44,18 @@ const CustomTab = ({ config })=>/*#__PURE__*/ jsx(Fragment, {
|
|
|
44
44
|
children: config.render()
|
|
45
45
|
});
|
|
46
46
|
CustomTab.displayName = 'ObjectDetails.CustomTab';
|
|
47
|
-
const ObjectDetails = ({ item })=>{
|
|
47
|
+
const ObjectDetails = ({ item, multipleSelected })=>{
|
|
48
48
|
const { bucketName } = useParams();
|
|
49
49
|
const { extraObjectTabs } = useDataBrowserUICustomization();
|
|
50
|
+
const placeholderMessages = {
|
|
51
|
+
deleteMarker: 'A "Delete Marker" is selected',
|
|
52
|
+
folder: 'A "Folder" is selected'
|
|
53
|
+
};
|
|
50
54
|
const getPlaceholderMessage = ()=>{
|
|
51
|
-
|
|
52
|
-
children: "Select an object to view details"
|
|
53
|
-
});
|
|
54
|
-
if ('deleteMarker' === item.type) return /*#__PURE__*/ jsx(Text, {
|
|
55
|
-
children: 'A "Delete Marker" is selected'
|
|
56
|
-
});
|
|
57
|
-
if ('folder' === item.type) return /*#__PURE__*/ jsx(Text, {
|
|
58
|
-
children: 'A "Folder" is selected'
|
|
59
|
-
});
|
|
55
|
+
const message = multipleSelected ? 'Multiple items selected' : item?.type && placeholderMessages[item.type] || 'Select an object to view details';
|
|
60
56
|
return /*#__PURE__*/ jsx(Text, {
|
|
61
|
-
|
|
57
|
+
color: "textSecondary",
|
|
58
|
+
children: message
|
|
62
59
|
});
|
|
63
60
|
};
|
|
64
61
|
const defaultTabsMap = useMemo(()=>({
|
|
@@ -66,7 +63,7 @@ const ObjectDetails = ({ item })=>{
|
|
|
66
63
|
id: 'summary',
|
|
67
64
|
label: 'Summary',
|
|
68
65
|
path: '',
|
|
69
|
-
withoutPadding:
|
|
66
|
+
withoutPadding: true,
|
|
70
67
|
query: {
|
|
71
68
|
tab: ''
|
|
72
69
|
},
|
|
@@ -77,7 +74,7 @@ const ObjectDetails = ({ item })=>{
|
|
|
77
74
|
id: 'metadata',
|
|
78
75
|
label: 'Metadata',
|
|
79
76
|
path: '',
|
|
80
|
-
withoutPadding:
|
|
77
|
+
withoutPadding: true,
|
|
81
78
|
query: {
|
|
82
79
|
tab: 'metadata'
|
|
83
80
|
},
|
|
@@ -88,7 +85,7 @@ const ObjectDetails = ({ item })=>{
|
|
|
88
85
|
id: 'tags',
|
|
89
86
|
label: 'Tags',
|
|
90
87
|
path: '',
|
|
91
|
-
withoutPadding:
|
|
88
|
+
withoutPadding: true,
|
|
92
89
|
query: {
|
|
93
90
|
tab: 'tags'
|
|
94
91
|
},
|
|
@@ -129,13 +126,12 @@ const ObjectDetails = ({ item })=>{
|
|
|
129
126
|
objectKey,
|
|
130
127
|
versionId
|
|
131
128
|
]);
|
|
132
|
-
const isPlaceholder = !bucketName || !item || !item.Key || 'deleteMarker' === item.type || 'folder' === item.type;
|
|
129
|
+
const isPlaceholder = multipleSelected || !bucketName || !item || !item.Key || 'deleteMarker' === item.type || 'folder' === item.type;
|
|
133
130
|
const placeholderMessage = isPlaceholder ? getPlaceholderMessage() : null;
|
|
134
131
|
const tabsContent = useMemo(()=>allTabs.map((tab)=>/*#__PURE__*/ jsx(Tabs.Tab, {
|
|
135
132
|
label: tab.label,
|
|
136
133
|
path: tab.path,
|
|
137
134
|
query: tab.query,
|
|
138
|
-
withoutPadding: tab.withoutPadding,
|
|
139
135
|
children: isPlaceholder ? /*#__PURE__*/ jsx(Box, {
|
|
140
136
|
padding: spacing.r16,
|
|
141
137
|
display: "flex",
|
|
@@ -35,8 +35,9 @@ export declare const isObjectLike: (item: TableItem) => item is ObjectItem | Ver
|
|
|
35
35
|
interface ObjectListProps {
|
|
36
36
|
bucketName: string;
|
|
37
37
|
prefix: string;
|
|
38
|
-
onObjectSelect: (object: TableItem) => void;
|
|
38
|
+
onObjectSelect: (object: TableItem | null) => void;
|
|
39
39
|
onPrefixChange: (newPrefix: string) => void;
|
|
40
|
+
onSelectedObjectsChange?: (selectedObjects: TableItem[]) => void;
|
|
40
41
|
}
|
|
41
|
-
export declare const ObjectList: ({ bucketName, prefix, onObjectSelect, onPrefixChange }: ObjectListProps) => import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export declare const ObjectList: ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSelectedObjectsChange, }: ObjectListProps) => import("react/jsx-runtime").JSX.Element;
|
|
42
43
|
export {};
|