@finos/legend-application-studio 28.19.14 → 28.19.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/lib/__lib__/LegendStudioTesting.d.ts +2 -1
  2. package/lib/__lib__/LegendStudioTesting.d.ts.map +1 -1
  3. package/lib/__lib__/LegendStudioTesting.js +1 -0
  4. package/lib/__lib__/LegendStudioTesting.js.map +1 -1
  5. package/lib/application/LegendStudioApplicationConfig.d.ts +13 -0
  6. package/lib/application/LegendStudioApplicationConfig.d.ts.map +1 -1
  7. package/lib/application/LegendStudioApplicationConfig.js +22 -0
  8. package/lib/application/LegendStudioApplicationConfig.js.map +1 -1
  9. package/lib/components/editor/editor-group/dataProduct/DataPoductEditor.d.ts +8 -2
  10. package/lib/components/editor/editor-group/dataProduct/DataPoductEditor.d.ts.map +1 -1
  11. package/lib/components/editor/editor-group/dataProduct/DataPoductEditor.js +379 -253
  12. package/lib/components/editor/editor-group/dataProduct/DataPoductEditor.js.map +1 -1
  13. package/lib/components/editor/side-bar/CreateNewElementModal.d.ts.map +1 -1
  14. package/lib/components/editor/side-bar/CreateNewElementModal.js +1 -2
  15. package/lib/components/editor/side-bar/CreateNewElementModal.js.map +1 -1
  16. package/lib/index.css +2 -2
  17. package/lib/index.css.map +1 -1
  18. package/lib/package.json +1 -1
  19. package/lib/stores/editor/NewElementState.d.ts.map +1 -1
  20. package/lib/stores/editor/NewElementState.js +6 -3
  21. package/lib/stores/editor/NewElementState.js.map +1 -1
  22. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +20 -9
  23. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
  24. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +72 -24
  25. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
  26. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.d.ts +4 -1
  27. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.d.ts.map +1 -1
  28. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.js +10 -1
  29. package/lib/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.js.map +1 -1
  30. package/package.json +9 -9
  31. package/src/__lib__/LegendStudioTesting.ts +1 -0
  32. package/src/application/LegendStudioApplicationConfig.ts +33 -0
  33. package/src/components/editor/editor-group/dataProduct/DataPoductEditor.tsx +971 -745
  34. package/src/components/editor/side-bar/CreateNewElementModal.tsx +0 -15
  35. package/src/stores/editor/NewElementState.ts +8 -1
  36. package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +122 -26
  37. package/src/stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.ts +28 -2
@@ -18,6 +18,8 @@ import { observer } from 'mobx-react-lite';
18
18
  import { useEditorStore } from '../../EditorStoreProvider.js';
