@finos/legend-application-studio 28.21.30 → 28.21.32

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 (71) hide show
  1. package/lib/__lib__/LegendStudioApplicationNavigationContext.d.ts +2 -1
  2. package/lib/__lib__/LegendStudioApplicationNavigationContext.d.ts.map +1 -1
  3. package/lib/__lib__/LegendStudioApplicationNavigationContext.js +1 -0
  4. package/lib/__lib__/LegendStudioApplicationNavigationContext.js.map +1 -1
  5. package/lib/components/ElementIconUtils.d.ts.map +1 -1
  6. package/lib/components/ElementIconUtils.js +2 -0
  7. package/lib/components/ElementIconUtils.js.map +1 -1
  8. package/lib/components/editor/availability-editor.css +1 -0
  9. package/lib/components/editor/availability-editor.css.map +1 -0
  10. package/lib/components/editor/editor-group/EditorGroup.d.ts.map +1 -1
  11. package/lib/components/editor/editor-group/EditorGroup.js +5 -0
  12. package/lib/components/editor/editor-group/EditorGroup.js.map +1 -1
  13. package/lib/components/editor/editor-group/availability/AvailabilityEditor.d.ts +19 -0
  14. package/lib/components/editor/editor-group/availability/AvailabilityEditor.d.ts.map +1 -0
  15. package/lib/components/editor/editor-group/availability/AvailabilityEditor.js +50 -0
  16. package/lib/components/editor/editor-group/availability/AvailabilityEditor.js.map +1 -0
  17. package/lib/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.d.ts +22 -0
  18. package/lib/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.d.ts.map +1 -0
  19. package/lib/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.js +205 -0
  20. package/lib/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.js.map +1 -0
  21. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts +5 -0
  22. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
  23. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +1 -1
  24. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
  25. package/lib/components/editor/panel-group/DevToolPanel.d.ts.map +1 -1
  26. package/lib/components/editor/panel-group/DevToolPanel.js +27 -24
  27. package/lib/components/editor/panel-group/DevToolPanel.js.map +1 -1
  28. package/lib/components/extensions/Core_LegendStudioApplicationPlugin.d.ts.map +1 -1
  29. package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js +1 -0
  30. package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js.map +1 -1
  31. package/lib/index.css +2 -2
  32. package/lib/index.css.map +1 -1
  33. package/lib/package.json +1 -1
  34. package/lib/stores/editor/EditorGraphState.d.ts.map +1 -1
  35. package/lib/stores/editor/EditorGraphState.js +4 -1
  36. package/lib/stores/editor/EditorGraphState.js.map +1 -1
  37. package/lib/stores/editor/EditorTabManagerState.d.ts.map +1 -1
  38. package/lib/stores/editor/EditorTabManagerState.js +5 -1
  39. package/lib/stores/editor/EditorTabManagerState.js.map +1 -1
  40. package/lib/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.d.ts +32 -0
  41. package/lib/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.d.ts.map +1 -0
  42. package/lib/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.js +46 -0
  43. package/lib/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.js.map +1 -0
  44. package/lib/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.d.ts +111 -0
  45. package/lib/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.d.ts.map +1 -0
  46. package/lib/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.js +425 -0
  47. package/lib/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.js.map +1 -0
  48. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +3 -0
  49. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
  50. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +8 -2
  51. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
  52. package/lib/stores/editor/utils/ModelClassifierUtils.d.ts +2 -1
  53. package/lib/stores/editor/utils/ModelClassifierUtils.d.ts.map +1 -1
  54. package/lib/stores/editor/utils/ModelClassifierUtils.js +1 -0
  55. package/lib/stores/editor/utils/ModelClassifierUtils.js.map +1 -1
  56. package/package.json +12 -12
  57. package/src/__lib__/LegendStudioApplicationNavigationContext.ts +1 -0
  58. package/src/components/ElementIconUtils.tsx +2 -0
  59. package/src/components/editor/editor-group/EditorGroup.tsx +4 -0
  60. package/src/components/editor/editor-group/availability/AvailabilityEditor.tsx +114 -0
  61. package/src/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.tsx +829 -0
  62. package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +1 -1
  63. package/src/components/editor/panel-group/DevToolPanel.tsx +61 -60
  64. package/src/components/extensions/Core_LegendStudioApplicationPlugin.tsx +1 -0
  65. package/src/stores/editor/EditorGraphState.ts +3 -0
  66. package/src/stores/editor/EditorTabManagerState.ts +4 -0
  67. package/src/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.ts +56 -0
  68. package/src/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.ts +631 -0
  69. package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +13 -3
  70. package/src/stores/editor/utils/ModelClassifierUtils.ts +1 -0
  71. package/tsconfig.json +4 -0
