@finos/legend-application-studio 28.21.2 → 28.21.4

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