@finos/legend-application-studio 28.21.3 → 28.21.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.
Files changed (66) hide show
  1. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts +1 -1
  2. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
  3. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +3 -3
  4. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
  5. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts +3 -0
  6. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
  7. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +72 -52
  8. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
  9. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  10. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +22 -1
  11. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  12. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts +23 -0
  13. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts.map +1 -0
  14. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +267 -0
  15. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -0
  16. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.d.ts.map +1 -1
  17. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js +113 -75
  18. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js.map +1 -1
  19. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
  20. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +39 -5
  21. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
  22. package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
  23. package/lib/components/editor/side-bar/DevMetadataPanel.js +37 -6
  24. package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
  25. package/lib/index.css +2 -2
  26. package/lib/index.css.map +1 -1
  27. package/lib/package.json +1 -1
  28. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +17 -2
  29. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
  30. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +56 -49
  31. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
  32. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +4 -1
  33. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
  34. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +4 -0
  35. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
  36. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +113 -0
  37. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -0
  38. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +647 -0
  39. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -0
  40. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts +18 -4
  41. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
  42. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +214 -53
  43. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
  44. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts +17 -1
  45. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts.map +1 -1
  46. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js +46 -1
  47. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js.map +1 -1
  48. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +9 -0
  49. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
  50. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +55 -0
  51. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
  52. package/package.json +16 -16
  53. package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +3 -0
  54. package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +331 -231
  55. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +32 -0
  56. package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +935 -0
  57. package/src/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.tsx +425 -308
  58. package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +160 -15
  59. package/src/components/editor/side-bar/DevMetadataPanel.tsx +194 -10
  60. package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +82 -51
  61. package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +4 -0
  62. package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +927 -0
  63. package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +303 -72
  64. package/src/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.ts +66 -0
  65. package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +76 -0
  66. package/tsconfig.json +2 -0
