@finos/legend-application-studio 22.0.0 → 22.1.0

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 (28) hide show
  1. package/lib/components/EditorComponentTestUtils.d.ts +9 -6
  2. package/lib/components/EditorComponentTestUtils.d.ts.map +1 -1
  3. package/lib/components/EditorComponentTestUtils.js +9 -6
  4. package/lib/components/EditorComponentTestUtils.js.map +1 -1
  5. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectConfigurationEditor.d.ts.map +1 -1
  6. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectConfigurationEditor.js +7 -139
  7. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectConfigurationEditor.js.map +1 -1
  8. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectDependencyEditor.d.ts +22 -0
  9. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectDependencyEditor.d.ts.map +1 -0
  10. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectDependencyEditor.js +282 -0
  11. package/lib/components/editor/edit-panel/project-configuration-editor/ProjectDependencyEditor.js.map +1 -0
  12. package/lib/index.css +2 -2
  13. package/lib/index.css.map +1 -1
  14. package/lib/package.json +1 -1
  15. package/lib/stores/EditorGraphState.d.ts.map +1 -1
  16. package/lib/stores/EditorGraphState.js +9 -6
  17. package/lib/stores/EditorGraphState.js.map +1 -1
  18. package/lib/stores/editor-state/ProjectConfigurationEditorState.d.ts +22 -9
  19. package/lib/stores/editor-state/ProjectConfigurationEditorState.d.ts.map +1 -1
  20. package/lib/stores/editor-state/ProjectConfigurationEditorState.js +93 -51
  21. package/lib/stores/editor-state/ProjectConfigurationEditorState.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/components/EditorComponentTestUtils.tsx +12 -9
  24. package/src/components/editor/edit-panel/project-configuration-editor/ProjectConfigurationEditor.tsx +5 -397
  25. package/src/components/editor/edit-panel/project-configuration-editor/ProjectDependencyEditor.tsx +724 -0
  26. package/src/stores/EditorGraphState.ts +16 -10
  27. package/src/stores/editor-state/ProjectConfigurationEditorState.ts +117 -66
  28. package/tsconfig.json +1 -0
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ EDITOR_LANGUAGE,
19
+ TextInputEditor,
20
+ useApplicationStore,
21
+ TAB_SIZE,
22
+ } from '@finos/legend-application';
23
+ import {
24
+ type SelectComponent,
25
+ type TreeData,
26
+ type TreeNodeContainerProps,
27
+ compareLabelFn,
28
+ clsx,
29
+ CustomSelectorInput,
30
+ TimesIcon,
31
+ ExternalLinkSquareIcon,
32
+ Dialog,
33
+ DropdownMenu,
34
+ CaretDownIcon,
35
+ MenuContentItem,
36
+ MenuContent,
37
+ Modal,
38
+ ModalBody,
39
+ ModalFooter,
40
+ ModalHeader,
41
+ PanelLoadingIndicator,
42
+ TreeView,
43
+ ChevronDownIcon,
44
+ ChevronRightIcon,
45
+ ContextMenu,
46
+ RepoIcon,
47
+ CompressIcon,
48
+ SubjectIcon,
49
+ ViewHeadlineIcon,
50
+ } from '@finos/legend-art';
51
+ import {
52
+ MASTER_SNAPSHOT_ALIAS,
53
+ type ProjectDependencyGraphReport,
54
+ type ProjectData,
55
+ SNAPSHOT_VERSION_ALIAS,
56
+ } from '@finos/legend-server-depot';
57
+ import type { ProjectDependency } from '@finos/legend-server-sdlc';
58
+ import {
59
+ assertErrorThrown,
60
+ guaranteeNonNullable,
61
+ isNonNullable,
62
+ LogEvent,
63
+ } from '@finos/legend-shared';
64
+ import {
65
+ compareSemVerVersions,
66
+ generateGAVCoordinates,
67
+ } from '@finos/legend-storage';
68
+ import { flowResult } from 'mobx';
69
+ import { observer } from 'mobx-react-lite';
70
+ import { forwardRef, useRef, useState } from 'react';
71
+ import {
72
+ type DependencyTreeNodeData,
73
+ buildDependencyNodeChildren,
74
+ ProjectConfigurationEditorState,
75
+ } from '../../../../stores/editor-state/ProjectConfigurationEditorState.js';
76
+ import { LEGEND_STUDIO_APP_EVENT } from '../../../../stores/LegendStudioAppEvent.js';
77
+ import {
78
+ generateViewProjectByGAVRoute,
79
+ generateViewVersionRoute,
80
+ } from '../../../../stores/LegendStudioRouter.js';
81
+ import { LEGEND_STUDIO_TEST_ID } from '../../../LegendStudioTestID.js';
82
+ import { useEditorStore } from '../../EditorStoreProvider.js';
83
+
84
+ interface VersionOption {
85
+ label: string;
86
+ value: string;
87
+ }
88
+ interface ProjectOption {
89
+ label: string;
90
+ value: ProjectData;
91
+ }
92
+
93
+ const buildProjectOption = (project: ProjectData): ProjectOption => ({
94
+ label: project.coordinates,
95
+ value: project,
96
+ });
97
+
98
+ const ProjectDependencyActions = observer(
99
+ (props: { config: ProjectConfigurationEditorState }) => {
100
+ const { config } = props;
101
+ const hasConflicts = config.dependencyReport?.conflicts.length;
102
+ const viewTree = (): void => {
103
+ if (config.dependencyReport) {
104
+ config.setDependencyTreeReportModal(true);
105
+ }
106
+ };
107
+ const viewConflict = (): void => {
108
+ if (config.dependencyReport) {
109
+ config.setDependencyConflictModal(true);
110
+ }
111
+ };
112
+ return (
113
+ <div className="project-dependency-editor__info">
114
+ <button
115
+ className="btn btn--dark"
116
+ tabIndex={-1}
117
+ onClick={viewTree}
118
+ disabled={!config.dependencyReport}
119
+ title="View Dependency Explorer"
120
+ >
121
+ View Dependency Explorer
122
+ </button>
123
+
124
+ {Boolean(hasConflicts) && (
125
+ <button
126
+ className="project-dependency-editor__conflicts-btn"
127
+ tabIndex={-1}
128
+ onClick={viewConflict}
129
+ disabled={!config.dependencyReport?.conflictPaths.size}
130
+ title="View any conflicts in your dependencies"
131
+ >
132
+ View Conflicts
133
+ </button>
134
+ )}
135
+ </div>
136
+ );
137
+ },
138
+ );
139
+
140
+ const formatOptionLabel = (option: ProjectOption): React.ReactNode => (
141
+ <div className="project-dependency-editor__label">
142
+ <div className="project-dependency-editor__label__tag">
143
+ {option.value.projectId}
144
+ </div>
145
+ <div className="project-dependency-editor__label__name">
146
+ {option.value.coordinates}
147
+ </div>
148
+ </div>
149
+ );
150
+
151
+ const DependencyTreeNodeContextMenu = observer(
152
+ forwardRef<
153
+ HTMLDivElement,
154
+ {
155
+ node: DependencyTreeNodeData;
156
+ }
157
+ >(function WorkflowExplorerContextMenu(props, ref) {
158
+ const { node } = props;
159
+ const value = node.value;
160
+ const applicationStore = useApplicationStore();
161
+ const viewProject = (): void => {
162
+ applicationStore.navigator.visitAddress(
163
+ applicationStore.navigator.generateAddress(
164
+ generateViewProjectByGAVRoute(
165
+ guaranteeNonNullable(value.groupId),
166
+ guaranteeNonNullable(value.artifactId),
167
+ value.versionId === MASTER_SNAPSHOT_ALIAS
168
+ ? SNAPSHOT_VERSION_ALIAS
169
+ : value.versionId,
170
+ ),
171
+ ),
172
+ );
173
+ };
174
+ const viewSDLCProject = (): void => {
175
+ applicationStore.navigator.visitAddress(
176
+ applicationStore.navigator.generateAddress(
177
+ generateViewVersionRoute(value.projectId, value.versionId),
178
+ ),
179
+ );
180
+ };
181
+
182
+ return (
183
+ <MenuContent data-testid={LEGEND_STUDIO_TEST_ID.EXPLORER_CONTEXT_MENU}>
184
+ <MenuContentItem onClick={viewProject}>Visit Project</MenuContentItem>
185
+ <MenuContentItem onClick={viewSDLCProject}>
186
+ Visit SDLC Project
187
+ </MenuContentItem>
188
+ </MenuContent>
189
+ );
190
+ }),
191
+ );
192
+
193
+ const DependencyTreeNodeContainer: React.FC<
194
+ TreeNodeContainerProps<
195
+ DependencyTreeNodeData,
196
+ {
197
+ onNodeExpand: (node: DependencyTreeNodeData) => void;
198
+ }
199
+ >
200
+ > = (props) => {
201
+ const { node, level, stepPaddingInRem, onNodeSelect, innerProps } = props;
202
+ const { onNodeExpand } = innerProps;
203
+ const isExpandable = Boolean(node.childrenIds?.length);
204
+ const selectNode = (): void => onNodeSelect?.(node);
205
+ const expandNode = (): void => onNodeExpand(node);
206
+ const value = node.value;
207
+ const label = `${value.artifactId}:${value.versionId}`;
208
+ const nodeExpandIcon = isExpandable ? (
209
+ node.isOpen ? (
210
+ <ChevronDownIcon />
211
+ ) : (
212
+ <ChevronRightIcon />
213
+ )
214
+ ) : (
215
+ <div />
216
+ );
217
+
218
+ return (
219
+ <ContextMenu
220
+ content={<DependencyTreeNodeContextMenu node={node} />}
221
+ menuProps={{ elevation: 7 }}
222
+ >
223
+ <div
224
+ className={clsx(
225
+ 'tree-view__node__container project-dependency-explorer-tree__node__container',
226
+ {
227
+ 'menu__trigger--on-menu-open': !node.isSelected,
228
+ },
229
+ {
230
+ 'project-dependency-explorer-tree__node__container--selected':
231
+ node.isSelected,
232
+ },
233
+ )}
234
+ style={{
235
+ paddingLeft: `${(level - 1) * (stepPaddingInRem ?? 1)}rem`,
236
+ }}
237
+ onClick={selectNode}
238
+ >
239
+ <div className="tree-view__node__icon project-dependency-explorer-tree__node__icon">
240
+ <div
241
+ onClick={expandNode}
242
+ className="project-dependency-explorer-tree__node__icon__expand"
243
+ >
244
+ {nodeExpandIcon}
245
+ </div>
246
+ <div className="project-dependency-explorer-tree__node__icon__type">
247
+ <RepoIcon />
248
+ </div>
249
+ </div>
250
+
251
+ <button
252
+ className="tree-view__node__label project-dependency-explorer-tree__node__label"
253
+ tabIndex={-1}
254
+ title={node.id}
255
+ >
256
+ {label}
257
+ </button>
258
+ </div>
259
+ </ContextMenu>
260
+ );
261
+ };
262
+
263
+ const DependencyTreeView: React.FC<{
264
+ configState: ProjectConfigurationEditorState;
265
+ treeData: TreeData<DependencyTreeNodeData>;
266
+ setTreeData: (treeData: TreeData<DependencyTreeNodeData>) => void;
267
+ }> = (props) => {
268
+ const { treeData, setTreeData } = props;
269
+ const onNodeExpand = (node: DependencyTreeNodeData): void => {
270
+ if (node.childrenIds?.length) {
271
+ node.isOpen = !node.isOpen;
272
+ node.childrenIds
273
+ .map((id) => treeData.nodes.get(id))
274
+ .filter(isNonNullable)
275
+ .forEach((c) => buildDependencyNodeChildren(c, treeData.nodes));
276
+ }
277
+ setTreeData({ ...treeData });
278
+ };
279
+ const onNodeSelect = (node: DependencyTreeNodeData): void => {
280
+ buildDependencyNodeChildren(node, treeData.nodes);
281
+ onNodeExpand(node);
282
+ setTreeData({ ...treeData });
283
+ };
284
+
285
+ const getChildNodes = (
286
+ node: DependencyTreeNodeData,
287
+ ): DependencyTreeNodeData[] => {
288
+ if (!node.childrenIds || node.childrenIds.length === 0) {
289
+ return [];
290
+ }
291
+ const childrenNodes = node.childrenIds
292
+ .map((id) => treeData.nodes.get(id))
293
+ .filter(isNonNullable);
294
+ return childrenNodes;
295
+ };
296
+ return (
297
+ <TreeView
298
+ components={{
299
+ TreeNodeContainer: DependencyTreeNodeContainer,
300
+ }}
301
+ treeData={treeData}
302
+ getChildNodes={getChildNodes}
303
+ onNodeSelect={onNodeSelect}
304
+ innerProps={{
305
+ onNodeExpand,
306
+ }}
307
+ />
308
+ );
309
+ };
310
+
311
+ const ProjectDependencyExplorer = observer(
312
+ (props: { configState: ProjectConfigurationEditorState }) => {
313
+ const { configState } = props;
314
+ const closeModal = (): void =>
315
+ configState.setDependencyTreeReportModal(false);
316
+ const [viewAsTree, setViewAsTree] = useState(true);
317
+ const setTreeData = (treeData: TreeData<DependencyTreeNodeData>): void => {
318
+ if (viewAsTree) {
319
+ configState.setDependencyTreeData(treeData);
320
+ } else {
321
+ configState.setFlattenDependencyTreeData(treeData);
322
+ }
323
+ };
324
+ const toggleViewAsListOrAsTree = (): void => {
325
+ setViewAsTree(!viewAsTree);
326
+ };
327
+ const treeData = viewAsTree
328
+ ? configState.dependencyTreeData
329
+ : configState.flattenDependencyTreeData;
330
+ const collapseTree = (): void => {
331
+ if (treeData) {
332
+ Array.from(treeData.nodes.values()).forEach((node) => {
333
+ node.isOpen = false;
334
+ });
335
+ }
336
+ };
337
+ return (
338
+ <Dialog
339
+ open={Boolean(configState.dependencyTreeReportModal)}
340
+ onClose={closeModal}
341
+ classes={{
342
+ root: 'editor-modal__root-container',
343
+ container: 'editor-modal__container',
344
+ paper: 'editor-modal__content',
345
+ }}
346
+ >
347
+ <Modal darkMode={true} className="editor-modal">
348
+ <ModalHeader title="Dependency Explorer" />
349
+ <ModalBody>
350
+ <div className="panel project-dependency-explorer">
351
+ <div className="panel__header">
352
+ <div className="panel__header__title">
353
+ <div className="panel__header__title__label">explorer</div>
354
+ </div>
355
+ <div className="panel__header__actions">
356
+ <button
357
+ className="panel__header__action"
358
+ onClick={collapseTree}
359
+ tabIndex={-1}
360
+ >
361
+ {viewAsTree && <CompressIcon title="Collapse Tree" />}
362
+ </button>
363
+ <div className="panel__header__action query-builder__functions-explorer__custom-icon">
364
+ {!viewAsTree ? (
365
+ <SubjectIcon
366
+ title="View as Tree"
367
+ onClick={toggleViewAsListOrAsTree}
368
+ />
369
+ ) : (
370
+ <ViewHeadlineIcon
371
+ title="View as Flatten List"
372
+ onClick={toggleViewAsListOrAsTree}
373
+ />
374
+ )}
375
+ </div>
376
+ </div>
377
+ </div>
378
+ <div className="project-dependency-explorer__content">
379
+ {treeData && (
380
+ <DependencyTreeView
381
+ configState={configState}
382
+ treeData={treeData}
383
+ setTreeData={setTreeData}
384
+ />
385
+ )}
386
+ </div>
387
+ </div>
388
+ </ModalBody>
389
+ <ModalFooter>
390
+ <button
391
+ className="btn modal__footer__close-btn"
392
+ onClick={closeModal}
393
+ >
394
+ Close
395
+ </button>
396
+ </ModalFooter>
397
+ </Modal>
398
+ </Dialog>
399
+ );
400
+ },
401
+ );
402
+
403
+ export const getConflictsString = (
404
+ report: ProjectDependencyGraphReport,
405
+ ): string =>
406
+ Array.from(report.conflictPaths.entries())
407
+ .map(([k, conflictVersionPaths]) => {
408
+ const base = `project:\n${
409
+ ' '.repeat(TAB_SIZE) +
410
+ generateGAVCoordinates(k.groupId, k.artifactId, undefined)
411
+ }`;
412
+ const versionConflictString = conflictVersionPaths
413
+ .map((conflictVersion) => {
414
+ const versions = `version: ${conflictVersion.versionNode.versionId}\n`;
415
+ const paths = `paths:\n${conflictVersion.paths
416
+ .map(
417
+ (p, idx) =>
418
+ `${' '.repeat(TAB_SIZE) + (idx + 1)}:\n${p
419
+ .map((l) => l.id)
420
+ .join('>')}`,
421
+ )
422
+ .join('\n')}`;
423
+ return versions + paths;
424
+ })
425
+ .join('\n');
426
+
427
+ return `${base}\n${versionConflictString}`;
428
+ })
429
+ .join('\n\n');
430
+
431
+ const ProjectDependencyConflictModal = observer(
432
+ (props: { configState: ProjectConfigurationEditorState }) => {
433
+ const { configState } = props;
434
+ const report = configState.dependencyReport;
435
+ const closeModal = (): void =>
436
+ configState.setDependencyConflictModal(false);
437
+ return (
438
+ <Dialog
439
+ open={Boolean(configState.dependencyConflictModal)}
440
+ onClose={closeModal}
441
+ classes={{
442
+ root: 'editor-modal__root-container',
443
+ container: 'editor-modal__container',
444
+ paper: 'editor-modal__content',
445
+ }}
446
+ >
447
+ <Modal darkMode={true} className="editor-modal">
448
+ <ModalHeader title="Conflict Viewer" />
449
+ <ModalBody>
450
+ {report && (
451
+ <TextInputEditor
452
+ inputValue={getConflictsString(report)}
453
+ isReadOnly={true}
454
+ language={EDITOR_LANGUAGE.TEXT}
455
+ showMiniMap={true}
456
+ />
457
+ )}
458
+ </ModalBody>
459
+ <ModalFooter>
460
+ <button
461
+ className="btn modal__footer__close-btn"
462
+ onClick={closeModal}
463
+ >
464
+ Close
465
+ </button>
466
+ </ModalFooter>
467
+ </Modal>
468
+ </Dialog>
469
+ );
470
+ },
471
+ );
472
+
473
+ const ProjectVersionDependencyEditor = observer(
474
+ (props: {
475
+ projectDependency: ProjectDependency;
476
+ deleteValue: () => void;
477
+ isReadOnly: boolean;
478
+ projects: Map<string, ProjectData>;
479
+ }) => {
480
+ // init
481
+ const { projectDependency, deleteValue, isReadOnly, projects } = props;
482
+ const projectDependencyData = projects.get(projectDependency.projectId);
483
+ const editorStore = useEditorStore();
484
+ const applicationStore = useApplicationStore();
485
+ const projectSelectorRef = useRef<SelectComponent>(null);
486
+ const versionSelectorRef = useRef<SelectComponent>(null);
487
+ const configState = editorStore.projectConfigurationEditorState;
488
+ // project
489
+ const selectedProject = configState.projects.get(
490
+ projectDependency.projectId,
491
+ );
492
+ const selectedProjectOption = selectedProject
493
+ ? buildProjectOption(selectedProject)
494
+ : null;
495
+ const projectDisabled =
496
+ !configState.associatedProjectsAndVersionsFetched ||
497
+ configState.isReadOnly;
498
+ const projectsOptions = Array.from(configState.projects.values())
499
+ .map(buildProjectOption)
500
+ .sort(compareLabelFn);
501
+ const onProjectSelectionChange = (val: ProjectOption | null): void => {
502
+ if (
503
+ (val !== null || selectedProjectOption !== null) &&
504
+ (!val ||
505
+ !selectedProjectOption ||
506
+ val.value !== selectedProjectOption.value)
507
+ ) {
508
+ projectDependency.setProjectId(val?.value.coordinates ?? '');
509
+ if (val) {
510
+ projectDependency.setVersionId(val.value.latestVersion);
511
+ flowResult(configState.fetchDependencyInfo()).catch(
512
+ applicationStore.alertUnhandledError,
513
+ );
514
+ }
515
+ }
516
+ };
517
+ // version
518
+ const version = projectDependency.versionId;
519
+ const versions = selectedProject?.versions ?? [];
520
+ let versionOptions = versions
521
+ .slice()
522
+ .sort((v1, v2) => compareSemVerVersions(v2, v1))
523
+ .map((v) => ({ value: v, label: v }));
524
+ versionOptions = [
525
+ { label: SNAPSHOT_VERSION_ALIAS, value: MASTER_SNAPSHOT_ALIAS },
526
+ ...versionOptions,
527
+ ];
528
+ const selectedVersionOption: VersionOption | null =
529
+ versionOptions.find((v) => v.value === version) ?? null;
530
+ const versionDisabled =
531
+ Boolean(!versions.length || !projectDependency.projectId.length) ||
532
+ !configState.associatedProjectsAndVersionsFetched ||
533
+ isReadOnly;
534
+
535
+ const onVersionSelectionChange = (val: VersionOption | null): void => {
536
+ if (
537
+ (val !== null || selectedVersionOption !== null) &&
538
+ (!val ||
539
+ !selectedVersionOption ||
540
+ val.value !== selectedVersionOption.value)
541
+ ) {
542
+ try {
543
+ projectDependency.setVersionId(val?.value ?? '');
544
+ flowResult(configState.fetchDependencyInfo()).catch(
545
+ applicationStore.alertUnhandledError,
546
+ );
547
+ } catch (error) {
548
+ assertErrorThrown(error);
549
+ applicationStore.log.error(
550
+ LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE),
551
+ error,
552
+ );
553
+ }
554
+ }
555
+ };
556
+ const viewProject = (): void => {
557
+ if (!projectDependency.isLegacyDependency) {
558
+ applicationStore.navigator.visitAddress(
559
+ applicationStore.navigator.generateAddress(
560
+ generateViewProjectByGAVRoute(
561
+ guaranteeNonNullable(projectDependency.groupId),
562
+ guaranteeNonNullable(projectDependency.artifactId),
563
+ projectDependency.versionId === MASTER_SNAPSHOT_ALIAS
564
+ ? SNAPSHOT_VERSION_ALIAS
565
+ : projectDependency.versionId,
566
+ ),
567
+ ),
568
+ );
569
+ }
570
+ };
571
+ // NOTE: This assumes that the dependant project is in the same studio instance as the current project
572
+ // In the future, the studio instance may be part of the project data
573
+ const viewSDLCProject = (): void => {
574
+ if (projectDependencyData) {
575
+ applicationStore.navigator.visitAddress(
576
+ applicationStore.navigator.generateAddress(
577
+ generateViewVersionRoute(projectDependencyData.projectId, version),
578
+ ),
579
+ );
580
+ }
581
+ };
582
+ const projectSelectorPlaceholder = !projectDependency.projectId.length
583
+ ? 'Choose project'
584
+ : versionDisabled
585
+ ? 'No project version found. Please create a new one.'
586
+ : 'Select version';
587
+
588
+ return (
589
+ <div className="project-dependency-editor">
590
+ <CustomSelectorInput
591
+ className="project-dependency-editor__selector"
592
+ ref={projectSelectorRef}
593
+ disabled={projectDisabled}
594
+ options={projectsOptions}
595
+ isClearable={true}
596
+ escapeClearsValue={true}
597
+ onChange={onProjectSelectionChange}
598
+ value={selectedProjectOption}
599
+ isLoading={configState.fetchingProjectVersionsState.isInProgress}
600
+ formatOptionLabel={formatOptionLabel}
601
+ darkMode={true}
602
+ />
603
+ <CustomSelectorInput
604
+ className="project-dependency-editor__selector"
605
+ ref={versionSelectorRef}
606
+ options={versionOptions}
607
+ isClearable={true}
608
+ escapeClearsValue={true}
609
+ onChange={onVersionSelectionChange}
610
+ value={selectedVersionOption}
611
+ disabled={versionDisabled}
612
+ placeholder={projectSelectorPlaceholder}
613
+ isLoading={
614
+ editorStore.projectConfigurationEditorState
615
+ .fetchingProjectVersionsState.isInProgress
616
+ }
617
+ darkMode={true}
618
+ />
619
+ <div className="project-dependency-editor__visit-project-btn">
620
+ <button
621
+ className="project-dependency-editor__visit-project-btn__btn btn--dark"
622
+ disabled={
623
+ projectDependency.isLegacyDependency ||
624
+ !selectedProject ||
625
+ !selectedVersionOption
626
+ }
627
+ onClick={viewProject}
628
+ tabIndex={-1}
629
+ title="View project"
630
+ >
631
+ <ExternalLinkSquareIcon />
632
+ </button>
633
+ <DropdownMenu
634
+ className="project-dependency-editor__visit-project-btn__dropdown-trigger btn--dark"
635
+ content={
636
+ <MenuContent>
637
+ <MenuContentItem
638
+ disabled={
639
+ projectDependency.isLegacyDependency ||
640
+ !selectedProject ||
641
+ !selectedVersionOption ||
642
+ !projectDependencyData
643
+ }
644
+ onClick={viewSDLCProject}
645
+ >
646
+ View SDLC project
647
+ </MenuContentItem>
648
+ </MenuContent>
649
+ }
650
+ >
651
+ <CaretDownIcon title="Show more options..." />
652
+ </DropdownMenu>
653
+ </div>
654
+ <button
655
+ className="project-dependency-editor__remove-btn btn--dark btn--caution"
656
+ disabled={isReadOnly}
657
+ onClick={deleteValue}
658
+ tabIndex={-1}
659
+ title="Close"
660
+ >
661
+ <TimesIcon />
662
+ </button>
663
+ </div>
664
+ );
665
+ },
666
+ );
667
+
668
+ export const ProjectDependencyEditor = observer(() => {
669
+ const editorStore = useEditorStore();
670
+ const applicationStore = useApplicationStore();
671
+ const configState = editorStore.tabManagerState.getCurrentEditorState(
672
+ ProjectConfigurationEditorState,
673
+ );
674
+ const currentProjectConfiguration = configState.currentProjectConfiguration;
675
+ const deleteProjectDependency =
676
+ (val: ProjectDependency): (() => void) =>
677
+ (): void => {
678
+ currentProjectConfiguration.deleteProjectDependency(val);
679
+ flowResult(configState.fetchDependencyInfo()).catch(
680
+ applicationStore.alertUnhandledError,
681
+ );
682
+ };
683
+ const isReadOnly = editorStore.isInViewerMode;
684
+ const isLoading =
685
+ configState.updatingConfigurationState.isInProgress ||
686
+ configState.fetchingProjectVersionsState.isInProgress ||
687
+ configState.fetchingDependencyInfoState.isInProgress;
688
+
689
+ return (
690
+ <div className="panel__content__lists">
691
+ <PanelLoadingIndicator isLoading={isLoading} />
692
+ {isLoading && (
693
+ <div className="project-dependency-editor__progress-msg">
694
+ {configState.updatingConfigurationState.isInProgress
695
+ ? `Updating configuration...`
696
+ : configState.fetchingProjectVersionsState.isInProgress
697
+ ? `Fetching dependency versions`
698
+ : 'Updating project dependency tree and potential conflicts'}
699
+ </div>
700
+ )}
701
+ <ProjectDependencyActions config={configState} />
702
+ {currentProjectConfiguration.projectDependencies.map(
703
+ (projectDependency) => (
704
+ <ProjectVersionDependencyEditor
705
+ key={projectDependency._UUID}
706
+ projectDependency={projectDependency}
707
+ deleteValue={deleteProjectDependency(projectDependency)}
708
+ isReadOnly={isReadOnly}
709
+ projects={configState.projects}
710
+ />
711
+ ),
712
+ )}
713
+ {configState.dependencyReport &&
714
+ configState.dependencyTreeData &&
715
+ configState.dependencyTreeReportModal && (
716
+ <ProjectDependencyExplorer configState={configState} />
717
+ )}
718
+ {configState.dependencyConflictModal &&
719
+ configState.dependencyReport?.conflictPaths.size && (
720
+ <ProjectDependencyConflictModal configState={configState} />
721
+ )}
722
+ </div>
723
+ );
724
+ });