19
19
  import {
20
20
  type AccessPointGroupState,
21
+ type AccessPointState,
22
+ DATA_PRODUCT_TAB,
21
23
  DataProductEditorState,
22
24
  generateUrlToDeployOnOpen,
23
25
  LakehouseAccessPointState,
@@ -29,10 +31,6 @@ import {
29
31
  PanelHeader,
30
32
  PanelHeaderActions,
31
33
  Dialog,
32
- PanelDivider,
33
- InputWithInlineValidation,
34
- useResizeDetector,
35
- AccessPointIcon,
36
34
  TimesIcon,
37
35
  PlusIcon,
38
36
  PanelHeaderActionItem,
@@ -52,12 +50,26 @@ import {
52
50
  CaretDownIcon,
53
51
  WarningIcon,
54
52
  PanelFormSection,
53
+ useDragPreviewLayer,
54
+ DragPreviewLayer,
55
+ PanelDnDEntry,
56
+ PanelEntryDragHandle,
57
+ HomeIcon,
58
+ QuestionCircleIcon,
59
+ ErrorWarnIcon,
60
+ GroupWorkIcon,
61
+ CustomSelectorInput,
62
+ Switch,
63
+ BuildingIcon,
64
+ Tooltip,
65
+ InfoCircleIcon,
55
66
  } from '@finos/legend-art';
56
67
  import React, {
57
68
  useRef,
58
69
  useState,
59
70
  useEffect,
60
71
  type ChangeEventHandler,
72
+ useCallback,
61
73
  } from 'react';
62
74
  import { filterByType } from '@finos/legend-shared';
63
75
  import { InlineLambdaEditor } from '@finos/legend-query-builder';
@@ -65,7 +77,13 @@ import { action, flowResult } from 'mobx';
65
77
  import { useAuth } from 'react-oidc-context';
66
78
  import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
67
79
  import { CodeEditor } from '@finos/legend-lego/code-editor';
68
- import { LakehouseTargetEnv, Email } from '@finos/legend-graph';
80
+ import {
81
+ LakehouseTargetEnv,
82
+ Email,
83
+ type DataProduct,
84
+ type LakehouseAccessPoint,
85
+ StereotypeExplicitReference,
86
+ } from '@finos/legend-graph';
69
87
  import {
70
88
  accessPointGroup_setDescription,
71
89
  accessPointGroup_setName,
@@ -78,6 +96,7 @@ import {
78
96
  supportInfo_setSupportUrl,
79
97
  supportInfo_addEmail,
80
98
  supportInfo_deleteEmail,
99
+ accessPoint_setClassification,
81
100
  } from '../../../../stores/graph-modifier/DSL_DataProduct_GraphModifierHelper.js';
82
101
  import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js';
83
102
  import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../__lib__/LegendStudioApplicationNavigationContext.js';
@@ -86,6 +105,11 @@ import {
86
105
  ActionAlertType,
87
106
  useApplicationNavigationContext,
88
107
  } from '@finos/legend-application';
108
+ import { useDrag, useDrop } from 'react-dnd';
109
+ import {
110
+ annotatedElement_addStereotype,
111
+ annotatedElement_deleteStereotype,
112
+ } from '../../../../stores/graph-modifier/DomainGraphModifierHelper.js';
89
113
 
90
114
  export enum AP_GROUP_MODAL_ERRORS {
91
115
  GROUP_NAME_EMPTY = 'Group Name is empty',
@@ -97,332 +121,20 @@ export enum AP_GROUP_MODAL_ERRORS {
97
121
  }
98
122
 
99
123
  export const AP_EMPTY_DESC_WARNING =
100
- 'Describe the data this access point produces';
124
+ 'Click here to describe the data this access point produces';
101
125
 
102
- const NewAccessPointAccessPoint = observer(
103
- (props: { dataProductEditorState: DataProductEditorState }) => {
104
- const { dataProductEditorState: dataProductEditorState } = props;
105
- const accessPointInputRef = useRef<HTMLInputElement>(null);
106
- const [id, setId] = useState<string | undefined>(undefined);
107
- const handleIdChange: React.ChangeEventHandler<HTMLInputElement> = (
108
- event,
109
- ) => setId(event.target.value);
110
- const [description, setDescription] = useState<string | undefined>(
111
- undefined,
112
- );
113
- const handleDescriptionChange: React.ChangeEventHandler<
114
- HTMLInputElement
115
- > = (event) => setDescription(event.target.value);
116
- const handleClose = () => {
117
- dataProductEditorState.setAccessPointModal(false);
118
- };
119
- const handleSubmit = () => {
120
- if (id) {
121
- const accessPointGroup =
122
- dataProductEditorState.editingGroupState ?? 'default';
123
- dataProductEditorState.addAccessPoint(
124
- id,
125
- description,
126
- accessPointGroup,
127
- );
128
- handleClose();
129
- }
130
- };
131
- const handleEnter = (): void => {
132
- accessPointInputRef.current?.focus();
133
- };
134
- const disableCreateButton =
135
- id === '' ||
136
- id === undefined ||
137
- description === '' ||
138
- description === undefined ||
139
- dataProductEditorState.accessPoints.map((e) => e.id).includes(id);
140
- const nameErrors =
141
- id === ''
142
- ? AP_GROUP_MODAL_ERRORS.AP_NAME_EMPTY
143
- : dataProductEditorState.accessPoints
144
- .map((e) => e.id)
145
- .includes(id ?? '')
146
- ? AP_GROUP_MODAL_ERRORS.AP_NAME_EXISTS
147
- : undefined;
148
-
149
- const descriptionErrors =
150
- description === ''
151
- ? AP_GROUP_MODAL_ERRORS.AP_DESCRIPTION_EMPTY
152
- : undefined;
153
- return (
154
- <Dialog
155
- open={true}
156
- onClose={handleClose}
157
- TransitionProps={{
158
- onEnter: handleEnter,
159
- }}
160
- classes={{
161
- container: 'search-modal__container',
162
- }}
163
- PaperProps={{
164
- classes: {
165
- root: 'search-modal__inner-container',
166
- },
167
- }}
168
- >
169
- <form
170
- onSubmit={(event) => {
171
- event.preventDefault();
172
- handleSubmit();
173
- }}
174
- className={clsx('modal search-modal', {
175
- 'modal--dark': true,
176
- })}
177
- style={{
178
- display: 'flex',
179
- flexDirection: 'column',
180
- gap: '1rem',
181
- }}
182
- >
183
- <div className="modal__title">New Access Point</div>
184
- <div>
185
- <div className="panel__content__form__section__header__label">
186
- Name
187
- </div>
188
- <InputWithInlineValidation
189
- className={clsx('input new-access-point-modal__id-input', {
190
- 'input--dark': true,
191
- })}
192
- ref={accessPointInputRef}
193
- spellCheck={false}
194
- value={id ?? ''}
195
- onChange={handleIdChange}
196
- placeholder="Access Point Name"
197
- error={nameErrors}
198
- />
199
- </div>
200
- <div>
201
- <div className="panel__content__form__section__header__label">
202
- Description
203
- </div>
204
- <InputWithInlineValidation
205
- className={clsx('input new-access-point-modal__id-input', {
206
- 'input--dark': true,
207
- })}
208
- spellCheck={false}
209
- value={description ?? ''}
210
- onChange={handleDescriptionChange}
211
- placeholder="Access Point Description"
212
- error={descriptionErrors}
213
- />
214
- </div>
215
- <div></div>
216
- <PanelDivider />
217
- <div className="search-modal__actions">
218
- <button
219
- className={clsx('btn btn--primary', {
220
- 'btn--dark': true,
221
- })}
222
- disabled={disableCreateButton}
223
- >
224
- Create
225
- </button>
226
- </div>
227
- </form>
228
- </Dialog>
229
- );
230
- },
231
- );
126
+ const AP_DND_TYPE = 'ACCESS_POINT';
127
+ const AP_GROUP_DND_TYPE = 'ACCESS_POINT_GROUP';
232
128
 
233
- const NewAccessPointGroupModal = observer(
234
- (props: { dataProductEditorState: DataProductEditorState }) => {
235
- const { dataProductEditorState: dataProductEditorState } = props;
236
- const accessPointGroupInputRef = useRef<HTMLInputElement>(null);
237
- const [groupName, setGroupName] = useState<string | undefined>(undefined);
238
- const handleGroupNameChange: React.ChangeEventHandler<HTMLInputElement> = (
239
- event,
240
- ) => setGroupName(event.target.value);
241
- const [groupDescription, setGroupDescription] = useState<
242
- string | undefined
243
- >(undefined);
244
- const handleGroupDescriptionChange: React.ChangeEventHandler<
245
- HTMLInputElement
246
- > = (event) => setGroupDescription(event.target.value);
247
- const [apName, setApName] = useState<string | undefined>(undefined);
248
- const handleApNameChange: React.ChangeEventHandler<HTMLInputElement> = (
249
- event,
250
- ) => setApName(event.target.value);
251
- const [apDescription, setApDescription] = useState<string | undefined>(
252
- undefined,
253
- );
254
- const handleApDescriptionChange: React.ChangeEventHandler<
255
- HTMLInputElement
256
- > = (event) => setApDescription(event.target.value);
257
- const handleClose = () => {
258
- dataProductEditorState.setAccessPointGroupModal(false);
259
- };
260
- const handleEnter = (): void => {
261
- accessPointGroupInputRef.current?.focus();
262
- };
129
+ export type AccessPointDragSource = {
130
+ accessPointState: AccessPointState;
131
+ };
263
132
 
264
- const groupNameErrors =
265
- groupName === ''
266
- ? AP_GROUP_MODAL_ERRORS.GROUP_NAME_EMPTY
267
- : dataProductEditorState.accessPointGroupStates
268
- .map((e) => e.value.id)
269
- .includes(groupName ?? '')
270
- ? AP_GROUP_MODAL_ERRORS.GROUP_NAME_EXISTS
271
- : undefined;
272
- const groupDescriptionErrors =
273
- groupDescription === ''
274
- ? AP_GROUP_MODAL_ERRORS.GROUP_DESCRIPTION_EMPTY
275
- : undefined;
276
- const apNameErrors =
277
- apName === ''
278
- ? AP_GROUP_MODAL_ERRORS.AP_NAME_EMPTY
279
- : dataProductEditorState.accessPoints
280
- .map((e) => e.id)
281
- .includes(apName ?? '')
282
- ? AP_GROUP_MODAL_ERRORS.AP_NAME_EXISTS
283
- : undefined;
284
- const apDescriptionErrors =
285
- apDescription === ''
286
- ? AP_GROUP_MODAL_ERRORS.AP_DESCRIPTION_EMPTY
287
- : undefined;
288
-
289
- const disableCreateButton =
290
- !groupName ||
291
- !groupDescription ||
292
- !apName ||
293
- !apDescription ||
294
- Boolean(
295
- groupNameErrors ??
296
- groupDescriptionErrors ??
297
- apNameErrors ??
298
- apDescriptionErrors,
299
- );
300
- const handleSubmit = () => {
301
- if (!disableCreateButton && apName) {
302
- const createdGroup = dataProductEditorState.createGroupAndAdd(
303
- groupName,
304
- groupDescription,
305
- );
306
- dataProductEditorState.addAccessPoint(
307
- apName,
308
- apDescription,
309
- createdGroup,
310
- );
311
- handleClose();
312
- }
313
- };
133
+ export type APGDragSource = {
134
+ groupState: AccessPointGroupState;
135
+ };
314
136
 
315
- return (
316
- <Dialog
317
- open={true}
318
- onClose={handleClose}
319
- TransitionProps={{
320
- onEnter: handleEnter,
321
- }}
322
- classes={{
323
- container: 'search-modal__container',
324
- }}
325
- PaperProps={{
326
- classes: {
327
- root: 'search-modal__inner-container',
328
- },
329
- }}
330
- >
331
- <form
332
- onSubmit={(event) => {
333
- event.preventDefault();
334
- handleSubmit();
335
- }}
336
- className={clsx('modal search-modal', {
337
- 'modal--dark': true,
338
- })}
339
- style={{
340
- display: 'flex',
341
- flexDirection: 'column',
342
- gap: '1rem',
343
- }}
344
- >
345
- <div className="modal__title">New Access Point Group</div>
346
- <div>
347
- <div className="panel__content__form__section__header__label">
348
- Group Name
349
- </div>
350
- <InputWithInlineValidation
351
- className={clsx('input new-access-point-modal__id-input', {
352
- 'input--dark': true,
353
- })}
354
- ref={accessPointGroupInputRef}
355
- spellCheck={false}
356
- value={groupName ?? ''}
357
- onChange={handleGroupNameChange}
358
- placeholder="Access Point Group Name"
359
- error={groupNameErrors}
360
- />
361
- </div>
362
- <div>
363
- <div className="panel__content__form__section__header__label">
364
- Group Description
365
- </div>
366
- <InputWithInlineValidation
367
- className={clsx('input new-access-point-modal__id-input', {
368
- 'input--dark': true,
369
- })}
370
- spellCheck={false}
371
- value={groupDescription ?? ''}
372
- onChange={handleGroupDescriptionChange}
373
- placeholder="Access Point Group Description"
374
- error={groupDescriptionErrors}
375
- />
376
- </div>
377
- <div>
378
- <div className="panel__content__form__section__header__label">
379
- Access Point
380
- </div>
381
- <div className="new-access-point-group-modal">
382
- <div className="panel__content__form__section__header__label">
383
- Name
384
- </div>
385
- <InputWithInlineValidation
386
- className={clsx('input new-access-point-modal__id-input', {
387
- 'input--dark': true,
388
- })}
389
- spellCheck={false}
390
- value={apName ?? ''}
391
- onChange={handleApNameChange}
392
- placeholder="Access Point Name"
393
- error={apNameErrors}
394
- />
395
- <div className="panel__content__form__section__header__label">
396
- Description
397
- </div>
398
- <InputWithInlineValidation
399
- className={clsx('input new-access-point-modal__id-input', {
400
- 'input--dark': true,
401
- })}
402
- spellCheck={false}
403
- value={apDescription ?? ''}
404
- onChange={handleApDescriptionChange}
405
- placeholder="Access Point Description"
406
- error={apDescriptionErrors}
407
- />
408
- </div>
409
- </div>
410
- <PanelDivider />
411
- <div className="search-modal__actions">
412
- <button
413
- className={clsx('btn btn--primary', {
414
- 'btn--dark': true,
415
- })}
416
- disabled={disableCreateButton}
417
- >
418
- Create
419
- </button>
420
- </div>
421
- </form>
422
- </Dialog>
423
- );
424
- },
425
- );
137
+ const newNamePlaceholder = '';
426
138
 
427
139
  interface HoverTextAreaProps {
428
140
  text: string;
@@ -456,204 +168,396 @@ const hoverIcon = () => {
456
168
  );
457
169
  };
458
170
 
171
+ const AccessPointTitle = observer(
172
+ (props: { accessPoint: LakehouseAccessPoint }) => {
173
+ const { accessPoint } = props;
174
+ const [editingName, setEditingName] = useState(
175
+ accessPoint.id === newNamePlaceholder,
176
+ );
177
+ const handleNameEdit = () => setEditingName(true);
178
+ const handleNameBlur = () => {
179
+ if (accessPoint.id !== newNamePlaceholder) {
180
+ setEditingName(false);
181
+ }
182
+ };
183
+ const updateAccessPointName: React.ChangeEventHandler<HTMLTextAreaElement> =
184
+ action((event) => {
185
+ if (!event.target.value.includes(' ')) {
186
+ accessPoint.id = event.target.value;
187
+ }
188
+ });
189
+
190
+ return editingName ? (
191
+ <textarea
192
+ className="access-point-editor__name"
193
+ spellCheck={false}
194
+ value={accessPoint.id}
195
+ onChange={updateAccessPointName}
196
+ placeholder={'Access Point Name'}
197
+ onBlur={handleNameBlur}
198
+ style={{
199
+ borderColor:
200
+ accessPoint.id === newNamePlaceholder
201
+ ? 'var(--color-red-300)'
202
+ : 'transparent',
203
+ }}
204
+ />
205
+ ) : (
206
+ <div onClick={handleNameEdit} title="Click to edit access point title">
207
+ <div className="access-point-editor__name__label">{accessPoint.id}</div>
208
+ </div>
209
+ );
210
+ },
211
+ );
212
+
213
+ const AccessPointClassification = observer(
214
+ (props: {
215
+ accessPoint: LakehouseAccessPoint;
216
+ groupState: AccessPointGroupState;
217
+ }) => {
218
+ const { accessPoint, groupState } = props;
219
+ const applicationStore = useEditorStore().applicationStore;
220
+ const CHOOSE_CLASSIFICATION = 'Choose Classification';
221
+ const updateAccessPointClassificationTextbox: React.ChangeEventHandler<HTMLTextAreaElement> =
222
+ action((event) => {
223
+ accessPoint.classification = event.target.value;
224
+ });
225
+
226
+ const conditionalClassifications = (): string[] => {
227
+ if (groupState.containsPublicStereotype) {
228
+ return (
229
+ applicationStore.config.options.dataProductConfig
230
+ ?.publicClassifications ?? []
231
+ );
232
+ } else {
233
+ return (
234
+ applicationStore.config.options.dataProductConfig?.classifications ??
235
+ []
236
+ );
237
+ }
238
+ };
239
+
240
+ const classificationOptions = [CHOOSE_CLASSIFICATION]
241
+ .concat(conditionalClassifications())
242
+ .map((classfication) => ({
243
+ label: classfication,
244
+ value: classfication,
245
+ }));
246
+
247
+ const updateAccessPointClassificationFromDropdown = action(
248
+ (val: { label: string; value: string } | null): void => {
249
+ accessPoint_setClassification(
250
+ accessPoint,
251
+ val?.value === CHOOSE_CLASSIFICATION ? undefined : val?.value,
252
+ );
253
+ },
254
+ );
255
+
256
+ const currentClassification =
257
+ accessPoint.classification !== undefined
258
+ ? {
259
+ label: accessPoint.classification,
260
+ value: accessPoint.classification,
261
+ }
262
+ : {
263
+ label: CHOOSE_CLASSIFICATION,
264
+ value: CHOOSE_CLASSIFICATION,
265
+ };
266
+
267
+ const classificationDocumentationLink = (): void => {
268
+ const docLink =
269
+ applicationStore.config.options.dataProductConfig?.classificationDoc;
270
+ if (docLink) {
271
+ applicationStore.navigationService.navigator.visitAddress(docLink);
272
+ }
273
+ };
274
+ return (
275
+ <div className="access-point-editor__classification">
276
+ {classificationOptions.length > 1 ? (
277
+ <div>
278
+ <CustomSelectorInput
279
+ className="explorer__new-element-modal__driver__dropdown"
280
+ options={classificationOptions}
281
+ onChange={updateAccessPointClassificationFromDropdown}
282
+ value={currentClassification}
283
+ darkMode={
284
+ !applicationStore.layoutService
285
+ .TEMPORARY__isLightColorThemeEnabled
286
+ }
287
+ />
288
+ </div>
289
+ ) : (
290
+ <textarea
291
+ className="panel__content__form__section__input"
292
+ spellCheck={false}
293
+ value={accessPoint.classification ?? ''}
294
+ onChange={updateAccessPointClassificationTextbox}
295
+ placeholder="Add classification"
296
+ style={{
297
+ overflow: 'hidden',
298
+ width: '125px',
299
+ resize: 'none',
300
+ padding: '0.25rem',
301
+ }}
302
+ />
303
+ )}
304
+ <Tooltip
305
+ title="Learn more about data classification scheme here."
306
+ arrow={true}
307
+ placement={'top'}
308
+ >
309
+ <button onClick={classificationDocumentationLink}>
310
+ <InfoCircleIcon />
311
+ </button>
312
+ </Tooltip>
313
+ </div>
314
+ );
315
+ },
316
+ );
317
+
459
318
  export const LakehouseDataProductAcccessPointEditor = observer(
460
319
  (props: {
461
320
  accessPointState: LakehouseAccessPointState;
462
321
  isReadOnly: boolean;
463
322
  }) => {
464
323
  const { accessPointState } = props;
324
+ const editorStore = useEditorStore();
465
325
  const accessPoint = accessPointState.accessPoint;
466
- const productEditorState = accessPointState.state;
326
+ const groupState = accessPointState.state;
467
327
  const lambdaEditorState = accessPointState.lambdaState;
468
- const propertyHasParserError = productEditorState.accessPointStates
328
+ const propertyHasParserError = groupState.accessPointStates
469
329
  .filter(filterByType(LakehouseAccessPointState))
470
330
  .find((pm) => pm.lambdaState.parserError);
471
331
  const [editingDescription, setEditingDescription] = useState(false);
472
332
  const [isHovering, setIsHovering] = useState(false);
333
+ const ref = useRef<HTMLDivElement>(null);
473
334
 
474
- const handleEdit = () => setEditingDescription(true);
475
- const handleBlur = () => {
335
+ const handleDescriptionEdit = () => setEditingDescription(true);
336
+ const handleDescriptionBlur = () => {
476
337
  setEditingDescription(false);
477
338
  setIsHovering(false);
478
339
  };
479
-
480
340
  const handleMouseOver: React.MouseEventHandler<HTMLDivElement> = () => {
481
341
  setIsHovering(true);
482
342
  };
483
343
  const handleMouseOut: React.MouseEventHandler<HTMLDivElement> = () => {
484
344
  setIsHovering(false);
485
345
  };
486
-
487
346
  const updateAccessPointDescription: React.ChangeEventHandler<HTMLTextAreaElement> =
488
347
  action((event) => {
489
348
  accessPoint.description = event.target.value;
490
349
  });
491
-
492
350
  const updateAccessPointTargetEnvironment = action(
493
351
  (targetEnvironment: LakehouseTargetEnv) => {
494
352
  accessPoint.targetEnvironment = targetEnvironment;
495
353
  },
496
354
  );
497
355
 
356
+ const handleRemoveAccessPoint = (): void => {
357
+ editorStore.applicationStore.alertService.setActionAlertInfo({
358
+ message: `Are you sure you want to delete Access Point ${accessPoint.id}?`,
359
+ type: ActionAlertType.CAUTION,
360
+ actions: [
361
+ {
362
+ label: 'Confirm',
363
+ type: ActionAlertActionType.PROCEED_WITH_CAUTION,
364
+ handler: (): void => {
365
+ groupState.deleteAccessPoint(accessPointState);
366
+ },
367
+ },
368
+ {
369
+ label: 'Cancel',
370
+ type: ActionAlertActionType.PROCEED,
371
+ default: true,
372
+ },
373
+ ],
374
+ });
375
+ };
376
+
377
+ //Drag and drop - reorder access points/move between groups
378
+ const handleHover = useCallback(
379
+ (item: AccessPointDragSource): void => {
380
+ const draggingProperty = item.accessPointState;
381
+ const hoveredProperty = accessPointState;
382
+ groupState.swapAccessPoints(draggingProperty, hoveredProperty);
383
+ },
384
+ [accessPointState, groupState],
385
+ );
386
+
387
+ const [{ isBeingDraggedAP }, dropConnector] = useDrop<
388
+ AccessPointDragSource,
389
+ void,
390
+ { isBeingDraggedAP: AccessPointState | undefined }
391
+ >(
392
+ () => ({
393
+ accept: [AP_DND_TYPE],
394
+ hover: (item) => handleHover(item),
395
+ collect: (monitor) => ({
396
+ isBeingDraggedAP: monitor.getItem<AccessPointDragSource | null>()
397
+ ?.accessPointState,
398
+ }),
399
+ }),
400
+ [handleHover],
401
+ );
402
+ const isBeingDragged = accessPoint === isBeingDraggedAP?.accessPoint;
403
+
404
+ const [, dragConnector, dragPreviewConnector] =
405
+ useDrag<AccessPointDragSource>(
406
+ () => ({
407
+ type: AP_DND_TYPE,
408
+ item: () => ({
409
+ accessPointState: accessPointState,
410
+ }),
411
+ collect: (monitor) => ({
412
+ isDragging: monitor.isDragging(),
413
+ }),
414
+ }),
415
+ [accessPointState],
416
+ );
417
+ dragConnector(ref);
418
+ dropConnector(ref);
419
+ useDragPreviewLayer(dragPreviewConnector);
420
+
498
421
  return (
499
- <div
500
- className={clsx('access-point-editor', {
501
- backdrop__element: propertyHasParserError,
502
- })}
422
+ <PanelDnDEntry
423
+ ref={ref}
424
+ placeholder={<div className="dnd__placeholder--light"></div>}
425
+ showPlaceholder={isBeingDragged}
503
426
  >
504
- <div className="access-point-editor__metadata">
505
- <div className={clsx('access-point-editor__name', {})}>
506
- <div className="access-point-editor__name__label">
507
- {accessPoint.id}
508
- </div>
509
- </div>
510
- {editingDescription ? (
511
- <textarea
512
- className="panel__content__form__section__input"
513
- spellCheck={false}
514
- value={accessPoint.description ?? ''}
515
- onChange={updateAccessPointDescription}
516
- placeholder="Access Point description"
517
- onBlur={handleBlur}
518
- style={{
519
- overflow: 'hidden',
520
- resize: 'none',
521
- padding: '0.25rem',
522
- }}
523
- />
524
- ) : (
525
- <div
526
- onClick={handleEdit}
527
- title="Click to edit access point description"
528
- className="access-point-editor__description-container"
529
- >
530
- {accessPoint.description ? (
531
- <HoverTextArea
532
- text={accessPoint.description}
533
- handleMouseOver={handleMouseOver}
534
- handleMouseOut={handleMouseOut}
427
+ <div
428
+ className={clsx('access-point-editor', {
429
+ backdrop__element: propertyHasParserError,
430
+ })}
431
+ >
432
+ <PanelEntryDragHandle
433
+ dragSourceConnector={ref}
434
+ isDragging={isBeingDragged}
435
+ title={'Drag this Access Point to another group'}
436
+ className="access-point-editor__dnd-handle"
437
+ />
438
+ <div style={{ flex: 1 }}>
439
+ <div className="access-point-editor__metadata">
440
+ <AccessPointTitle accessPoint={accessPoint} />
441
+ {editingDescription ? (
442
+ <textarea
443
+ className="panel__content__form__section__input"
444
+ spellCheck={false}
445
+ value={accessPoint.description ?? ''}
446
+ onChange={updateAccessPointDescription}
447
+ placeholder="Access Point description"
448
+ onBlur={handleDescriptionBlur}
449
+ style={{
450
+ overflow: 'hidden',
451
+ resize: 'none',
452
+ padding: '0.25rem',
453
+ }}
535
454
  />
536
455
  ) : (
537
456
  <div
538
- className="access-point-editor__group-container__description--warning"
539
- onMouseOver={handleMouseOver}
540
- onMouseOut={handleMouseOut}
457
+ onClick={handleDescriptionEdit}
458
+ title="Click to edit access point description"
459
+ className="access-point-editor__description-container"
541
460
  >
542
- <WarningIcon />
543
- {AP_EMPTY_DESC_WARNING}
461
+ {accessPoint.description ? (
462
+ <HoverTextArea
463
+ text={accessPoint.description}
464
+ handleMouseOver={handleMouseOver}
465
+ handleMouseOut={handleMouseOut}
466
+ />
467
+ ) : (
468
+ <div
469
+ className="access-point-editor__group-container__description--warning"
470
+ onMouseOver={handleMouseOver}
471
+ onMouseOut={handleMouseOut}
472
+ >
473
+ <WarningIcon />
474
+ {AP_EMPTY_DESC_WARNING}
475
+ </div>
476
+ )}
477
+ {isHovering && hoverIcon()}
544
478
  </div>
545
479
  )}
480
+ <div className="access-point-editor__info">
481
+ {editorStore.applicationStore.config.options
482
+ .dataProductConfig && (
483
+ <AccessPointClassification
484
+ accessPoint={accessPoint}
485
+ groupState={groupState}
486
+ />
487
+ )}
546
488
 
547
- {isHovering && hoverIcon()}
548
- </div>
549
- )}
550
- <div className="access-point-editor__info">
551
- <div
552
- className={clsx('access-point-editor__type')}
553
- title={'Change target environment'}
554
- >
555
- <div className="access-point-editor__type__label">
556
- {accessPoint.targetEnvironment}
489
+ <div
490
+ className={clsx('access-point-editor__type')}
491
+ title={'Change target environment'}
492
+ >
493
+ <div className="access-point-editor__type__label">
494
+ {accessPoint.targetEnvironment}
495
+ </div>
496
+ <ControlledDropdownMenu
497
+ className="access-point-editor__dropdown"
498
+ content={
499
+ <MenuContent>
500
+ {Object.values(LakehouseTargetEnv).map(
501
+ (environment) => (
502
+ <MenuContentItem
503
+ key={environment}
504
+ className="btn__dropdown-combo__option"
505
+ onClick={() =>
506
+ updateAccessPointTargetEnvironment(environment)
507
+ }
508
+ >
509
+ {environment}
510
+ </MenuContentItem>
511
+ ),
512
+ )}
513
+ </MenuContent>
514
+ }
515
+ menuProps={{
516
+ anchorOrigin: {
517
+ vertical: 'bottom',
518
+ horizontal: 'right',
519
+ },
520
+ transformOrigin: {
521
+ vertical: 'top',
522
+ horizontal: 'right',
523
+ },
524
+ }}
525
+ >
526
+ <CaretDownIcon />
527
+ </ControlledDropdownMenu>
528
+ </div>
557
529
  </div>
558
- <div
559
- style={{
560
- background: 'transparent',
561
- height: '100%',
562
- alignItems: 'center',
563
- display: 'flex',
564
- }}
565
- >
566
- <ControlledDropdownMenu
567
- className="access-point-editor__dropdown"
568
- content={
569
- <MenuContent>
570
- {Object.values(LakehouseTargetEnv).map((environment) => (
571
- <MenuContentItem
572
- key={environment}
573
- className="btn__dropdown-combo__option"
574
- onClick={() =>
575
- updateAccessPointTargetEnvironment(environment)
576
- }
577
- >
578
- {environment}
579
- </MenuContentItem>
580
- ))}
581
- </MenuContent>
582
- }
583
- menuProps={{
584
- anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
585
- transformOrigin: { vertical: 'top', horizontal: 'right' },
530
+ </div>
531
+ <div className="access-point-editor__content">
532
+ <div className="access-point-editor__generic-entry">
533
+ <div className="access-point-editor__entry__container">
534
+ <div className="access-point-editor__entry">
535
+ <InlineLambdaEditor
536
+ className={'access-point-editor__lambda-editor'}
537
+ disabled={
538
+ lambdaEditorState.val.state.state
539
+ .isConvertingTransformLambdaObjects
540
+ }
541
+ lambdaEditorState={lambdaEditorState}
542
+ forceBackdrop={Boolean(lambdaEditorState.parserError)}
543
+ />
544
+ </div>
545
+ </div>
546
+ <button
547
+ className="access-point-editor__generic-entry__remove-btn"
548
+ onClick={() => {
549
+ handleRemoveAccessPoint();
586
550
  }}
551
+ tabIndex={-1}
552
+ title="Remove"
587
553
  >
588
- <CaretDownIcon />
589
- </ControlledDropdownMenu>
554
+ <TimesIcon />
555
+ </button>
590
556
  </div>
591
557
  </div>
592
558
  </div>
593
559
  </div>
594
- <div className="access-point-editor__content">
595
- <div className="access-point-editor__generic-entry">
596
- <div className="access-point-editor__entry__container">
597
- <div className="access-point-editor__entry">
598
- <InlineLambdaEditor
599
- className={'access-point-editor__lambda-editor'}
600
- disabled={
601
- lambdaEditorState.val.state.state
602
- .isConvertingTransformLambdaObjects
603
- }
604
- lambdaEditorState={lambdaEditorState}
605
- forceBackdrop={Boolean(lambdaEditorState.parserError)}
606
- />
607
- </div>
608
- </div>
609
- <button
610
- className="access-point-editor__generic-entry__remove-btn"
611
- onClick={() => {
612
- productEditorState.deleteAccessPoint(accessPointState);
613
- }}
614
- tabIndex={-1}
615
- title="Remove"
616
- >
617
- <TimesIcon />
618
- </button>
619
- </div>
620
- </div>
621
- </div>
622
- );
623
- },
624
- );
625
-
626
- const DataProductEditorSplashScreen = observer(
627
- (props: { dataProductEditorState: DataProductEditorState }) => {
628
- const { dataProductEditorState } = props;
629
- const logoWidth = 280;
630
- const logoHeight = 270;
631
- const [showLogo, setShowLogo] = useState(false);
632
- const { ref, height, width } = useResizeDetector<HTMLDivElement>();
633
-
634
- useEffect(() => {
635
- setShowLogo((width ?? 0) > logoWidth && (height ?? 0) > logoHeight);
636
- }, [height, width]);
637
-
638
- return (
639
- <div ref={ref} className="data-product-editor__splash-screen">
640
- <div
641
- onClick={() => dataProductEditorState.setAccessPointGroupModal(true)}
642
- className="data-product-editor__splash-screen__label"
643
- >
644
- Add Access Point Group
645
- </div>
646
- <div className="data-product-editor__splash-screen__spacing"></div>
647
- <div
648
- onClick={() => dataProductEditorState.setAccessPointGroupModal(true)}
649
- title="Add new Access Point Group"
650
- className={clsx('data-product-editor__splash-screen__logo', {
651
- 'data-product-editor__splash-screen__logo--hidden': !showLogo,
652
- })}
653
- >
654
- <AccessPointIcon />
655
- </div>
656
- </div>
560
+ </PanelDnDEntry>
657
561
  );
658
562
  },
659
563
  );
@@ -708,14 +612,54 @@ const DataProductDeploymentResponseModal = observer(
708
612
  },
709
613
  );
710
614
 
711
- const AccessPointGroupSection = observer(
615
+ const AccessPointGroupPublicToggle = observer(
616
+ (props: { groupState: AccessPointGroupState }) => {
617
+ const { groupState } = props;
618
+
619
+ const handleSwitchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
620
+ const isChecked = event.target.checked;
621
+ if (isChecked && groupState.publicStereotype) {
622
+ annotatedElement_addStereotype(
623
+ groupState.value,
624
+ StereotypeExplicitReference.create(groupState.publicStereotype),
625
+ );
626
+ } else if (groupState.containsPublicStereotype) {
627
+ annotatedElement_deleteStereotype(
628
+ groupState.value,
629
+ groupState.containsPublicStereotype,
630
+ );
631
+ }
632
+ };
633
+ return (
634
+ <div className="access-point-editor__toggle">
635
+ <Switch
636
+ checked={Boolean(groupState.containsPublicStereotype)}
637
+ onChange={handleSwitchChange}
638
+ sx={{
639
+ '& .MuiSwitch-track': {
640
+ backgroundColor: groupState.containsPublicStereotype
641
+ ? 'default'
642
+ : 'var(--color-light-grey-400)',
643
+ },
644
+ }}
645
+ />
646
+ <BuildingIcon />
647
+ Enterprise Data. Anyone at the firm can access this without approvals.
648
+ </div>
649
+ );
650
+ },
651
+ );
652
+
653
+ const AccessPointGroupEditor = observer(
712
654
  (props: { groupState: AccessPointGroupState; isReadOnly: boolean }) => {
713
655
  const { groupState, isReadOnly } = props;
714
656
  const editorStore = useEditorStore();
715
657
  const productEditorState = groupState.state;
716
658
  const [editingDescription, setEditingDescription] = useState(false);
717
659
  const [isHoveringDescription, setIsHoveringDescription] = useState(false);
718
- const [editingName, setEditingName] = useState(false);
660
+ const [editingName, setEditingName] = useState(
661
+ groupState.value.id === newNamePlaceholder,
662
+ );
719
663
  const [isHoveringName, setIsHoveringName] = useState(false);
720
664
 
721
665
  const handleDescriptionEdit = () => setEditingDescription(true);
@@ -739,8 +683,10 @@ const AccessPointGroupSection = observer(
739
683
 
740
684
  const handleNameEdit = () => setEditingName(true);
741
685
  const handleNameBlur = () => {
742
- setEditingName(false);
743
- setIsHoveringName(false);
686
+ if (groupState.value.id !== newNamePlaceholder) {
687
+ setEditingName(false);
688
+ setIsHoveringName(false);
689
+ }
744
690
  };
745
691
  const handleMouseOverName: React.MouseEventHandler<HTMLDivElement> = () => {
746
692
  setIsHoveringName(true);
@@ -749,38 +695,45 @@ const AccessPointGroupSection = observer(
749
695
  setIsHoveringName(false);
750
696
  };
751
697
  const updateGroupName = (val: string): void => {
752
- if (val) {
698
+ if (val && !val.includes(' ')) {
753
699
  accessPointGroup_setName(groupState.value, val);
754
700
  }
755
701
  };
756
702
 
757
703
  const handleRemoveAccessPointGroup = (): void => {
758
704
  editorStore.applicationStore.alertService.setActionAlertInfo({
759
- message: `Deleting access point group ${groupState.value.id} will permanently remove it and all associated access points. Are you sure you want to proceed?`,
705
+ message: `Are you sure you want to delete Access Point Group ${groupState.value.id} and all of its Access Points?`,
760
706
  type: ActionAlertType.CAUTION,
761
707
  actions: [
762
708
  {
763
- label: 'Cancel',
764
- type: ActionAlertActionType.PROCEED,
765
- default: true,
766
- },
767
- {
768
- label: 'Proceed',
709
+ label: 'Confirm',
769
710
  type: ActionAlertActionType.PROCEED_WITH_CAUTION,
770
711
  handler: (): void => {
771
712
  productEditorState.deleteAccessPointGroup(groupState);
772
713
  },
773
714
  },
715
+ {
716
+ label: 'Cancel',
717
+ type: ActionAlertActionType.PROCEED,
718
+ default: true,
719
+ },
774
720
  ],
775
721
  });
776
722
  };
777
723
 
778
- const openNewModal = () => {
779
- productEditorState.setEditingGroupState(groupState);
780
- productEditorState.setAccessPointModal(true);
724
+ const handleAddAccessPoint = () => {
725
+ productEditorState.addAccessPoint(
726
+ newNamePlaceholder,
727
+ undefined,
728
+ productEditorState.selectedGroupState ?? groupState,
729
+ );
781
730
  };
731
+
782
732
  return (
783
- <div className="access-point-editor__group-container">
733
+ <div
734
+ className="access-point-editor__group-container"
735
+ data-testid={LEGEND_STUDIO_TEST_ID.ACCESS_POINT_GROUP_EDITOR}
736
+ >
784
737
  <div className="access-point-editor__group-container__title-editor">
785
738
  {editingName ? (
786
739
  <textarea
@@ -794,6 +747,11 @@ const AccessPointGroupSection = observer(
794
747
  overflow: 'hidden',
795
748
  resize: 'none',
796
749
  padding: '0.25rem',
750
+ borderColor:
751
+ groupState.value.id === newNamePlaceholder
752
+ ? 'var(--color-red-300)'
753
+ : 'transparent',
754
+ borderWidth: 'thin',
797
755
  }}
798
756
  />
799
757
  ) : (
@@ -815,7 +773,6 @@ const AccessPointGroupSection = observer(
815
773
  <button
816
774
  className="access-point-editor__generic-entry__remove-btn--group"
817
775
  onClick={() => {
818
- // productEditorState.deleteAccessPointGroup(groupState);
819
776
  handleRemoveAccessPointGroup();
820
777
  }}
821
778
  tabIndex={-1}
@@ -859,21 +816,23 @@ const AccessPointGroupSection = observer(
859
816
  onMouseOut={handleMouseOutDescription}
860
817
  >
861
818
  <WarningIcon />
862
- Describe this access point group to clarify what users are
863
- requesting access to. Entitlements are provisioned at the
864
- group level.
819
+ Users request access at the access point group level. Click
820
+ here to add a meaningful description to guide users.
865
821
  </div>
866
822
  )}
867
823
  {isHoveringDescription && hoverIcon()}
868
824
  </div>
869
825
  )}
870
826
  </div>
827
+ {editorStore.applicationStore.config.options.dataProductConfig && (
828
+ <AccessPointGroupPublicToggle groupState={groupState} />
829
+ )}
871
830
  <PanelHeader className="panel__header--access-point">
872
831
  <div className="panel__header__title">Access Points</div>
873
832
  <PanelHeaderActions>
874
833
  <PanelHeaderActionItem
875
834
  className="panel__header__action"
876
- onClick={openNewModal}
835
+ onClick={handleAddAccessPoint}
877
836
  disabled={isReadOnly}
878
837
  title="Create new access point"
879
838
  >
@@ -881,38 +840,513 @@ const AccessPointGroupSection = observer(
881
840
  </PanelHeaderActionItem>
882
841
  </PanelHeaderActions>
883
842
  </PanelHeader>
884
- {groupState.accessPointStates
885
- .filter(filterByType(LakehouseAccessPointState))
886
- .map((apState) => (
887
- <LakehouseDataProductAcccessPointEditor
888
- key={apState.accessPoint.id}
843
+ {groupState.accessPointStates.length === 0 && (
844
+ <div className="access-point-editor__group-container__description--warning">
845
+ <WarningIcon />
846
+ This group needs at least one access point defined.
847
+ </div>
848
+ )}
849
+ <div style={{ gap: '1rem', display: 'flex', flexDirection: 'column' }}>
850
+ {groupState.accessPointStates
851
+ .filter(filterByType(LakehouseAccessPointState))
852
+ .map((apState) => (
853
+ <LakehouseDataProductAcccessPointEditor
854
+ key={apState.uuid}
855
+ isReadOnly={isReadOnly}
856
+ accessPointState={apState}
857
+ />
858
+ ))}
859
+ </div>
860
+ </div>
861
+ );
862
+ },
863
+ );
864
+
865
+ const GroupTabRenderer = observer(
866
+ (props: {
867
+ group: AccessPointGroupState;
868
+ dataProductEditorState: DataProductEditorState;
869
+ }) => {
870
+ const { group, dataProductEditorState } = props;
871
+ const changeGroup = (newGroup: AccessPointGroupState): void => {
872
+ dataProductEditorState.setSelectedGroupState(newGroup);
873
+ };
874
+ const selectedGroupState = dataProductEditorState.selectedGroupState;
875
+ const ref = useRef<HTMLDivElement>(null);
876
+
877
+ const groupError = (): boolean => {
878
+ return (
879
+ group.accessPointStates.length === 0 ||
880
+ group.value.id === newNamePlaceholder ||
881
+ Boolean(
882
+ group.accessPointStates.find(
883
+ (apState) => apState.accessPoint.id === newNamePlaceholder,
884
+ ),
885
+ )
886
+ );
887
+ };
888
+
889
+ //Drag and Drop - reorder groups and accept access points from other groups
890
+ const handleHover = useCallback(
891
+ (item: APGDragSource): void => {
892
+ const draggingProperty = item.groupState;
893
+ const hoveredProperty = group;
894
+ dataProductEditorState.swapAccessPointGroups(
895
+ draggingProperty,
896
+ hoveredProperty,
897
+ );
898
+ },
899
+ [group, dataProductEditorState],
900
+ );
901
+
902
+ const [{ isOver }, dropConnector] = useDrop<
903
+ APGDragSource | AccessPointDragSource,
904
+ void,
905
+ {
906
+ isOver: boolean;
907
+ }
908
+ >(
909
+ () => ({
910
+ accept: [AP_GROUP_DND_TYPE, AP_DND_TYPE],
911
+ hover: (item, monitor) => {
912
+ const itemType = monitor.getItemType();
913
+ if (itemType === AP_GROUP_DND_TYPE) {
914
+ const groupItem = item as APGDragSource;
915
+ handleHover(groupItem);
916
+ }
917
+ },
918
+ drop: (item, monitor) => {
919
+ const itemType = monitor.getItemType();
920
+ if (itemType === AP_DND_TYPE) {
921
+ const accessPointItem = item as AccessPointDragSource;
922
+ group.addAccessPoint(accessPointItem.accessPointState);
923
+ accessPointItem.accessPointState.state.deleteAccessPoint(
924
+ accessPointItem.accessPointState,
925
+ );
926
+ accessPointItem.accessPointState.changeGroupState(group);
927
+ }
928
+ },
929
+ collect: (monitor) => ({
930
+ isBeingDraggedAPG:
931
+ monitor.getItemType() === AP_GROUP_DND_TYPE
932
+ ? monitor.getItem<APGDragSource | null>()?.groupState
933
+ : undefined,
934
+ isBeingDraggedAP:
935
+ monitor.getItemType() === AP_DND_TYPE
936
+ ? monitor.getItem<AccessPointDragSource | null>()
937
+ ?.accessPointState
938
+ : undefined,
939
+ isOver: monitor.isOver(),
940
+ }),
941
+ }),
942
+ [handleHover],
943
+ );
944
+
945
+ const [, dragConnector, dragPreviewConnector] = useDrag<APGDragSource>(
946
+ () => ({
947
+ type: AP_GROUP_DND_TYPE,
948
+ item: () => ({
949
+ groupState: group,
950
+ }),
951
+ collect: (monitor) => ({
952
+ isDragging: monitor.isDragging(),
953
+ }),
954
+ }),
955
+ [group],
956
+ );
957
+ dragConnector(ref);
958
+ dropConnector(ref);
959
+ dragPreviewConnector(ref);
960
+
961
+ return (
962
+ <div
963
+ ref={ref}
964
+ key={group.uuid}
965
+ onClick={(): void => changeGroup(group)}
966
+ className={clsx('service-editor__tab', {
967
+ 'service-editor__tab--active': group === selectedGroupState,
968
+ })}
969
+ style={{
970
+ backgroundColor: isOver
971
+ ? 'var(--color-dark-grey-100)'
972
+ : 'var(--color-dark-grey-50)',
973
+ }}
974
+ >
975
+ {group.value.id}
976
+ &nbsp;
977
+ {groupError() && (
978
+ <ErrorWarnIcon
979
+ title="Resolve Access Point Group error(s)"
980
+ style={{ color: 'var(--color-red-300)' }}
981
+ />
982
+ )}
983
+ </div>
984
+ );
985
+ },
986
+ );
987
+
988
+ const AccessPointGroupTab = observer(
989
+ (props: {
990
+ dataProductEditorState: DataProductEditorState;
991
+ isReadOnly: boolean;
992
+ }) => {
993
+ const { dataProductEditorState, isReadOnly } = props;
994
+ const groupStates = dataProductEditorState.accessPointGroupStates;
995
+ const selectedGroupState = dataProductEditorState.selectedGroupState;
996
+ const handleAddAccessPointGroup = () => {
997
+ const newGroup =
998
+ dataProductEditorState.createGroupAndAdd(newNamePlaceholder);
999
+ dataProductEditorState.setSelectedGroupState(newGroup);
1000
+ };
1001
+
1002
+ const AccessPointDragPreviewLayer: React.FC = () => (
1003
+ <DragPreviewLayer
1004
+ labelGetter={(item: AccessPointDragSource): string => {
1005
+ return item.accessPointState.accessPoint.id;
1006
+ }}
1007
+ types={[AP_DND_TYPE]}
1008
+ />
1009
+ );
1010
+
1011
+ const disableAddGroup = Boolean(
1012
+ dataProductEditorState.accessPointGroupStates.find(
1013
+ (group) => group.value.id === newNamePlaceholder,
1014
+ ),
1015
+ );
1016
+
1017
+ return (
1018
+ <div className="panel" style={{ overflow: 'visible' }}>
1019
+ <AccessPointDragPreviewLayer />
1020
+ <div
1021
+ className="panel__content__form__section__header__label"
1022
+ style={{ paddingLeft: '1rem' }}
1023
+ >
1024
+ Access Point Groups
1025
+ </div>
1026
+ <PanelHeader>
1027
+ <div className="uml-element-editor__tabs">
1028
+ {groupStates.map((group) => {
1029
+ return (
1030
+ <GroupTabRenderer
1031
+ key={group.uuid}
1032
+ group={group}
1033
+ dataProductEditorState={dataProductEditorState}
1034
+ />
1035
+ );
1036
+ })}
1037
+ <PanelHeaderActionItem
1038
+ className="panel__header__action"
1039
+ onClick={handleAddAccessPointGroup}
1040
+ disabled={isReadOnly || disableAddGroup}
1041
+ title={
1042
+ disableAddGroup
1043
+ ? 'Provide all group names'
1044
+ : 'Create new access point group'
1045
+ }
1046
+ >
1047
+ <PlusIcon />
1048
+ </PanelHeaderActionItem>
1049
+ </div>
1050
+
1051
+ <PanelHeaderActions></PanelHeaderActions>
1052
+ </PanelHeader>
1053
+ <PanelContent>
1054
+ {selectedGroupState && (
1055
+ <AccessPointGroupEditor
1056
+ key={selectedGroupState.uuid}
1057
+ groupState={selectedGroupState}
889
1058
  isReadOnly={isReadOnly}
890
- accessPointState={apState}
891
1059
  />
1060
+ )}
1061
+ </PanelContent>
1062
+ {dataProductEditorState.deployResponse && (
1063
+ <DataProductDeploymentResponseModal state={dataProductEditorState} />
1064
+ )}
1065
+ </div>
1066
+ );
1067
+ },
1068
+ );
1069
+
1070
+ const DataProductSidebar = observer(
1071
+ (props: { dataProductEditorState: DataProductEditorState }) => {
1072
+ const { dataProductEditorState } = props;
1073
+ const sidebarTabs = [
1074
+ {
1075
+ label: DATA_PRODUCT_TAB.HOME,
1076
+ icon: <HomeIcon />,
1077
+ },
1078
+ {
1079
+ label: DATA_PRODUCT_TAB.APG,
1080
+ title: 'Access Point Groups',
1081
+ icon: <GroupWorkIcon />,
1082
+ },
1083
+ {
1084
+ label: DATA_PRODUCT_TAB.SUPPORT,
1085
+ icon: <QuestionCircleIcon />,
1086
+ },
1087
+ ];
1088
+ return (
1089
+ <div
1090
+ className="data-space__viewer__activity-bar"
1091
+ style={{ position: 'static', maxHeight: '100%' }}
1092
+ >
1093
+ <div className="data-space__viewer__activity-bar__items">
1094
+ {sidebarTabs.map((activity) => (
1095
+ <button
1096
+ key={activity.label}
1097
+ className={clsx('data-space__viewer__activity-bar__item', {
1098
+ 'data-space__viewer__activity-bar__item--active':
1099
+ dataProductEditorState.selectedTab === activity.label,
1100
+ })}
1101
+ onClick={() =>
1102
+ dataProductEditorState.setSelectedTab(activity.label)
1103
+ }
1104
+ tabIndex={-1}
1105
+ title={activity.title ?? activity.label}
1106
+ style={{
1107
+ flexDirection: 'column',
1108
+ fontSize: '12px',
1109
+ margin: '1rem 0rem',
1110
+ }}
1111
+ >
1112
+ {activity.icon}
1113
+ {activity.label}
1114
+ </button>
892
1115
  ))}
893
- {productEditorState.accessPointModal && (
894
- <NewAccessPointAccessPoint
895
- dataProductEditorState={productEditorState}
1116
+ </div>
1117
+ </div>
1118
+ );
1119
+ },
1120
+ );
1121
+
1122
+ const HomeTab = observer(
1123
+ (props: { product: DataProduct; isReadOnly: boolean }) => {
1124
+ const { product, isReadOnly } = props;
1125
+ const updateDataProductTitle = (val: string | undefined): void => {
1126
+ dataProduct_setTitle(product, val ?? '');
1127
+ };
1128
+ const updateDataProductDescription: ChangeEventHandler<
1129
+ HTMLTextAreaElement
1130
+ > = (event) => {
1131
+ dataProduct_setDescription(product, event.target.value);
1132
+ };
1133
+
1134
+ return (
1135
+ <div style={{ flexDirection: 'column', display: 'flex' }}>
1136
+ <PanelFormTextField
1137
+ name="Title"
1138
+ value={product.title}
1139
+ prompt="Provide a descriptive name for the Data Product to appear in Marketplace."
1140
+ update={updateDataProductTitle}
1141
+ placeholder="Enter title"
1142
+ />
1143
+ <div style={{ margin: '1rem' }}>
1144
+ <div className="panel__content__form__section__header__label">
1145
+ Description
1146
+ </div>
1147
+ <div
1148
+ className="panel__content__form__section__header__prompt"
1149
+ style={{
1150
+ color:
1151
+ product.description === '' || product.description === undefined
1152
+ ? 'var(--color-red-300)'
1153
+ : 'var(--color-light-grey-400)',
1154
+ }}
1155
+ >
1156
+ Clearly describe the purpose, content, and intended use of the Data
1157
+ Product.
1158
+ </div>
1159
+ <textarea
1160
+ className="panel__content__form__section__textarea"
1161
+ spellCheck={false}
1162
+ disabled={isReadOnly}
1163
+ value={product.description}
1164
+ onChange={updateDataProductDescription}
1165
+ style={{
1166
+ padding: '0.5rem',
1167
+ width: '45rem',
1168
+ maxWidth: '45rem !important',
1169
+ borderColor:
1170
+ product.description === '' || product.description === undefined
1171
+ ? 'var(--color-red-300)'
1172
+ : 'transparent',
1173
+ }}
896
1174
  />
897
- )}
1175
+ </div>
898
1176
  </div>
899
1177
  );
900
1178
  },
901
1179
  );
902
1180
 
1181
+ const SupportTab = observer(
1182
+ (props: { product: DataProduct; isReadOnly: boolean }) => {
1183
+ const { product, isReadOnly } = props;
1184
+ const updateSupportInfoDocumentationUrl = (
1185
+ val: string | undefined,
1186
+ ): void => {
1187
+ dataProduct_setSupportInfoIfAbsent(product);
1188
+ if (product.supportInfo) {
1189
+ supportInfo_setDocumentationUrl(product.supportInfo, val ?? '');
1190
+ }
1191
+ };
1192
+
1193
+ const updateSupportInfoWebsite = (val: string | undefined): void => {
1194
+ dataProduct_setSupportInfoIfAbsent(product);
1195
+ if (product.supportInfo) {
1196
+ supportInfo_setWebsite(product.supportInfo, val ?? '');
1197
+ }
1198
+ };
1199
+
1200
+ const updateSupportInfoFaqUrl = (val: string | undefined): void => {
1201
+ dataProduct_setSupportInfoIfAbsent(product);
1202
+ if (product.supportInfo) {
1203
+ supportInfo_setFaqUrl(product.supportInfo, val ?? '');
1204
+ }
1205
+ };
1206
+
1207
+ const updateSupportInfoSupportUrl = (val: string | undefined): void => {
1208
+ dataProduct_setSupportInfoIfAbsent(product);
1209
+ if (product.supportInfo) {
1210
+ supportInfo_setSupportUrl(product.supportInfo, val ?? '');
1211
+ }
1212
+ };
1213
+
1214
+ const handleSupportInfoEmailAdd = (
1215
+ address: string,
1216
+ title: string,
1217
+ ): void => {
1218
+ dataProduct_setSupportInfoIfAbsent(product);
1219
+ if (product.supportInfo) {
1220
+ supportInfo_addEmail(product.supportInfo, new Email(address, title));
1221
+ }
1222
+ };
1223
+
1224
+ const handleSupportInfoEmailRemove = (email: Email): void => {
1225
+ if (product.supportInfo) {
1226
+ supportInfo_deleteEmail(product.supportInfo, email);
1227
+ }
1228
+ };
1229
+
1230
+ const SupportEmailComponent = observer(
1231
+ (supportEmailProps: { item: Email }): React.ReactElement => {
1232
+ const { item } = supportEmailProps;
1233
+
1234
+ return (
1235
+ <div className="panel__content__form__section__list__item__rows">
1236
+ <div className="row">
1237
+ <label className="label">Address</label>
1238
+ <div className="textbox">{item.address}</div>
1239
+ </div>
1240
+ <div className="row">
1241
+ <label className="label">Title</label>
1242
+ <div className="textbox">{item.title}</div>
1243
+ </div>
1244
+ </div>
1245
+ );
1246
+ },
1247
+ );
1248
+
1249
+ const NewSupportEmailComponent = observer(
1250
+ (newSupportEmailProps: { onFinishEditing: () => void }) => {
1251
+ const { onFinishEditing } = newSupportEmailProps;
1252
+ const [address, setAddress] = useState('');
1253
+ const [title, setTitle] = useState('');
1254
+
1255
+ return (
1256
+ <div className="data-product-editor__support-info__new-email">
1257
+ <div className="panel__content__form__section__list__new-item__input">
1258
+ <input
1259
+ className="input input-group__input panel__content__form__section__input input--dark"
1260
+ type="email"
1261
+ placeholder="Enter email"
1262
+ value={address}
1263
+ onChange={(event) => {
1264
+ setAddress(event.target.value);
1265
+ }}
1266
+ />
1267
+ </div>
1268
+ <div className="panel__content__form__section__list__new-item__input">
1269
+ <input
1270
+ className="input input-group__input panel__content__form__section__input input--dark"
1271
+ type="title"
1272
+ placeholder="Enter title"
1273
+ value={title}
1274
+ onChange={(event) => {
1275
+ setTitle(event.target.value);
1276
+ }}
1277
+ />
1278
+ </div>
1279
+ <button
1280
+ className="panel__content__form__section__list__new-item__add-btn btn btn--dark"
1281
+ onClick={() => {
1282
+ handleSupportInfoEmailAdd(address, title);
1283
+ setAddress('');
1284
+ setTitle('');
1285
+ onFinishEditing();
1286
+ }}
1287
+ >
1288
+ Save
1289
+ </button>
1290
+ </div>
1291
+ );
1292
+ },
1293
+ );
1294
+
1295
+ return (
1296
+ <PanelFormSection>
1297
+ <div className="panel__content__form__section__header__label">
1298
+ Support Information
1299
+ </div>
1300
+ <div className="panel__content__form__section__header__prompt">
1301
+ Configure support information for this Lakehouse Data Product.
1302
+ </div>
1303
+ <PanelFormTextField
1304
+ name="Documentation URL"
1305
+ value={product.supportInfo?.documentationUrl ?? ''}
1306
+ update={updateSupportInfoDocumentationUrl}
1307
+ placeholder="Enter Documentation URL"
1308
+ />
1309
+ <PanelFormTextField
1310
+ name="Website"
1311
+ value={product.supportInfo?.website}
1312
+ update={updateSupportInfoWebsite}
1313
+ placeholder="Enter Website"
1314
+ />
1315
+ <PanelFormTextField
1316
+ name="FAQ URL"
1317
+ value={product.supportInfo?.faqUrl}
1318
+ update={updateSupportInfoFaqUrl}
1319
+ placeholder="Enter FAQ URL"
1320
+ />
1321
+ <PanelFormTextField
1322
+ name="Support URL"
1323
+ value={product.supportInfo?.supportUrl}
1324
+ update={updateSupportInfoSupportUrl}
1325
+ placeholder="Enter Support URL"
1326
+ />
1327
+ <ListEditor
1328
+ title="Emails"
1329
+ items={product.supportInfo?.emails}
1330
+ keySelector={(email: Email) => email.address + email.title}
1331
+ ItemComponent={SupportEmailComponent}
1332
+ NewItemComponent={NewSupportEmailComponent}
1333
+ handleRemoveItem={handleSupportInfoEmailRemove}
1334
+ isReadOnly={isReadOnly}
1335
+ emptyMessage="No emails specified"
1336
+ />
1337
+ </PanelFormSection>
1338
+ );
1339
+ },
1340
+ );
1341
+
903
1342
  export const DataProductEditor = observer(() => {
904
1343
  const editorStore = useEditorStore();
905
1344
  const dataProductEditorState =
906
1345
  editorStore.tabManagerState.getCurrentEditorState(DataProductEditorState);
907
1346
  const product = dataProductEditorState.product;
908
- const accessPointStates = dataProductEditorState.accessPointGroupStates
909
- .map((e) => e.accessPointStates)
910
- .flat();
911
1347
  const isReadOnly = dataProductEditorState.isReadOnly;
912
- const openNewModal = () => {
913
- dataProductEditorState.setAccessPointGroupModal(true);
914
- };
915
1348
  const auth = useAuth();
1349
+
916
1350
  const deployDataProduct = (): void => {
917
1351
  // Trigger OAuth flow if not authenticated
918
1352
  if (!auth.isAuthenticated) {
@@ -937,42 +1371,25 @@ export const DataProductEditor = observer(() => {
937
1371
  }
938
1372
  };
939
1373
 
940
- const updateDataProductTitle = (val: string | undefined): void => {
941
- dataProduct_setTitle(product, val ?? '');
942
- };
943
- const updateDataProductDescription: ChangeEventHandler<
944
- HTMLTextAreaElement
945
- > = (event) => {
946
- dataProduct_setDescription(product, event.target.value);
947
- };
948
-
949
- const updateSupportInfoDocumentationUrl = (val: string | undefined): void => {
950
- dataProduct_setSupportInfoIfAbsent(product);
951
- if (product.supportInfo) {
952
- supportInfo_setDocumentationUrl(product.supportInfo, val ?? '');
953
- }
954
- };
955
-
956
- const updateSupportInfoWebsite = (val: string | undefined): void => {
957
- dataProduct_setSupportInfoIfAbsent(product);
958
- if (product.supportInfo) {
959
- supportInfo_setWebsite(product.supportInfo, val ?? '');
960
- }
961
- };
962
-
963
- const updateSupportInfoFaqUrl = (val: string | undefined): void => {
964
- dataProduct_setSupportInfoIfAbsent(product);
965
- if (product.supportInfo) {
966
- supportInfo_setFaqUrl(product.supportInfo, val ?? '');
1374
+ const selectedActivity = dataProductEditorState.selectedTab;
1375
+ const renderActivivtyBarTab = (): React.ReactNode => {
1376
+ switch (selectedActivity) {
1377
+ case DATA_PRODUCT_TAB.HOME:
1378
+ return <HomeTab product={product} isReadOnly={isReadOnly} />;
1379
+ case DATA_PRODUCT_TAB.SUPPORT:
1380
+ return <SupportTab product={product} isReadOnly={isReadOnly} />;
1381
+ case DATA_PRODUCT_TAB.APG:
1382
+ return (
1383
+ <AccessPointGroupTab
1384
+ dataProductEditorState={dataProductEditorState}
1385
+ isReadOnly={isReadOnly}
1386
+ />
1387
+ );
1388
+ default:
1389
+ return null;
967
1390
  }
968
1391
  };
969
1392
 
970
- const updateSupportInfoSupportUrl = (val: string | undefined): void => {
971
- dataProduct_setSupportInfoIfAbsent(product);
972
- if (product.supportInfo) {
973
- supportInfo_setSupportUrl(product.supportInfo, val ?? '');
974
- }
975
- };
976
1393
  useApplicationNavigationContext(
977
1394
  LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.DATA_PRODUCT_EDITOR,
978
1395
  );
@@ -995,84 +1412,6 @@ export const DataProductEditor = observer(() => {
995
1412
  dataProductEditorState,
996
1413
  ]);
997
1414
 
998
- const handleSupportInfoEmailAdd = (address: string, title: string): void => {
999
- dataProduct_setSupportInfoIfAbsent(product);
1000
- if (product.supportInfo) {
1001
- supportInfo_addEmail(product.supportInfo, new Email(address, title));
1002
- }
1003
- };
1004
-
1005
- const handleSupportInfoEmailRemove = (email: Email): void => {
1006
- if (product.supportInfo) {
1007
- supportInfo_deleteEmail(product.supportInfo, email);
1008
- }
1009
- };
1010
-
1011
- const SupportEmailComponent = observer(
1012
- (props: { item: Email }): React.ReactElement => {
1013
- const { item } = props;
1014
-
1015
- return (
1016
- <div className="panel__content__form__section__list__item__rows">
1017
- <div className="row">
1018
- <label className="label">Address</label>
1019
- <div className="textbox">{item.address}</div>
1020
- </div>
1021
- <div className="row">
1022
- <label className="label">Title</label>
1023
- <div className="textbox">{item.title}</div>
1024
- </div>
1025
- </div>
1026
- );
1027
- },
1028
- );
1029
-
1030
- const NewSupportEmailComponent = observer(
1031
- (props: { onFinishEditing: () => void }) => {
1032
- const { onFinishEditing } = props;
1033
- const [address, setAddress] = useState('');
1034
- const [title, setTitle] = useState('');
1035
-
1036
- return (
1037
- <div className="data-product-editor__support-info__new-email">
1038
- <div className="panel__content__form__section__list__new-item__input">
1039
- <input
1040
- className="input input-group__input panel__content__form__section__input input--dark"
1041
- type="email"
1042
- placeholder="Enter email"
1043
- value={address}
1044
- onChange={(event) => {
1045
- setAddress(event.target.value);
1046
- }}
1047
- />
1048
- </div>
1049
- <div className="panel__content__form__section__list__new-item__input">
1050
- <input
1051
- className="input input-group__input panel__content__form__section__input input--dark"
1052
- type="title"
1053
- placeholder="Enter title"
1054
- value={title}
1055
- onChange={(event) => {
1056
- setTitle(event.target.value);
1057
- }}
1058
- />
1059
- </div>
1060
- <button
1061
- className="panel__content__form__section__list__new-item__add-btn btn btn--dark"
1062
- onClick={() => {
1063
- handleSupportInfoEmailAdd(address, title);
1064
- setAddress('');
1065
- setTitle('');
1066
- onFinishEditing();
1067
- }}
1068
- >
1069
- Save
1070
- </button>
1071
- </div>
1072
- );
1073
- },
1074
- );
1075
-
1076
1415
  return (
1077
1416
  <div className="data-product-editor">
1078
1417
  <div className="panel">
@@ -1100,127 +1439,14 @@ export const DataProductEditor = observer(() => {
1100
1439
  </div>
1101
1440
  </PanelHeaderActions>
1102
1441
  </div>
1103
- <div className="panel" style={{ padding: '1rem', flex: 0 }}>
1104
- <PanelFormTextField
1105
- name="Title"
1106
- value={product.title}
1107
- prompt="Provide a title for this Lakehouse Data Product."
1108
- update={updateDataProductTitle}
1109
- placeholder="Enter title"
1110
- />
1111
- <div style={{ margin: '1rem' }}>
1112
- <div className="panel__content__form__section__header__label">
1113
- Description
1114
- </div>
1115
- <div className="panel__content__form__section__header__prompt">
1116
- Provide a description for this Lakehouse Data Product.
1117
- </div>
1118
- <textarea
1119
- className="panel__content__form__section__textarea"
1120
- spellCheck={false}
1121
- disabled={isReadOnly}
1122
- value={product.description}
1123
- onChange={updateDataProductDescription}
1124
- style={{
1125
- padding: '0.5rem',
1126
- width: '45rem',
1127
- maxWidth: '45rem !important',
1128
- }}
1129
- />
1130
- </div>
1131
1442
 
1132
- <PanelFormSection>
1133
- <div className="panel__content__form__section__header__label">
1134
- Support Information
1135
- </div>
1136
- <div className="panel__content__form__section__header__prompt">
1137
- Configure support information for this Lakehouse Data Product.
1138
- </div>
1139
- <PanelFormTextField
1140
- name="Documentation URL"
1141
- value={product.supportInfo?.documentationUrl ?? ''}
1142
- update={updateSupportInfoDocumentationUrl}
1143
- placeholder="Enter Documentation URL"
1144
- />
1145
- <PanelFormTextField
1146
- name="Website"
1147
- value={product.supportInfo?.website}
1148
- update={updateSupportInfoWebsite}
1149
- placeholder="Enter Website"
1150
- />
1151
- <PanelFormTextField
1152
- name="FAQ URL"
1153
- value={product.supportInfo?.faqUrl}
1154
- update={updateSupportInfoFaqUrl}
1155
- placeholder="Enter FAQ URL"
1156
- />
1157
- <PanelFormTextField
1158
- name="Support URL"
1159
- value={product.supportInfo?.supportUrl}
1160
- update={updateSupportInfoSupportUrl}
1161
- placeholder="Enter Support URL"
1162
- />
1163
- <ListEditor
1164
- title="Emails"
1165
- items={product.supportInfo?.emails}
1166
- keySelector={(email: Email) => email.address + email.title}
1167
- ItemComponent={SupportEmailComponent}
1168
- NewItemComponent={NewSupportEmailComponent}
1169
- handleRemoveItem={handleSupportInfoEmailRemove}
1170
- isReadOnly={isReadOnly}
1171
- emptyMessage="No emails specified"
1172
- />
1173
- </PanelFormSection>
1174
- </div>
1175
- <div className="panel" style={{ overflow: 'auto' }}>
1176
- <PanelHeader>
1177
- <div className="panel__header__title">
1178
- <div className="panel__header__title__label">
1179
- access point groups
1180
- </div>
1181
- </div>
1182
- <PanelHeaderActions>
1183
- <PanelHeaderActionItem
1184
- className="panel__header__action"
1185
- onClick={openNewModal}
1186
- disabled={isReadOnly}
1187
- title="Create new access point group"
1188
- >
1189
- <PlusIcon />
1190
- </PanelHeaderActionItem>
1191
- </PanelHeaderActions>
1192
- </PanelHeader>
1193
- <PanelContent>
1194
- <div
1195
- style={{ overflow: 'auto', margin: '1rem', marginLeft: '1.5rem' }}
1196
- >
1197
- {dataProductEditorState.accessPointGroupStates.map(
1198
- (groupState) =>
1199
- groupState.accessPointStates.length > 0 && (
1200
- <AccessPointGroupSection
1201
- key={groupState.uuid}
1202
- groupState={groupState}
1203
- isReadOnly={isReadOnly}
1204
- />
1205
- ),
1206
- )}
1207
- </div>
1208
- {!accessPointStates.length && (
1209
- <DataProductEditorSplashScreen
1210
- dataProductEditorState={dataProductEditorState}
1211
- />
1212
- )}
1213
- </PanelContent>
1214
- {dataProductEditorState.accessPointGroupModal && (
1215
- <NewAccessPointGroupModal
1216
- dataProductEditorState={dataProductEditorState}
1217
- />
1218
- )}
1219
- {dataProductEditorState.deployResponse && (
1220
- <DataProductDeploymentResponseModal
1221
- state={dataProductEditorState}
1222
- />
1223
- )}
1443
+ <div
1444
+ className="panel"
1445
+ style={{ padding: '1rem', flexDirection: 'row' }}
1446
+ >
1447
+ <DataProductSidebar dataProductEditorState={dataProductEditorState} />
1448
+
1449
+ {renderActivivtyBarTab()}
1224
1450
  </div>
1225
1451
  </div>
1226
1452
  </div>