@@ -0,0 +1,829 @@
1
+ /**
2
+ * Copyright (c) 2026-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 { forwardRef, useRef, useState } from 'react';
20
+ import {
21
+ BlankPanelPlaceholder,
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
+ PanelContent,
36
+ PanelFormTextField,
37
+ PanelHeader,
38
+ PanelHeaderActionItem,
39
+ PanelHeaderActions,
40
+ PlayIcon,
41
+ PlusIcon,
42
+ ResizablePanel,
43
+ ResizablePanelGroup,
44
+ ResizablePanelSplitter,
45
+ ResizablePanelSplitterLine,
46
+ RunAllIcon,
47
+ RunErrorsIcon,
48
+ } from '@finos/legend-art';
49
+ import { CodeEditor } from '@finos/legend-lego/code-editor';
50
+ import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
51
+ import {
52
+ type AvailabilityBarrierTest,
53
+ type AvailabilityTestSuite,
54
+ } from '@finos/legend-graph';
55
+ import {
56
+ TESTABLE_RESULT,
57
+ getTestableResultFromTestResult,
58
+ } from '../../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
59
+ import { getTestableResultIcon } from '../../../side-bar/testable/GlobalTestRunner.js';
60
+ import {
61
+ AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
62
+ type AvailabilityTestState,
63
+ type AvailabilityTestSuiteState,
64
+ type AvailabilityTestableState,
65
+ } from '../../../../../stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.js';
66
+ import {
67
+ atomicTest_setId,
68
+ testAssertion_setId,
69
+ testSuite_setId,
70
+ } from '../../../../../stores/graph-modifier/Testable_GraphModifierHelper.js';
71
+ import {
72
+ RenameModal,
73
+ TestAssertionResultViewer,
74
+ } from '../../testable/TestableSharedComponents.js';
75
+ import { TEST_ASSERTION_TAB } from '../../../../../stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js';
76
+ import { RelationElementEditor } from '../../data-editor/RelationElementsDataEditor.js';
77
+ import { validateTestableId } from '../../../../../stores/editor/utils/TestableUtils.js';
78
+
79
+ // ─── Format helpers ─────────────────────────────────────────────────────────
80
+
81
+ interface FormatOption {
82
+ label: string;
83
+ value: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT;
84
+ }
85
+
86
+ const FORMAT_OPTIONS: FormatOption[] = Object.values(
87
+ AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
88
+ ).map((format) => ({ label: format, value: format }));
89
+
90
+ const FormatSelector = observer(
91
+ (props: {
92
+ value: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT;
93
+ onChange: (v: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT) => void;
94
+ disabled?: boolean;
95
+ darkMode?: boolean;
96
+ }) => {
97
+ const { value, onChange, disabled, darkMode } = props;
98
+ const selectedOption =
99
+ FORMAT_OPTIONS.find((option) => option.value === value) ?? null;
100
+ return (
101
+ <div className="availability-test-editor__format-selector">
102
+ <CustomSelectorInput
103
+ options={FORMAT_OPTIONS}
104
+ onChange={(option: FormatOption | null): void => {
105
+ if (option) {
106
+ onChange(option.value);
107
+ }
108
+ }}
109
+ value={selectedOption}
110
+ placeholder="Select watermark format"
111
+ isClearable={false}
112
+ darkMode={darkMode ?? true}
113
+ disabled={disabled}
114
+ />
115
+ </div>
116
+ );
117
+ },
118
+ );
119
+
120
+ // ─── Create Suite Modal ─────────────────────────────────────────────────────
121
+
122
+ const CreateSuiteModal = observer(
123
+ (props: {
124
+ testableState: AvailabilityTestableState;
125
+ onClose: () => void;
126
+ }) => {
127
+ const { testableState, onClose } = props;
128
+ const editorStore = testableState.editorState.editorStore;
129
+ const applicationStore = editorStore.applicationStore;
130
+ const inputRef = useRef<HTMLInputElement>(null);
131
+
132
+ const existingSuiteIds = testableState.availability.tests.map(
133
+ (suite) => suite.id,
134
+ );
135
+ const [suiteName, setSuiteName] = useState<string | undefined>(undefined);
136
+ const [testName, setTestName] = useState<string | undefined>(undefined);
137
+ const [format, setFormat] =
138
+ useState<AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT>(
139
+ AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT,
140
+ );
141
+
142
+ const suiteError = validateTestableId(suiteName, existingSuiteIds);
143
+ const testError = validateTestableId(testName, undefined);
144
+ const isValid = Boolean(suiteName && !suiteError && testName && !testError);
145
+
146
+ const create = (): void => {
147
+ if (!suiteName || !testName) {
148
+ return;
149
+ }
150
+ try {
151
+ testableState.addSuite(format, suiteName, testName);
152
+ onClose();
153
+ } catch (err) {
154
+ applicationStore.notificationService.notifyError(
155
+ err instanceof Error ? err.message : String(err),
156
+ );
157
+ }
158
+ };
159
+
160
+ return (
161
+ <Dialog
162
+ open={true}
163
+ onClose={onClose}
164
+ classes={{ container: 'search-modal__container' }}
165
+ slotProps={{
166
+ transition: { onEnter: () => inputRef.current?.focus() },
167
+ paper: { classes: { root: 'search-modal__inner-container' } },
168
+ }}
169
+ >
170
+ <Modal
171
+ darkMode={
172
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
173
+ }
174
+ >
175
+ <ModalHeader>
176
+ <ModalTitle title="Create Test Suite" />
177
+ </ModalHeader>
178
+ <ModalBody>
179
+ <PanelFormTextField
180
+ ref={inputRef}
181
+ name="Suite Name"
182
+ prompt="Unique identifier for the test suite"
183
+ placeholder="e.g. suite_1"
184
+ value={suiteName}
185
+ update={(v): void => setSuiteName(v ?? '')}
186
+ errorMessage={suiteError}
187
+ />
188
+ <PanelFormTextField
189
+ name="Test Name"
190
+ prompt="Name for the first test in this suite"
191
+ placeholder="e.g. test_1"
192
+ value={testName}
193
+ update={(v): void => setTestName(v ?? '')}
194
+ errorMessage={testError}
195
+ />
196
+ <div className="panel__content__form__section">
197
+ <div className="panel__content__form__section__header__label">
198
+ Watermark Serialization Format
199
+ </div>
200
+ <div className="panel__content__form__section__header__prompt">
201
+ This format is fixed for the life of the test.
202
+ </div>
203
+ <FormatSelector
204
+ value={format}
205
+ onChange={setFormat}
206
+ darkMode={
207
+ !applicationStore.layoutService
208
+ .TEMPORARY__isLightColorThemeEnabled
209
+ }
210
+ />
211
+ </div>
212
+ </ModalBody>
213
+ <ModalFooter>
214
+ <ModalFooterButton
215
+ disabled={!isValid}
216
+ title={!isValid ? 'Fill in all required fields' : 'Create Suite'}
217
+ onClick={create}
218
+ text="Create"
219
+ />
220
+ <ModalFooterButton
221
+ onClick={onClose}
222
+ text="Close"
223
+ type="secondary"
224
+ />
225
+ </ModalFooter>
226
+ </Modal>
227
+ </Dialog>
228
+ );
229
+ },
230
+ );
231
+
232
+ // ─── Create Test Modal ──────────────────────────────────────────────────────
233
+
234
+ const CreateTestModal = observer(
235
+ (props: { suiteState: AvailabilityTestSuiteState; onClose: () => void }) => {
236
+ const { suiteState, onClose } = props;
237
+ const applicationStore = suiteState.editorStore.applicationStore;
238
+ const inputRef = useRef<HTMLInputElement>(null);
239
+
240
+ const existingIds = suiteState.suite.tests.map((test) => test.id);
241
+ const [testName, setTestName] = useState<string | undefined>(undefined);
242
+ const [format, setFormat] =
243
+ useState<AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT>(
244
+ AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT,
245
+ );
246
+
247
+ const testError = validateTestableId(testName, existingIds);
248
+ const isValid = Boolean(testName && !testError);
249
+
250
+ const create = (): void => {
251
+ if (!testName) {
252
+ return;
253
+ }
254
+ try {
255
+ suiteState.addTest(format, testName);
256
+ onClose();
257
+ } catch (err) {
258
+ applicationStore.notificationService.notifyError(
259
+ err instanceof Error ? err.message : String(err),
260
+ );
261
+ }
262
+ };
263
+
264
+ return (
265
+ <Dialog
266
+ open={true}
267
+ onClose={onClose}
268
+ classes={{ container: 'search-modal__container' }}
269
+ slotProps={{
270
+ transition: { onEnter: () => inputRef.current?.focus() },
271
+ paper: { classes: { root: 'search-modal__inner-container' } },
272
+ }}
273
+ >
274
+ <Modal
275
+ darkMode={
276
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
277
+ }
278
+ >
279
+ <ModalHeader>
280
+ <ModalTitle title={`Add Test to "${suiteState.suite.id}"`} />
281
+ </ModalHeader>
282
+ <ModalBody>
283
+ <PanelFormTextField
284
+ ref={inputRef}
285
+ name="Test Name"
286
+ prompt="Unique identifier for the test"
287
+ placeholder="e.g. test_1"
288
+ value={testName}
289
+ update={(v): void => setTestName(v ?? '')}
290
+ errorMessage={testError}
291
+ />
292
+ <div className="panel__content__form__section">
293
+ <div className="panel__content__form__section__header__label">
294
+ Watermark Serialization Format
295
+ </div>
296
+ <div className="panel__content__form__section__header__prompt">
297
+ This format is fixed for the life of the test.
298
+ </div>
299
+ <FormatSelector
300
+ value={format}
301
+ onChange={setFormat}
302
+ darkMode={
303
+ !applicationStore.layoutService
304
+ .TEMPORARY__isLightColorThemeEnabled
305
+ }
306
+ />
307
+ </div>
308
+ </ModalBody>
309
+ <ModalFooter>
310
+ <ModalFooterButton
311
+ disabled={!isValid}
312
+ title={!isValid ? 'Fill in all required fields' : 'Create Test'}
313
+ onClick={create}
314
+ text="Create"
315
+ />
316
+ <ModalFooterButton
317
+ onClick={onClose}
318
+ text="Close"
319
+ type="secondary"
320
+ />
321
+ </ModalFooter>
322
+ </Modal>
323
+ </Dialog>
324
+ );
325
+ },
326
+ );
327
+
328
+ // ─── Availability expected-JSON editor ──────────────────────────────────────
329
+
330
+ const ExpectedJsonEditor = observer(
331
+ (props: { testState: AvailabilityTestState }) => {
332
+ const { testState } = props;
333
+ const isReadOnly = testState.isReadOnly;
334
+ const canAdd = testState.parsedExpected !== undefined;
335
+ const [editorRefreshNonce, setEditorRefreshNonce] = useState(0);
336
+
337
+ const handleAdd = (): void => {
338
+ testState.addExpectedEntry();
339
+ // Force a lightweight rerender/remount so Monaco reflects external
340
+ // state updates immediately (without tab switching).
341
+ setEditorRefreshNonce((val) => val + 1);
342
+ };
343
+
344
+ return (
345
+ <div className="availability-assertion-editor__expected">
346
+ <div className="availability-assertion-editor__expected__body availability-assertion-editor__expected__body--code">
347
+ <CodeEditor
348
+ inputValue={testState.expectedValue}
349
+ language={CODE_EDITOR_LANGUAGE.JSON}
350
+ updateInput={(val: string): void => {
351
+ testState.setExpectedValue(val);
352
+ }}
353
+ hideGutter={true}
354
+ rightActions={
355
+ <button
356
+ tabIndex={-1}
357
+ className="code-editor__header__action"
358
+ disabled={isReadOnly || !canAdd}
359
+ onClick={handleAdd}
360
+ title={
361
+ canAdd
362
+ ? 'Add entry'
363
+ : 'Fix invalid JSON first, then add entry'
364
+ }
365
+ >
366
+ <PlusIcon />
367
+ </button>
368
+ }
369
+ key={`availability-expected-json-${String(editorRefreshNonce)}`}
370
+ />
371
+ </div>
372
+ </div>
373
+ );
374
+ },
375
+ );
376
+
377
+ const AvailabilityAssertionEditor = observer(
378
+ (props: { testState: AvailabilityTestState }) => {
379
+ const { testState } = props;
380
+ const assertionEditorState = testState.assertionEditorStates[0];
381
+ const selectedTab =
382
+ assertionEditorState?.selectedTab ?? TEST_ASSERTION_TAB.EXPECTED;
383
+ const changeTab = (tab: TEST_ASSERTION_TAB): void =>
384
+ assertionEditorState?.setSelectedTab(tab);
385
+
386
+ return (
387
+ <div className="availability-assertion-editor panel">
388
+ <PanelHeader>
389
+ <div className="availability-assertion-editor__title">
390
+ <div className="availability-assertion-editor__title__value">
391
+ {testState.test.id}
392
+ </div>
393
+ </div>
394
+ <div className="availability-assertion-editor__meta">
395
+ <div className="availability-assertion-editor__meta__label">
396
+ Format
397
+ </div>
398
+ <div className="availability-assertion-editor__meta__value">
399
+ {testState.format}
400
+ </div>
401
+ </div>
402
+ </PanelHeader>
403
+ <div className="testable-test-assertion-editor__header">
404
+ <div className="testable-test-assertion-editor__header__tabs">
405
+ {Object.values(TEST_ASSERTION_TAB).map((tab) => (
406
+ <div
407
+ key={tab}
408
+ onClick={(): void => changeTab(tab)}
409
+ className={clsx('testable-test-assertion-editor__header__tab', {
410
+ 'testable-test-assertion-editor__header__tab--active':
411
+ tab === selectedTab,
412
+ })}
413
+ >
414
+ {tab === TEST_ASSERTION_TAB.EXPECTED ? 'Expected' : 'Result'}
415
+ </div>
416
+ ))}
417
+ </div>
418
+ </div>
419
+ <PanelContent>
420
+ <div className="availability-assertion-editor__body">
421
+ {selectedTab === TEST_ASSERTION_TAB.EXPECTED && (
422
+ <ExpectedJsonEditor testState={testState} />
423
+ )}
424
+ {selectedTab === TEST_ASSERTION_TAB.RESULT &&
425
+ assertionEditorState && (
426
+ <TestAssertionResultViewer
427
+ testAssertionEditorState={assertionEditorState}
428
+ />
429
+ )}
430
+ </div>
431
+ </PanelContent>
432
+ </div>
433
+ );
434
+ },
435
+ );
436
+
437
+ // ─── Test list item ─────────────────────────────────────────────────────────
438
+
439
+ const AvailabilityTestContextMenu = observer(
440
+ forwardRef<
441
+ HTMLDivElement,
442
+ {
443
+ suiteState: AvailabilityTestSuiteState;
444
+ test: AvailabilityBarrierTest;
445
+ }
446
+ >(function AvailabilityTestContextMenu(props, ref) {
447
+ const { suiteState, test } = props;
448
+ return (
449
+ <MenuContent ref={ref}>
450
+ <MenuContentItem onClick={(): void => suiteState.setTestToRename(test)}>
451
+ Rename
452
+ </MenuContentItem>
453
+ <MenuContentItem onClick={(): void => suiteState.deleteTest(test)}>
454
+ Delete
455
+ </MenuContentItem>
456
+ </MenuContent>
457
+ );
458
+ }),
459
+ );
460
+
461
+ const AvailabilityTestItem = observer(
462
+ (props: {
463
+ suiteState: AvailabilityTestSuiteState;
464
+ testState: AvailabilityTestState;
465
+ }) => {
466
+ const { suiteState, testState } = props;
467
+ const isActive = suiteState.selectTestState === testState;
468
+ const testResult = getTestableResultFromTestResult(
469
+ testState.testResultState.result,
470
+ );
471
+ const icon = getTestableResultIcon(testResult);
472
+
473
+ return (
474
+ <ContextMenu
475
+ className={clsx('testable-test-explorer__item', {
476
+ 'testable-test-explorer__item--active': isActive,
477
+ })}
478
+ content={
479
+ <AvailabilityTestContextMenu
480
+ suiteState={suiteState}
481
+ test={testState.test}
482
+ />
483
+ }
484
+ >
485
+ <div
486
+ className="testable-test-explorer__item__label"
487
+ onClick={(): void => suiteState.changeTest(testState.test)}
488
+ tabIndex={-1}
489
+ >
490
+ <div className="testable-test-explorer__item__label__icon">
491
+ {icon}
492
+ </div>
493
+ <div className="testable-test-explorer__item__label__text">
494
+ {testState.test.id}
495
+ </div>
496
+ <div className="mapping-test-explorer__item__actions">
497
+ <button
498
+ className="mapping-test-explorer__item__action mapping-test-explorer__run-test-btn"
499
+ onClick={(event): void => {
500
+ event.stopPropagation();
501
+ flowResult(testState.runTest()).catch(
502
+ suiteState.editorStore.applicationStore.alertUnhandledError,
503
+ );
504
+ }}
505
+ tabIndex={-1}
506
+ title="Run Test"
507
+ >
508
+ <PlayIcon />
509
+ </button>
510
+ </div>
511
+ </div>
512
+ </ContextMenu>
513
+ );
514
+ },
515
+ );
516
+
517
+ // ─── Tests panel (right side) ───────────────────────────────────────────────
518
+
519
+ const AvailabilityTestsEditor = observer(
520
+ (props: {
521
+ suiteState: AvailabilityTestSuiteState;
522
+ testableState: AvailabilityTestableState;
523
+ }) => {
524
+ const { suiteState, testableState } = props;
525
+ const selectedTestState = suiteState.selectTestState;
526
+ const isReadOnly = testableState.editorState.isReadOnly;
527
+
528
+ return (
529
+ <div className="panel service-test-editor">
530
+ <ResizablePanelGroup orientation="vertical">
531
+ <ResizablePanel minSize={100} size={220}>
532
+ <div className="binding-editor__header">
533
+ <div className="binding-editor__header__title">
534
+ <div className="panel__header__title__content">Tests</div>
535
+ </div>
536
+ <div className="panel__header__actions">
537
+ <button
538
+ className="panel__header__action"
539
+ tabIndex={-1}
540
+ onClick={(): void => {
541
+ flowResult(suiteState.runSuite()).catch(
542
+ suiteState.editorStore.applicationStore
543
+ .alertUnhandledError,
544
+ );
545
+ }}
546
+ disabled={suiteState.suite.tests.length === 0}
547
+ title="Run all tests in this suite"
548
+ >
549
+ <RunAllIcon />
550
+ </button>
551
+ <button
552
+ className="panel__header__action"
553
+ tabIndex={-1}
554
+ onClick={(): void => {
555
+ flowResult(suiteState.runFailingTests()).catch(
556
+ suiteState.editorStore.applicationStore
557
+ .alertUnhandledError,
558
+ );
559
+ }}
560
+ disabled={suiteState.suite.tests.length === 0}
561
+ title="Run failing tests"
562
+ >
563
+ <RunErrorsIcon />
564
+ </button>
565
+ {!isReadOnly && (
566
+ <button
567
+ className="panel__header__action"
568
+ tabIndex={-1}
569
+ onClick={(): void =>
570
+ testableState.setShowCreateTestModal(true)
571
+ }
572
+ title="Add test to this suite"
573
+ >
574
+ <PlusIcon />
575
+ </button>
576
+ )}
577
+ </div>
578
+ </div>
579
+ <div>
580
+ {suiteState.testStates.map((testState) => (
581
+ <AvailabilityTestItem
582
+ key={testState.test.id}
583
+ suiteState={suiteState}
584
+ testState={testState}
585
+ />
586
+ ))}
587
+ </div>
588
+ </ResizablePanel>
589
+ <ResizablePanelSplitter>
590
+ <ResizablePanelSplitterLine color="var(--color-dark-grey-200)" />
591
+ </ResizablePanelSplitter>
592
+ <ResizablePanel minSize={56}>
593
+ {selectedTestState ? (
594
+ <AvailabilityAssertionEditor testState={selectedTestState} />
595
+ ) : (
596
+ <BlankPanelPlaceholder
597
+ text="Select a test"
598
+ tooltipText="Select a test from the list above"
599
+ />
600
+ )}
601
+ </ResizablePanel>
602
+ </ResizablePanelGroup>
603
+ </div>
604
+ );
605
+ },
606
+ );
607
+
608
+ // ─── Suite editor (horizontal split) ────────────────────────────────────────
609
+
610
+ const AvailabilitySuiteEditor = observer(
611
+ (props: {
612
+ suiteState: AvailabilityTestSuiteState;
613
+ testableState: AvailabilityTestableState;
614
+ }) => {
615
+ const { suiteState, testableState } = props;
616
+ const isReadOnly = testableState.editorState.isReadOnly;
617
+
618
+ return (
619
+ <div className="service-test-suite-editor">
620
+ <ResizablePanelGroup orientation="horizontal">
621
+ <ResizablePanel size={520} minSize={28}>
622
+ <div className="panel service-test-data-editor">
623
+ <div className="service-test-data-editor__data">
624
+ <RelationElementEditor
625
+ relationElementState={suiteState.testDataState}
626
+ isReadOnly={isReadOnly}
627
+ hideColumnDefinitions={true}
628
+ />
629
+ </div>
630
+ </div>
631
+ </ResizablePanel>
632
+ <ResizablePanelSplitter>
633
+ <ResizablePanelSplitterLine color="var(--color-dark-grey-200)" />
634
+ </ResizablePanelSplitter>
635
+ <ResizablePanel minSize={56}>
636
+ <AvailabilityTestsEditor
637
+ suiteState={suiteState}
638
+ testableState={testableState}
639
+ />
640
+ </ResizablePanel>
641
+ </ResizablePanelGroup>
642
+ </div>
643
+ );
644
+ },
645
+ );
646
+
647
+ // ─── Suite tab context menu ─────────────────────────────────────────────────
648
+
649
+ const AvailabilitySuiteContextMenu = observer(
650
+ forwardRef<
651
+ HTMLDivElement,
652
+ {
653
+ suite: AvailabilityTestSuite;
654
+ testableState: AvailabilityTestableState;
655
+ }
656
+ >(function AvailabilitySuiteContextMenu(props, ref) {
657
+ const { suite, testableState } = props;
658
+ return (
659
+ <MenuContent ref={ref}>
660
+ <MenuContentItem
661
+ onClick={(): void => testableState.setSuiteToRename(suite)}
662
+ >
663
+ Rename
664
+ </MenuContentItem>
665
+ <MenuContentItem onClick={(): void => testableState.deleteSuite(suite)}>
666
+ Delete
667
+ </MenuContentItem>
668
+ </MenuContent>
669
+ );
670
+ }),
671
+ );
672
+
673
+ // ─── Top-level testable editor ──────────────────────────────────────────────
674
+
675
+ export const AvailabilityTestableEditor = observer(
676
+ (props: { testableState: AvailabilityTestableState }) => {
677
+ const { testableState } = props;
678
+ const availability = testableState.availability;
679
+ const selectedSuiteState = testableState.selectedSuiteState;
680
+ const isReadOnly = testableState.editorState.isReadOnly;
681
+
682
+ const renameSuite = (val: string): void => {
683
+ if (testableState.suiteToRename) {
684
+ testSuite_setId(testableState.suiteToRename, val);
685
+ }
686
+ };
687
+
688
+ const renameTest = (val: string): void => {
689
+ const testToRename = testableState.selectedSuiteState?.testToRename;
690
+ if (testToRename) {
691
+ atomicTest_setId(testToRename, val);
692
+ }
693
+ };
694
+
695
+ const renameAssertion = (val: string): void => {
696
+ const assertionToRename =
697
+ testableState.selectedSuiteState?.selectTestState?.assertionToRename;
698
+ if (assertionToRename) {
699
+ testAssertion_setId(assertionToRename, val);
700
+ }
701
+ };
702
+
703
+ return (
704
+ <Panel className="service-test-suite-editor">
705
+ <PanelHeader>
706
+ {availability.tests.length ? (
707
+ <PanelHeader className="service-test-suite-editor__header service-test-suite-editor__header--with-tabs">
708
+ <div className="uml-element-editor__tabs">
709
+ {availability.tests.map((suite) => {
710
+ const isActive = selectedSuiteState?.suite === suite;
711
+ const suiteResult = selectedSuiteState
712
+ ? isActive
713
+ ? selectedSuiteState.result
714
+ : TESTABLE_RESULT.DID_NOT_RUN
715
+ : TESTABLE_RESULT.DID_NOT_RUN;
716
+ return (
717
+ <div
718
+ key={suite.id}
719
+ onClick={(): void => testableState.changeSuite(suite)}
720
+ className={clsx('service-test-suite-editor__tab', {
721
+ 'service-test-suite-editor__tab--active': isActive,
722
+ })}
723
+ >
724
+ <ContextMenu
725
+ className="mapping-editor__header__tab__content"
726
+ content={
727
+ <AvailabilitySuiteContextMenu
728
+ suite={suite}
729
+ testableState={testableState}
730
+ />
731
+ }
732
+ >
733
+ <div className="testable-test-explorer__item__result">
734
+ {getTestableResultIcon(suiteResult)}
735
+ </div>
736
+ {suite.id}
737
+ </ContextMenu>
738
+ </div>
739
+ );
740
+ })}
741
+ </div>
742
+ </PanelHeader>
743
+ ) : (
744
+ <div></div>
745
+ )}
746
+
747
+ <PanelHeaderActions>
748
+ {!isReadOnly && (
749
+ <PanelHeaderActionItem
750
+ onClick={(): void =>
751
+ testableState.setShowCreateSuiteModal(true)
752
+ }
753
+ title="Add Suite"
754
+ >
755
+ <PlusIcon />
756
+ </PanelHeaderActionItem>
757
+ )}
758
+ </PanelHeaderActions>
759
+ </PanelHeader>
760
+
761
+ <Panel className="service-test-suite-editor">
762
+ {selectedSuiteState ? (
763
+ <AvailabilitySuiteEditor
764
+ suiteState={selectedSuiteState}
765
+ testableState={testableState}
766
+ />
767
+ ) : (
768
+ <BlankPanelPlaceholder
769
+ text="Add Test Suite"
770
+ onClick={(): void => testableState.setShowCreateSuiteModal(true)}
771
+ clickActionType="add"
772
+ tooltipText="Click to add availability test suite"
773
+ />
774
+ )}
775
+
776
+ {testableState.showCreateSuiteModal && (
777
+ <CreateSuiteModal
778
+ testableState={testableState}
779
+ onClose={(): void => testableState.setShowCreateSuiteModal(false)}
780
+ />
781
+ )}
782
+
783
+ {testableState.showCreateTestModal && selectedSuiteState && (
784
+ <CreateTestModal
785
+ suiteState={selectedSuiteState}
786
+ onClose={(): void => testableState.setShowCreateTestModal(false)}
787
+ />
788
+ )}
789
+
790
+ {testableState.suiteToRename && (
791
+ <RenameModal
792
+ val={testableState.suiteToRename.id}
793
+ isReadOnly={isReadOnly}
794
+ showModal={true}
795
+ closeModal={(): void => testableState.setSuiteToRename(undefined)}
796
+ setValue={renameSuite}
797
+ />
798
+ )}
799
+
800
+ {selectedSuiteState?.testToRename && (
801
+ <RenameModal
802
+ val={selectedSuiteState.testToRename.id}
803
+ isReadOnly={isReadOnly}
804
+ showModal={true}
805
+ closeModal={(): void =>
806
+ selectedSuiteState.setTestToRename(undefined)
807
+ }
808
+ setValue={renameTest}
809
+ />
810
+ )}
811
+
812
+ {selectedSuiteState?.selectTestState?.assertionToRename && (
813
+ <RenameModal
814
+ val={selectedSuiteState.selectTestState.assertionToRename.id}
815
+ isReadOnly={isReadOnly}
816
+ showModal={true}
817
+ closeModal={(): void =>
818
+ selectedSuiteState.selectTestState?.setAssertionToRename(
819
+ undefined,
820
+ )
821
+ }
822
+ setValue={renameAssertion}
823
+ />
824
+ )}
825
+ </Panel>
826
+ </Panel>
827
+ );
828
+ },
829
+ );