@@ -0,0 +1,935 @@
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 { observer } from 'mobx-react-lite';
18
+ import { flowResult } from 'mobx';
19
+ import {
20
+ BlankPanelPlaceholder,
21
+ BlankPanelContent,
22
+ clsx,
23
+ ContextMenu,
24
+ CustomSelectorInput,
25
+ Dialog,
26
+ ErrorWarnIcon,
27
+ MenuContent,
28
+ MenuContentItem,
29
+ Modal,
30
+ ModalBody,
31
+ ModalFooter,
32
+ ModalFooterButton,
33
+ ModalHeader,
34
+ ModalTitle,
35
+ Panel,
36
+ PanelFormTextField,
37
+ PanelHeader,
38
+ PanelHeaderActionItem,
39
+ PanelHeaderActions,
40
+ PanelLoadingIndicator,
41
+ PlayIcon,
42
+ PlusIcon,
43
+ ResizablePanel,
44
+ ResizablePanelGroup,
45
+ ResizablePanelSplitter,
46
+ ResizablePanelSplitterLine,
47
+ RunAllIcon,
48
+ TimesIcon,
49
+ } from '@finos/legend-art';
50
+ import type { DataProductTestSuite } from '@finos/legend-graph';
51
+ import type { DataProductEditorState } from '../../../../../stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js';
52
+ import {
53
+ type DataProductTestableState,
54
+ type DataProductTestSuiteState,
55
+ type DataProductTestState,
56
+ type DataProductTestDataState,
57
+ type DataProductElementTestDataState,
58
+ } from '../../../../../stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js';
59
+ import { forwardRef, useEffect, useRef, useState } from 'react';
60
+ import { getTestableResultIcon } from '../../../side-bar/testable/GlobalTestRunner.js';
61
+ import {
62
+ TESTABLE_RESULT,
63
+ getTestableResultFromTestResult,
64
+ } from '../../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
65
+ import { RelationElementsDataEditor } from '../../data-editor/RelationElementsDataEditor.js';
66
+ import { validateTestableId } from '../../../../../stores/editor/utils/TestableUtils.js';
67
+ import { useEditorStore } from '../../../EditorStoreProvider.js';
68
+ import { guaranteeNonNullable } from '@finos/legend-shared';
69
+ import {
70
+ RenameModal,
71
+ TestAssertionEditor,
72
+ } from '../../testable/TestableSharedComponents.js';
73
+ import { testSuite_setId } from '../../../../../stores/graph-modifier/Testable_GraphModifierHelper.js';
74
+
75
+ // ──────────────────────────────────────────────────────────────────────────────
76
+ // Create Suite Modal — test name + access point (datasets are auto-inferred)
77
+ // ──────────────────────────────────────────────────────────────────────────────
78
+
79
+ interface ItemOption {
80
+ value: string;
81
+ label: string;
82
+ }
83
+
84
+ const CreateSuiteModal = observer(
85
+ (props: { testableState: DataProductTestableState; onClose: () => void }) => {
86
+ const { testableState, onClose } = props;
87
+ const editorStore = useEditorStore();
88
+ const applicationStore = editorStore.applicationStore;
89
+ const inputRef = useRef<HTMLInputElement>(null);
90
+
91
+ const [testName, setTestName] = useState<string | undefined>(undefined);
92
+ const [selectedAccessPointId, setSelectedAccessPointId] = useState<
93
+ string | undefined
94
+ >(undefined);
95
+
96
+ // Auto-generate suite name
97
+ const existingIds = testableState.dataProduct.tests.map((s) => s.id);
98
+ const generateSuiteName = (): string => {
99
+ let idx = 1;
100
+ while (existingIds.includes(`suite_${idx}`)) {
101
+ idx++;
102
+ }
103
+ return `suite_${idx}`;
104
+ };
105
+
106
+ const testError = validateTestableId(testName, undefined);
107
+
108
+ // Access points on the current DataProduct (for the test target)
109
+ const accessPointOptions: ItemOption[] = testableState.ownAccessPoints.map(
110
+ (ap) => ({
111
+ value: ap.id,
112
+ label: ap.id,
113
+ }),
114
+ );
115
+ const selectedApOption =
116
+ accessPointOptions.find((o) => o.value === selectedAccessPointId) ?? null;
117
+
118
+ const isValid = testName && !testError && selectedAccessPointId;
119
+
120
+ const create = (): void => {
121
+ if (!testName || !selectedAccessPointId) {
122
+ return;
123
+ }
124
+ flowResult(
125
+ testableState.createSuite(
126
+ generateSuiteName(),
127
+ testName,
128
+ selectedAccessPointId,
129
+ ),
130
+ )
131
+ .then((err) => {
132
+ if (err) {
133
+ applicationStore.notificationService.notifyError(err);
134
+ } else {
135
+ onClose();
136
+ }
137
+ })
138
+ .catch(applicationStore.alertUnhandledError);
139
+ };
140
+
141
+ return (
142
+ <Dialog
143
+ open={true}
144
+ onClose={onClose}
145
+ classes={{ container: 'search-modal__container' }}
146
+ slotProps={{
147
+ transition: { onEnter: () => inputRef.current?.focus() },
148
+ paper: { classes: { root: 'search-modal__inner-container' } },
149
+ }}
150
+ >
151
+ <Modal
152
+ darkMode={
153
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
154
+ }
155
+ >
156
+ <ModalHeader>
157
+ <ModalTitle title="Create Test Suite" />
158
+ </ModalHeader>
159
+ <ModalBody>
160
+ <PanelFormTextField
161
+ ref={inputRef}
162
+ name="Test Name"
163
+ prompt="Name for the first test in this suite"
164
+ placeholder="e.g. test_1"
165
+ value={testName}
166
+ update={(value): void => setTestName(value ?? '')}
167
+ errorMessage={testError}
168
+ />
169
+ <div className="panel__content__form__section">
170
+ <div className="panel__content__form__section__header__label">
171
+ Access Point to Test
172
+ </div>
173
+ <div className="panel__content__form__section__header__prompt">
174
+ Select the access point of the current DataProduct that the
175
+ first test in this suite will verify
176
+ </div>
177
+ <CustomSelectorInput
178
+ options={accessPointOptions}
179
+ onChange={(opt: ItemOption | null): void =>
180
+ setSelectedAccessPointId(opt?.value)
181
+ }
182
+ value={selectedApOption}
183
+ placeholder="Select access point..."
184
+ isClearable={false}
185
+ darkMode={true}
186
+ disabled={accessPointOptions.length === 0}
187
+ />
188
+ </div>
189
+ </ModalBody>
190
+ <ModalFooter>
191
+ <ModalFooterButton
192
+ disabled={!isValid}
193
+ title={!isValid ? 'Fill in all required fields' : 'Create Suite'}
194
+ onClick={create}
195
+ text="Create"
196
+ />
197
+ <ModalFooterButton
198
+ onClick={onClose}
199
+ text="Close"
200
+ type="secondary"
201
+ />
202
+ </ModalFooter>
203
+ </Modal>
204
+ </Dialog>
205
+ );
206
+ },
207
+ );
208
+
209
+ // ──────────────────────────────────────────────────────────────────────────────
210
+ // Create Test Modal — asks for test name + access point to test
211
+ // ──────────────────────────────────────────────────────────────────────────────
212
+
213
+ const CreateTestModal = observer(
214
+ (props: { suiteState: DataProductTestSuiteState; onClose: () => void }) => {
215
+ const { suiteState, onClose } = props;
216
+ const editorStore = suiteState.editorStore;
217
+ const applicationStore = editorStore.applicationStore;
218
+ const inputRef = useRef<HTMLInputElement>(null);
219
+
220
+ const existingIds = suiteState.suite.tests.map((t) => t.id);
221
+ const [testName, setTestName] = useState<string | undefined>(undefined);
222
+ const [selectedAccessPointId, setSelectedAccessPointId] = useState<
223
+ string | undefined
224
+ >(undefined);
225
+ const testNameError = validateTestableId(testName, existingIds);
226
+
227
+ const accessPointOptions: ItemOption[] =
228
+ suiteState.testableState.ownAccessPoints.map((ap) => ({
229
+ value: ap.id,
230
+ label: ap.id,
231
+ }));
232
+ const selectedApOption =
233
+ accessPointOptions.find((o) => o.value === selectedAccessPointId) ?? null;
234
+
235
+ const isValid = testName && !testNameError && selectedAccessPointId;
236
+
237
+ const create = (): void => {
238
+ if (!testName || !selectedAccessPointId) {
239
+ return;
240
+ }
241
+ flowResult(suiteState.addNewTest(testName, selectedAccessPointId))
242
+ .then((err) => {
243
+ if (err) {
244
+ applicationStore.notificationService.notifyError(err);
245
+ } else {
246
+ onClose();
247
+ }
248
+ })
249
+ .catch(applicationStore.alertUnhandledError);
250
+ };
251
+
252
+ return (
253
+ <Dialog
254
+ open={true}
255
+ onClose={onClose}
256
+ classes={{ container: 'search-modal__container' }}
257
+ slotProps={{
258
+ transition: { onEnter: () => inputRef.current?.focus() },
259
+ paper: { classes: { root: 'search-modal__inner-container' } },
260
+ }}
261
+ >
262
+ <Modal
263
+ darkMode={
264
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
265
+ }
266
+ >
267
+ <ModalHeader>
268
+ <ModalTitle title={`Add Test to "${suiteState.suite.id}"`} />
269
+ </ModalHeader>
270
+ <ModalBody>
271
+ <PanelFormTextField
272
+ ref={inputRef}
273
+ name="Test Name"
274
+ prompt="Unique identifier for the test"
275
+ placeholder="e.g. test_1"
276
+ value={testName}
277
+ update={(value): void => setTestName(value ?? '')}
278
+ errorMessage={testNameError}
279
+ />
280
+ <div className="panel__content__form__section">
281
+ <div className="panel__content__form__section__header__label">
282
+ Access Point to Test
283
+ </div>
284
+ <div className="panel__content__form__section__header__prompt">
285
+ Select which access point of the DataProduct this test will
286
+ verify
287
+ </div>
288
+ <CustomSelectorInput
289
+ options={accessPointOptions}
290
+ onChange={(opt: ItemOption | null): void =>
291
+ setSelectedAccessPointId(opt?.value)
292
+ }
293
+ value={selectedApOption}
294
+ placeholder="Select access point..."
295
+ isClearable={false}
296
+ darkMode={true}
297
+ disabled={accessPointOptions.length === 0}
298
+ />
299
+ </div>
300
+ </ModalBody>
301
+ <ModalFooter>
302
+ <ModalFooterButton
303
+ disabled={!isValid}
304
+ title={!isValid ? 'Fill in all required fields' : 'Create Test'}
305
+ onClick={create}
306
+ text="Create"
307
+ />
308
+ <ModalFooterButton
309
+ onClick={onClose}
310
+ text="Close"
311
+ type="secondary"
312
+ />
313
+ </ModalFooter>
314
+ </Modal>
315
+ </Dialog>
316
+ );
317
+ },
318
+ );
319
+
320
+ // ──────────────────────────────────────────────────────────────────────────────
321
+ // Element Test Data Item (sidebar list row in test data panel)
322
+ // ──────────────────────────────────────────────────────────────────────────────
323
+
324
+ const ElementTestDataItem = observer(
325
+ (props: {
326
+ elementState: DataProductElementTestDataState;
327
+ testDataState: DataProductTestDataState;
328
+ isReadOnly: boolean;
329
+ }) => {
330
+ const { elementState, testDataState, isReadOnly } = props;
331
+ const isActive =
332
+ testDataState.selectedElementTestDataState === elementState;
333
+
334
+ const select = (): void =>
335
+ testDataState.setSelectedElementTestDataState(elementState);
336
+
337
+ return (
338
+ <div
339
+ className={clsx('testable-test-explorer__item', {
340
+ 'testable-test-explorer__item--active': isActive,
341
+ })}
342
+ >
343
+ <div
344
+ className="testable-test-explorer__item__label"
345
+ onClick={select}
346
+ tabIndex={-1}
347
+ >
348
+ <div className="testable-test-explorer__item__label__text">
349
+ <span title={elementState.element.path}>
350
+ {elementState.element.name}
351
+ </span>
352
+ </div>
353
+ {!isReadOnly && (
354
+ <div className="mapping-test-explorer__item__actions">
355
+ <button
356
+ className="mapping-test-explorer__item__action"
357
+ onClick={(e): void => {
358
+ e.stopPropagation();
359
+ testDataState.deleteElement(elementState);
360
+ }}
361
+ tabIndex={-1}
362
+ title="Delete"
363
+ >
364
+ <TimesIcon />
365
+ </button>
366
+ </div>
367
+ )}
368
+ </div>
369
+ </div>
370
+ );
371
+ },
372
+ );
373
+
374
+ const ElementTestDataEditor = observer(
375
+ (props: {
376
+ elementState: DataProductElementTestDataState;
377
+ isReadOnly: boolean;
378
+ }) => {
379
+ const { elementState, isReadOnly } = props;
380
+ const dataState = elementState.relationElementsDataState;
381
+
382
+ if (!dataState) {
383
+ return (
384
+ <BlankPanelContent>No relation data for this element</BlankPanelContent>
385
+ );
386
+ }
387
+
388
+ return (
389
+ <RelationElementsDataEditor
390
+ dataState={dataState}
391
+ isReadOnly={isReadOnly}
392
+ hideColumnDefinitions={true}
393
+ />
394
+ );
395
+ },
396
+ );
397
+
398
+ // ──────────────────────────────────────────────────────────────────────────────
399
+ // Test Data Editor (top panel) — always-visible elements sidebar + per-element editor
400
+ // ──────────────────────────────────────────────────────────────────────────────
401
+
402
+ const AddElementModal = observer(
403
+ (props: { testDataState: DataProductTestDataState }) => {
404
+ const { testDataState } = props;
405
+ const applicationStore = testDataState.editorStore.applicationStore;
406
+ const options = testDataState.availableElementsToAdd.map((e) => ({
407
+ value: e.path,
408
+ label: e.path,
409
+ }));
410
+ const [selectedPath, setSelectedPath] = useState<string | undefined>(
411
+ options[0]?.value,
412
+ );
413
+ const close = (): void => testDataState.setShowAddElementModal(false);
414
+ const add = (): void => {
415
+ if (selectedPath) {
416
+ testDataState.addElement(selectedPath);
417
+ close();
418
+ }
419
+ };
420
+ const onChange = (val: { label: string; value: string } | null): void => {
421
+ setSelectedPath(val?.value);
422
+ };
423
+
424
+ return (
425
+ <Dialog
426
+ open={testDataState.showAddElementModal}
427
+ onClose={close}
428
+ classes={{ container: 'search-modal__container' }}
429
+ slotProps={{
430
+ paper: {
431
+ classes: { root: 'search-modal__inner-container' },
432
+ },
433
+ }}
434
+ >
435
+ <Modal
436
+ darkMode={
437
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
438
+ }
439
+ >
440
+ <ModalHeader>
441
+ <ModalTitle title="Add Element" />
442
+ </ModalHeader>
443
+ <ModalBody>
444
+ <CustomSelectorInput
445
+ className="panel__content__form__section__dropdown"
446
+ options={options}
447
+ onChange={onChange}
448
+ value={
449
+ selectedPath
450
+ ? { value: selectedPath, label: selectedPath }
451
+ : null
452
+ }
453
+ placeholder="Select element..."
454
+ darkMode={
455
+ !applicationStore.layoutService
456
+ .TEMPORARY__isLightColorThemeEnabled
457
+ }
458
+ />
459
+ </ModalBody>
460
+ <ModalFooter>
461
+ <ModalFooterButton
462
+ disabled={!selectedPath}
463
+ onClick={add}
464
+ text="Add"
465
+ />
466
+ <ModalFooterButton onClick={close} text="Close" type="secondary" />
467
+ </ModalFooter>
468
+ </Modal>
469
+ </Dialog>
470
+ );
471
+ },
472
+ );
473
+
474
+ const DataProductTestDataEditor = observer(
475
+ (props: { testDataState: DataProductTestDataState; isReadOnly: boolean }) => {
476
+ const { testDataState, isReadOnly } = props;
477
+
478
+ const addElement = (): void => {
479
+ if (testDataState.availableElementsToAdd.length === 0) {
480
+ testDataState.editorStore.applicationStore.notificationService.notifyWarning(
481
+ 'No elements available to add',
482
+ );
483
+ return;
484
+ }
485
+ testDataState.setShowAddElementModal(true);
486
+ };
487
+
488
+ const hasTestData = testDataState.elementTestDataStates.length > 0;
489
+
490
+ return (
491
+ <div
492
+ className={clsx('service-test-data-editor panel', {
493
+ 'service-test-data-editor--no-data': !hasTestData,
494
+ })}
495
+ >
496
+ <div className="service-test-data-editor__data">
497
+ <ResizablePanelGroup orientation="vertical">
498
+ {/* Left: elements list — always visible */}
499
+ <ResizablePanel minSize={100} size={180}>
500
+ <div className="binding-editor__header">
501
+ <div className="binding-editor__header__title">
502
+ <div className="panel__header__title__content">Test Data</div>
503
+ </div>
504
+ {!isReadOnly && (
505
+ <div className="panel__header__actions">
506
+ <button
507
+ className="panel__header__action"
508
+ tabIndex={-1}
509
+ onClick={addElement}
510
+ title="Add Element"
511
+ >
512
+ <PlusIcon />
513
+ </button>
514
+ </div>
515
+ )}
516
+ </div>
517
+ {!hasTestData ? (
518
+ <div className="service-test-data-editor__warning">
519
+ <ErrorWarnIcon />
520
+ <span>Add an element to configure test data</span>
521
+ </div>
522
+ ) : (
523
+ <div>
524
+ {testDataState.elementTestDataStates.map((elementState) => (
525
+ <ElementTestDataItem
526
+ key={elementState.element.path}
527
+ elementState={elementState}
528
+ testDataState={testDataState}
529
+ isReadOnly={isReadOnly}
530
+ />
531
+ ))}
532
+ </div>
533
+ )}
534
+ </ResizablePanel>
535
+ <ResizablePanelSplitter>
536
+ <ResizablePanelSplitterLine color="var(--color-dark-grey-200)" />
537
+ </ResizablePanelSplitter>
538
+ {/* Right: per-element dataset tabs + CSV editor */}
539
+ <ResizablePanel minSize={200}>
540
+ {testDataState.selectedElementTestDataState ? (
541
+ <ElementTestDataEditor
542
+ elementState={testDataState.selectedElementTestDataState}
543
+ isReadOnly={isReadOnly}
544
+ />
545
+ ) : (
546
+ <BlankPanelContent>
547
+ Select an element to configure its test data
548
+ </BlankPanelContent>
549
+ )}
550
+ </ResizablePanel>
551
+ </ResizablePanelGroup>
552
+ </div>
553
+ {testDataState.showAddElementModal && (
554
+ <AddElementModal testDataState={testDataState} />
555
+ )}
556
+ </div>
557
+ );
558
+ },
559
+ );
560
+
561
+ // ──────────────────────────────────────────────────────────────────────────────
562
+ // Test Item (inside tests panel)
563
+ // ──────────────────────────────────────────────────────────────────────────────
564
+
565
+ const TestItem = observer(
566
+ (props: {
567
+ testState: DataProductTestState;
568
+ suiteState: DataProductTestSuiteState;
569
+ isReadOnly: boolean;
570
+ }) => {
571
+ const { testState, suiteState, isReadOnly } = props;
572
+ const isActive = suiteState.selectTestState === testState;
573
+ const isRunning = testState.runningTestAction.isInProgress;
574
+ const _testableResult = getTestableResultFromTestResult(
575
+ testState.testResultState.result,
576
+ );
577
+ const testResult = isRunning
578
+ ? TESTABLE_RESULT.IN_PROGRESS
579
+ : _testableResult;
580
+
581
+ const select = (): void => suiteState.changeTest(testState.test);
582
+ const runTest = (): void => {
583
+ flowResult(testState.runTest()).catch(
584
+ testState.editorStore.applicationStore.alertUnhandledError,
585
+ );
586
+ };
587
+ const deleteTest = (): void => {
588
+ if (!isReadOnly) {
589
+ suiteState.deleteTest(testState.test);
590
+ }
591
+ };
592
+
593
+ return (
594
+ <div
595
+ className={clsx('testable-test-explorer__item', {
596
+ 'testable-test-explorer__item--active': isActive,
597
+ })}
598
+ >
599
+ <div
600
+ className="testable-test-explorer__item__label"
601
+ onClick={select}
602
+ tabIndex={-1}
603
+ >
604
+ <div className="testable-test-explorer__item__label__icon">
605
+ {getTestableResultIcon(testResult)}
606
+ </div>
607
+ <div className="testable-test-explorer__item__label__text">
608
+ {testState.test.id}
609
+ </div>
610
+ </div>
611
+ <div className="mapping-test-explorer__item__actions">
612
+ <button
613
+ className="mapping-test-explorer__item__action mapping-test-explorer__run-test-btn"
614
+ onClick={runTest}
615
+ disabled={isRunning}
616
+ tabIndex={-1}
617
+ title={`Run test ${testState.test.id}`}
618
+ >
619
+ <PlayIcon />
620
+ </button>
621
+ {!isReadOnly && (
622
+ <button
623
+ className="mapping-test-explorer__item__action mapping-test-explorer__run-test-btn"
624
+ onClick={deleteTest}
625
+ tabIndex={-1}
626
+ title={`Delete test ${testState.test.id}`}
627
+ >
628
+ <TimesIcon />
629
+ </button>
630
+ )}
631
+ </div>
632
+ </div>
633
+ );
634
+ },
635
+ );
636
+
637
+ // ──────────────────────────────────────────────────────────────────────────────
638
+ // Test Editor — shows SETUP (input data) and ASSERTION (expected + result) tabs
639
+ // ──────────────────────────────────────────────────────────────────────────────
640
+
641
+ const DataProductTestEditor = observer(
642
+ (props: { testState: DataProductTestState; isReadOnly: boolean }) => {
643
+ const { testState } = props;
644
+ const selectedAssertion = testState.selectedAsertionState;
645
+
646
+ return (
647
+ <div className="function-test-editor panel">
648
+ <div className="panel__header">
649
+ <div className="panel__header service-test-editor__header--with-tabs">
650
+ <div className="panel__header__title__content">Assertion</div>
651
+ </div>
652
+ </div>
653
+ <div className="panel">
654
+ {selectedAssertion && (
655
+ <TestAssertionEditor testAssertionState={selectedAssertion} />
656
+ )}
657
+ {!selectedAssertion && (
658
+ <BlankPanelPlaceholder
659
+ text="No assertion"
660
+ tooltipText="No assertion configured for this test"
661
+ />
662
+ )}
663
+ </div>
664
+ </div>
665
+ );
666
+ },
667
+ );
668
+
669
+ // ──────────────────────────────────────────────────────────────────────────────
670
+ // Tests Editor (bottom panel) — tests list + test detail
671
+ // ──────────────────────────────────────────────────────────────────────────────
672
+
673
+ const DataProductTestsEditor = observer(
674
+ (props: {
675
+ suiteState: DataProductTestSuiteState;
676
+ testableState: DataProductTestableState;
677
+ isReadOnly: boolean;
678
+ }) => {
679
+ const { suiteState, testableState, isReadOnly } = props;
680
+ const selectedTest = suiteState.selectTestState;
681
+
682
+ return (
683
+ <div className="panel service-test-editor">
684
+ <div className="service-test-editor__content">
685
+ <ResizablePanelGroup orientation="vertical">
686
+ <ResizablePanel minSize={100} size={200}>
687
+ <div className="binding-editor__header">
688
+ <div className="binding-editor__header__title">
689
+ <div className="panel__header__title__content">Tests</div>
690
+ </div>
691
+ <div className="panel__header__actions">
692
+ <button
693
+ className="panel__header__action testable-test-explorer__play__all__icon"
694
+ tabIndex={-1}
695
+ onClick={(): void => {
696
+ flowResult(suiteState.runSuite()).catch(
697
+ testableState.editorStore.applicationStore
698
+ .alertUnhandledError,
699
+ );
700
+ }}
701
+ disabled={
702
+ suiteState.runningSuiteState.isInProgress ||
703
+ suiteState.suite.tests.length === 0
704
+ }
705
+ title="Run all tests in this suite"
706
+ >
707
+ <RunAllIcon />
708
+ </button>
709
+ {!isReadOnly && (
710
+ <button
711
+ className="panel__header__action"
712
+ tabIndex={-1}
713
+ onClick={(): void =>
714
+ testableState.setShowCreateTestModal(true)
715
+ }
716
+ title="Add test to this suite"
717
+ >
718
+ <PlusIcon />
719
+ </button>
720
+ )}
721
+ </div>
722
+ </div>
723
+ <div>
724
+ {suiteState.testStates.map((ts) => (
725
+ <TestItem
726
+ key={ts.test.id}
727
+ testState={ts}
728
+ suiteState={suiteState}
729
+ isReadOnly={isReadOnly}
730
+ />
731
+ ))}
732
+ </div>
733
+ </ResizablePanel>
734
+ <ResizablePanelSplitter>
735
+ <ResizablePanelSplitterLine color="var(--color-dark-grey-200)" />
736
+ </ResizablePanelSplitter>
737
+ <ResizablePanel minSize={56}>
738
+ {selectedTest ? (
739
+ <DataProductTestEditor
740
+ testState={selectedTest}
741
+ isReadOnly={isReadOnly}
742
+ />
743
+ ) : (
744
+ <BlankPanelPlaceholder
745
+ text="Select a test"
746
+ tooltipText="Select a test from the list above"
747
+ />
748
+ )}
749
+ </ResizablePanel>
750
+ </ResizablePanelGroup>
751
+ </div>
752
+ </div>
753
+ );
754
+ },
755
+ );
756
+
757
+ // ──────────────────────────────────────────────────────────────────────────────
758
+ // Suite Editor — horizontal split: test data (top) + tests (bottom)
759
+ // ──────────────────────────────────────────────────────────────────────────────
760
+
761
+ const DataProductTestSuiteEditor = observer(
762
+ (props: { suiteState: DataProductTestSuiteState }) => {
763
+ const { suiteState } = props;
764
+ const testableState = suiteState.testableState;
765
+ const isReadOnly = testableState.dataProductEditorState.isReadOnly;
766
+
767
+ return (
768
+ <div className="service-test-suite-editor">
769
+ <ResizablePanelGroup orientation="horizontal">
770
+ <ResizablePanel size={580} minSize={28}>
771
+ <DataProductTestDataEditor
772
+ testDataState={suiteState.testDataState}
773
+ isReadOnly={isReadOnly}
774
+ />
775
+ </ResizablePanel>
776
+ <ResizablePanelSplitter>
777
+ <ResizablePanelSplitterLine color="var(--color-dark-grey-200)" />
778
+ </ResizablePanelSplitter>
779
+ <ResizablePanel minSize={56}>
780
+ <DataProductTestsEditor
781
+ suiteState={suiteState}
782
+ testableState={testableState}
783
+ isReadOnly={isReadOnly}
784
+ />
785
+ </ResizablePanel>
786
+ </ResizablePanelGroup>
787
+ </div>
788
+ );
789
+ },
790
+ );
791
+
792
+ // ──────────────────────────────────────────────────────────────────────────────
793
+ // Suite Tab Context Menu (rename/delete)
794
+ // ──────────────────────────────────────────────────────────────────────────────
795
+
796
+ const SuiteHeaderTabContextMenu = observer(
797
+ forwardRef<
798
+ HTMLDivElement,
799
+ {
800
+ testSuite: DataProductTestSuite;
801
+ testableState: DataProductTestableState;
802
+ }
803
+ >(function SuiteHeaderTabContextMenu(props, ref) {
804
+ const { testSuite, testableState } = props;
805
+ const deleteSuite = (): void => {
806
+ const suiteState = testableState.suiteStates.find(
807
+ (s) => s.suite === testSuite,
808
+ );
809
+ if (suiteState) {
810
+ testableState.deleteSuite(suiteState);
811
+ }
812
+ };
813
+ const rename = (): void => testableState.setSuiteToRename(testSuite);
814
+
815
+ return (
816
+ <MenuContent ref={ref}>
817
+ <MenuContentItem onClick={rename}>Rename</MenuContentItem>
818
+ <MenuContentItem onClick={deleteSuite}>Delete</MenuContentItem>
819
+ </MenuContent>
820
+ );
821
+ }),
822
+ );
823
+
824
+ // ──────────────────────────────────────────────────────────────────────────────
825
+ // Main Testing Tab — suite tabs at top, suite editor below
826
+ // ──────────────────────────────────────────────────────────────────────────────
827
+
828
+ export const DataProductTestableEditor = observer(
829
+ (props: {
830
+ dataProductEditorState: DataProductEditorState;
831
+ isReadOnly: boolean;
832
+ }) => {
833
+ const { dataProductEditorState, isReadOnly } = props;
834
+ const testableState = dataProductEditorState.testableState;
835
+ const selectedSuiteState = testableState.selectedSuiteState;
836
+ const dp = testableState.dataProduct;
837
+
838
+ useEffect(() => {
839
+ testableState.init();
840
+ }, [testableState]);
841
+
842
+ const addSuite = (): void => {
843
+ testableState.setShowCreateSuiteModal(true);
844
+ };
845
+
846
+ const changeSuite = (suite: DataProductTestSuite): void => {
847
+ testableState.changeSuite(suite);
848
+ };
849
+
850
+ const renameSuite = (val: string): void =>
851
+ testSuite_setId(guaranteeNonNullable(testableState.suiteToRename), val);
852
+
853
+ return (
854
+ <Panel className="service-test-suite-editor">
855
+ <PanelLoadingIndicator
856
+ isLoading={testableState.runningAllTestsState.isInProgress}
857
+ />
858
+
859
+ {testableState.showCreateSuiteModal && (
860
+ <CreateSuiteModal
861
+ testableState={testableState}
862
+ onClose={(): void => testableState.setShowCreateSuiteModal(false)}
863
+ />
864
+ )}
865
+
866
+ {testableState.showCreateTestModal && selectedSuiteState && (
867
+ <CreateTestModal
868
+ suiteState={selectedSuiteState}
869
+ onClose={(): void => testableState.setShowCreateTestModal(false)}
870
+ />
871
+ )}
872
+
873
+ <PanelHeader>
874
+ {dp.tests.length ? (
875
+ <PanelHeader className="service-test-suite-editor__header service-test-suite-editor__header--with-tabs">
876
+ <div className="uml-element-editor__tabs">
877
+ {dp.tests.map((suite) => (
878
+ <div
879
+ key={suite.id}
880
+ onClick={(): void => changeSuite(suite)}
881
+ className={clsx('service-test-suite-editor__tab', {
882
+ 'service-test-suite-editor__tab--active':
883
+ selectedSuiteState?.suite === suite,
884
+ })}
885
+ >
886
+ <ContextMenu
887
+ className="mapping-editor__header__tab__content"
888
+ content={
889
+ <SuiteHeaderTabContextMenu
890
+ testableState={testableState}
891
+ testSuite={suite}
892
+ />
893
+ }
894
+ >
895
+ {suite.id}
896
+ </ContextMenu>
897
+ </div>
898
+ ))}
899
+ </div>
900
+ </PanelHeader>
901
+ ) : (
902
+ <div></div>
903
+ )}
904
+ <PanelHeaderActions>
905
+ <PanelHeaderActionItem onClick={addSuite} title="Add Test Suite">
906
+ <PlusIcon />
907
+ </PanelHeaderActionItem>
908
+ </PanelHeaderActions>
909
+ </PanelHeader>
910
+ <Panel className="service-test-suite-editor">
911
+ {selectedSuiteState && (
912
+ <DataProductTestSuiteEditor suiteState={selectedSuiteState} />
913
+ )}
914
+ {!dp.tests.length && (
915
+ <BlankPanelPlaceholder
916
+ text="Add Test Suite"
917
+ onClick={addSuite}
918
+ clickActionType="add"
919
+ tooltipText="Click to add test suite"
920
+ />
921
+ )}
922
+ {testableState.suiteToRename && (
923
+ <RenameModal
924
+ val={testableState.suiteToRename.id}
925
+ isReadOnly={isReadOnly}
926
+ showModal={true}
927
+ closeModal={(): void => testableState.setSuiteToRename(undefined)}
928
+ setValue={renameSuite}
929
+ />
930
+ )}
931
+ </Panel>
932
+ </Panel>
933
+ );
934
+ },
935
+ );