@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,631 @@
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 {
18
+ type Availability,
19
+ AvailabilityBarrierTest,
20
+ AvailabilityTestSuite,
21
+ EqualToJson,
22
+ ExternalFormatData,
23
+ RelationElement,
24
+ RelationRowTestData,
25
+ DEFAULT_TEST_PREFIX,
26
+ DEFAULT_TEST_SUITE_PREFIX,
27
+ DEFAULT_TEST_ASSERTION_PREFIX,
28
+ observe_AvailabilityBarrierTest,
29
+ observe_AvailabilityTestSuite,
30
+ observe_EqualToJson,
31
+ observe_ExternalFormatData,
32
+ observe_RelationElement,
33
+ observe_RelationRowTestData,
34
+ } from '@finos/legend-graph';
35
+ import { action, computed, flow, makeObservable, observable } from 'mobx';
36
+ import {
37
+ addUniqueEntry,
38
+ ContentType,
39
+ generateEnumerableNameFromToken,
40
+ type GeneratorFn,
41
+ guaranteeType,
42
+ isString,
43
+ } from '@finos/legend-shared';
44
+ import type { AvailabilityEditorState } from '../AvailabilityEditorState.js';
45
+ import {
46
+ getTestableResultFromTestResults,
47
+ type TESTABLE_RESULT,
48
+ } from '../../../../sidebar-state/testable/GlobalTestRunnerState.js';
49
+ import {
50
+ TestableTestEditorState,
51
+ TestableTestSuiteEditorState,
52
+ } from '../../testable/TestableEditorState.js';
53
+ import { RelationElementState } from '../../data/EmbeddedDataState.js';
54
+ import { atomicTest_addAssertion } from '../../../../../graph-modifier/Testable_GraphModifierHelper.js';
55
+ import { externalFormatData_setData } from '../../../../../graph-modifier/DSL_Data_GraphModifierHelper.js';
56
+
57
+ // ─── Constants ──────────────────────────────────────────────────────────────
58
+
59
+ export enum AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT {
60
+ DEFAULT = 'DEFAULT',
61
+ LITE = 'LITE',
62
+ ALLOY_QUERY = 'ALLOY_QUERY',
63
+ }
64
+
65
+ const DEFAULT_RELATION_PATH = 'barrier';
66
+
67
+ // Hard-coded columns shared by every availability barrier test. These match
68
+ // the schema the engine's barrier relation exposes to the test runtime.
69
+ export const AVAILABILITY_TEST_DATA_COLUMNS = [
70
+ 'event_id',
71
+ 'batch_id',
72
+ 'ingest_path',
73
+ 'appdir_id',
74
+ 'batch_attributes',
75
+ 'ingest_completed_ts_utc',
76
+ ];
77
+
78
+ // Default fixed timestamp populated for new rows.
79
+ export const DEFAULT_INGEST_COMPLETED_TS_UTC = '2025-01-01T00:00:00Z';
80
+
81
+ const AVAILABILITY_COLUMN_DEFAULTS: Record<string, string> = {
82
+ ingest_completed_ts_utc: DEFAULT_INGEST_COMPLETED_TS_UTC,
83
+ };
84
+
85
+ // ─── Watermark expected-JSON models ─────────────────────────────────────────
86
+ // These types model the shape of the `EqualToJson.expected` payload for each
87
+ // watermark serialization format, so we can add/remove entries with typed
88
+ // mutations rather than juggling `unknown` and path-walking helpers.
89
+
90
+ type WatermarkBatch = {
91
+ batchId: number;
92
+ ingestDefinitionPath: string;
93
+ ingestDefinitionReference: { ingestDefinitionUrn: string };
94
+ };
95
+
96
+ type WatermarkExpectedDefault = {
97
+ availabilityDefinitionReference: { availabilityDefinitionUrn: string };
98
+ evaluatedWatermark: { watermarkBatches: WatermarkBatch[] };
99
+ evaluationResult: boolean;
100
+ eventId: number;
101
+ watermarkId: string;
102
+ };
103
+
104
+ type WatermarkLiteEntry = { ingestDefinitionPath: string; batchId: number };
105
+
106
+ type WatermarkAlloyQueryEntry = { batchId: number };
107
+
108
+ type WatermarkExpected =
109
+ | {
110
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT;
111
+ value: WatermarkExpectedDefault;
112
+ }
113
+ | {
114
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.LITE;
115
+ value: WatermarkLiteEntry[];
116
+ }
117
+ | {
118
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.ALLOY_QUERY;
119
+ value: Record<string, WatermarkAlloyQueryEntry>;
120
+ };
121
+
122
+ const buildWatermarkBatch = (): WatermarkBatch => ({
123
+ batchId: 0,
124
+ ingestDefinitionPath: '',
125
+ ingestDefinitionReference: { ingestDefinitionUrn: '' },
126
+ });
127
+
128
+ const buildLiteEntry = (): WatermarkLiteEntry => ({
129
+ ingestDefinitionPath: '',
130
+ batchId: 0,
131
+ });
132
+
133
+ const buildAlloyQueryEntry = (): WatermarkAlloyQueryEntry => ({ batchId: 0 });
134
+
135
+ const buildDefaultTemplate = (): WatermarkExpectedDefault => ({
136
+ availabilityDefinitionReference: { availabilityDefinitionUrn: '' },
137
+ evaluatedWatermark: { watermarkBatches: [buildWatermarkBatch()] },
138
+ evaluationResult: true,
139
+ eventId: 0,
140
+ watermarkId: '',
141
+ });
142
+
143
+ const stringifyExpected = (value: unknown): string =>
144
+ JSON.stringify(value, null, 2);
145
+
146
+ const buildExpectedTemplateString = (
147
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
148
+ ): string => {
149
+ switch (format) {
150
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.LITE:
151
+ return stringifyExpected([buildLiteEntry()]);
152
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.ALLOY_QUERY:
153
+ return stringifyExpected({ '': buildAlloyQueryEntry() });
154
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT:
155
+ default:
156
+ return stringifyExpected(buildDefaultTemplate());
157
+ }
158
+ };
159
+
160
+ // Structural validators — user edits raw JSON in Monaco, so the parsed value
161
+ // might not match the expected format. These narrow the parse result to a
162
+ // typed `WatermarkExpected`; on failure the "Add entry" action is disabled.
163
+ const isRecord = (val: unknown): val is Record<string, unknown> =>
164
+ typeof val === 'object' && val !== null && !Array.isArray(val);
165
+
166
+ const isDefaultShape = (val: unknown): val is WatermarkExpectedDefault =>
167
+ isRecord(val) &&
168
+ isRecord(val.evaluatedWatermark) &&
169
+ Array.isArray(val.evaluatedWatermark.watermarkBatches);
170
+
171
+ const isLiteShape = (val: unknown): val is WatermarkLiteEntry[] =>
172
+ Array.isArray(val);
173
+
174
+ const isAlloyQueryShape = (
175
+ val: unknown,
176
+ ): val is Record<string, WatermarkAlloyQueryEntry> => isRecord(val);
177
+
178
+ const parseWatermarkExpected = (
179
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
180
+ raw: string,
181
+ ): WatermarkExpected | undefined => {
182
+ if (!raw) {
183
+ return undefined;
184
+ }
185
+ let parsed: unknown;
186
+ try {
187
+ parsed = JSON.parse(raw);
188
+ } catch {
189
+ return undefined;
190
+ }
191
+ switch (format) {
192
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT:
193
+ return isDefaultShape(parsed) ? { format, value: parsed } : undefined;
194
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.LITE:
195
+ return isLiteShape(parsed) ? { format, value: parsed } : undefined;
196
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.ALLOY_QUERY:
197
+ return isAlloyQueryShape(parsed) ? { format, value: parsed } : undefined;
198
+ default:
199
+ return undefined;
200
+ }
201
+ };
202
+
203
+ // Append a new entry to the parsed expected model. Returns the mutated value
204
+ // serialized as pretty JSON, ready to be written back onto the assertion.
205
+ const addWatermarkEntry = (expected: WatermarkExpected): string => {
206
+ switch (expected.format) {
207
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT: {
208
+ const next: WatermarkExpectedDefault = {
209
+ ...expected.value,
210
+ evaluatedWatermark: {
211
+ watermarkBatches: [
212
+ ...expected.value.evaluatedWatermark.watermarkBatches,
213
+ buildWatermarkBatch(),
214
+ ],
215
+ },
216
+ };
217
+ return stringifyExpected(next);
218
+ }
219
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.LITE:
220
+ return stringifyExpected([...expected.value, buildLiteEntry()]);
221
+ case AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.ALLOY_QUERY: {
222
+ const next = { ...expected.value };
223
+ // Pick a fresh unique key so a second empty-string add is still visible.
224
+ let key = '';
225
+ if (Object.prototype.hasOwnProperty.call(next, key)) {
226
+ let counter = 1;
227
+ while (Object.prototype.hasOwnProperty.call(next, `path_${counter}`)) {
228
+ counter++;
229
+ }
230
+ key = `path_${counter}`;
231
+ }
232
+ next[key] = buildAlloyQueryEntry();
233
+ return stringifyExpected(next);
234
+ }
235
+ default:
236
+ return stringifyExpected(expected);
237
+ }
238
+ };
239
+
240
+ // ─── Test-data helpers ──────────────────────────────────────────────────────
241
+
242
+ const createDefaultRelationElement = (
243
+ columns: readonly string[] = AVAILABILITY_TEST_DATA_COLUMNS,
244
+ ): RelationElement => {
245
+ const element = new RelationElement();
246
+ element.paths = [DEFAULT_RELATION_PATH];
247
+ element.columns = [...columns];
248
+ const row = new RelationRowTestData();
249
+ row.values = columns.map((col) => AVAILABILITY_COLUMN_DEFAULTS[col] ?? '');
250
+ element.rows = [observe_RelationRowTestData(row)];
251
+ return observe_RelationElement(element);
252
+ };
253
+
254
+ // Return the union of columns declared across all existing suites in the
255
+ // availability, preserving first-seen order. Used to seed a new suite's
256
+ // test data so users don't have to redeclare the whole schema.
257
+ const collectExistingColumns = (
258
+ availability: Availability,
259
+ ): string[] | undefined => {
260
+ const seen = new Set<string>();
261
+ const ordered: string[] = [];
262
+ availability.tests.forEach((test) => {
263
+ if (!(test instanceof AvailabilityTestSuite)) {
264
+ return;
265
+ }
266
+ test.testData?.columns.forEach((column) => {
267
+ if (!seen.has(column)) {
268
+ seen.add(column);
269
+ ordered.push(column);
270
+ }
271
+ });
272
+ });
273
+ return ordered.length > 0 ? ordered : undefined;
274
+ };
275
+
276
+ // ─── Assertion / test / suite factories ─────────────────────────────────────
277
+
278
+ const createDefaultAssertion = (
279
+ test: AvailabilityBarrierTest,
280
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
281
+ ): EqualToJson => {
282
+ const externalFormatData = new ExternalFormatData();
283
+ externalFormatData.contentType = ContentType.APPLICATION_JSON;
284
+ externalFormatData.data = buildExpectedTemplateString(format);
285
+ const assertion = new EqualToJson();
286
+ assertion.id = generateEnumerableNameFromToken(
287
+ test.assertions.map((value) => value.id),
288
+ DEFAULT_TEST_ASSERTION_PREFIX,
289
+ );
290
+ assertion.parentTest = test;
291
+ assertion.expected = observe_ExternalFormatData(externalFormatData);
292
+ return observe_EqualToJson(assertion);
293
+ };
294
+
295
+ const createDefaultTest = (
296
+ suite: AvailabilityTestSuite,
297
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
298
+ id?: string,
299
+ ): AvailabilityBarrierTest => {
300
+ const test = new AvailabilityBarrierTest();
301
+ test.id =
302
+ id ??
303
+ generateEnumerableNameFromToken(
304
+ suite.tests.map((value) => value.id),
305
+ DEFAULT_TEST_PREFIX,
306
+ );
307
+ test.__parent = suite;
308
+ test.watermarkSerializationFormat = format;
309
+ const observedTest = observe_AvailabilityBarrierTest(test);
310
+ atomicTest_addAssertion(
311
+ observedTest,
312
+ createDefaultAssertion(observedTest, format),
313
+ );
314
+ return observedTest;
315
+ };
316
+
317
+ const createDefaultSuite = (
318
+ availability: Availability,
319
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
320
+ suiteId?: string,
321
+ testId?: string,
322
+ ): AvailabilityTestSuite => {
323
+ const suite = new AvailabilityTestSuite();
324
+ suite.id =
325
+ suiteId ??
326
+ generateEnumerableNameFromToken(
327
+ availability.tests.map((value) => value.id),
328
+ DEFAULT_TEST_SUITE_PREFIX,
329
+ );
330
+ suite.__parent = availability;
331
+ suite.testData = createDefaultRelationElement(
332
+ collectExistingColumns(availability),
333
+ );
334
+ const observedSuite = observe_AvailabilityTestSuite(suite);
335
+ addUniqueEntry(
336
+ observedSuite.tests,
337
+ createDefaultTest(observedSuite, format, testId),
338
+ );
339
+ return observedSuite;
340
+ };
341
+
342
+ // ─── Per-test state ─────────────────────────────────────────────────────────
343
+
344
+ export class AvailabilityTestState extends TestableTestEditorState {
345
+ readonly suiteState: AvailabilityTestSuiteState;
346
+ override test: AvailabilityBarrierTest;
347
+
348
+ constructor(
349
+ suiteState: AvailabilityTestSuiteState,
350
+ test: AvailabilityBarrierTest,
351
+ ) {
352
+ super(
353
+ suiteState.testableState.availability,
354
+ test,
355
+ suiteState.testableState.editorState.isReadOnly,
356
+ suiteState.editorStore,
357
+ );
358
+ makeObservable(this, {
359
+ selectedAsertionState: observable,
360
+ selectedTab: observable,
361
+ assertionToRename: observable,
362
+ assertionEditorStates: observable,
363
+ testResultState: observable,
364
+ debugTestResultState: observable,
365
+ runningTestAction: observable,
366
+ format: computed,
367
+ firstJsonAssertion: computed,
368
+ parsedExpected: computed,
369
+ setAssertionToRename: action,
370
+ setSelectedTab: action,
371
+ addAssertion: action,
372
+ deleteAssertion: action,
373
+ openAssertion: action,
374
+ handleTestResult: action,
375
+ resetResult: action,
376
+ setExpectedValue: action,
377
+ addExpectedEntry: action,
378
+ runTest: flow,
379
+ debugTest: flow,
380
+ });
381
+
382
+ this.suiteState = suiteState;
383
+ this.test = test;
384
+ }
385
+
386
+ get format(): AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT {
387
+ const raw = this.test.watermarkSerializationFormat;
388
+ if (
389
+ raw === AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.LITE ||
390
+ raw === AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.ALLOY_QUERY
391
+ ) {
392
+ return raw;
393
+ }
394
+ return AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT.DEFAULT;
395
+ }
396
+
397
+ get firstJsonAssertion(): EqualToJson | undefined {
398
+ const first = this.test.assertions[0];
399
+ return first instanceof EqualToJson ? first : undefined;
400
+ }
401
+
402
+ get expectedValue(): string {
403
+ return this.firstJsonAssertion?.expected.data ?? '';
404
+ }
405
+
406
+ setExpectedValue(val: string): void {
407
+ const assertion = this.firstJsonAssertion;
408
+ if (!assertion) {
409
+ return;
410
+ }
411
+ externalFormatData_setData(assertion.expected, val);
412
+ }
413
+
414
+ get parsedExpected(): WatermarkExpected | undefined {
415
+ return parseWatermarkExpected(this.format, this.expectedValue);
416
+ }
417
+
418
+ // Append a new entry to the expected payload. No-op if the current text
419
+ // does not parse to a valid shape for the test's format.
420
+ addExpectedEntry(): void {
421
+ const parsed = this.parsedExpected;
422
+ if (!parsed) {
423
+ return;
424
+ }
425
+ this.setExpectedValue(addWatermarkEntry(parsed));
426
+ }
427
+ }
428
+
429
+ // ─── Per-suite state ────────────────────────────────────────────────────────
430
+
431
+ export class AvailabilityTestSuiteState extends TestableTestSuiteEditorState {
432
+ readonly testableState: AvailabilityTestableState;
433
+ override suite: AvailabilityTestSuite;
434
+ override testStates: AvailabilityTestState[] = [];
435
+ override selectTestState: AvailabilityTestState | undefined;
436
+ testToRename: AvailabilityBarrierTest | undefined;
437
+ readonly testDataState: RelationElementState;
438
+
439
+ constructor(
440
+ testableState: AvailabilityTestableState,
441
+ suite: AvailabilityTestSuite,
442
+ ) {
443
+ super(
444
+ testableState.availability,
445
+ suite,
446
+ testableState.editorState.isReadOnly,
447
+ testableState.editorState.editorStore,
448
+ );
449
+ makeObservable(this, {
450
+ selectTestState: observable,
451
+ testStates: observable,
452
+ testToRename: observable,
453
+ runningSuiteState: observable,
454
+ setTestToRename: action,
455
+ changeTest: action,
456
+ addTest: action,
457
+ deleteTest: action,
458
+ runSuite: flow,
459
+ runFailingTests: flow,
460
+ });
461
+
462
+ this.testableState = testableState;
463
+ this.suite = suite;
464
+
465
+ // Ensure the suite has test data with the canonical shape (hard-coded columns).
466
+ const testData = this.suite.testData ?? createDefaultRelationElement();
467
+ this.suite.testData = testData;
468
+ // Column editing is enabled so users can add/remove columns beyond the
469
+ // seeded schema; new rows are prefilled from `columnDefaults` so the fixed
470
+ // `ingest_completed_ts_utc` value doesn't need to be re-typed.
471
+ this.testDataState = new RelationElementState(testData, {
472
+ columnDefaults: AVAILABILITY_COLUMN_DEFAULTS,
473
+ });
474
+
475
+ this.testStates = this.buildTestStates();
476
+ this.selectTestState = this.testStates[0];
477
+ }
478
+
479
+ private buildTestStates(): AvailabilityTestState[] {
480
+ return this.suite.tests
481
+ .map((value) => guaranteeType(value, AvailabilityBarrierTest))
482
+ .map((value) => new AvailabilityTestState(this, value));
483
+ }
484
+
485
+ setTestToRename(test: AvailabilityBarrierTest | undefined): void {
486
+ this.testToRename = test;
487
+ }
488
+
489
+ addTest(
490
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
491
+ id?: string,
492
+ ): AvailabilityTestState {
493
+ const test = createDefaultTest(this.suite, format, id);
494
+ addUniqueEntry(this.suite.tests, test);
495
+ const state = new AvailabilityTestState(this, test);
496
+ addUniqueEntry(this.testStates, state);
497
+ this.selectTestState = state;
498
+ return state;
499
+ }
500
+
501
+ override deleteTest(test: AvailabilityBarrierTest): void {
502
+ super.deleteTest(test);
503
+ this.selectTestState = this.testStates[0];
504
+ }
505
+
506
+ get result(): TESTABLE_RESULT {
507
+ return getTestableResultFromTestResults(
508
+ this.testStates.map((state) => state.testResultState.result),
509
+ );
510
+ }
511
+ }
512
+
513
+ // ─── Top-level testable state ───────────────────────────────────────────────
514
+
515
+ export class AvailabilityTestableState {
516
+ readonly editorState: AvailabilityEditorState;
517
+ selectedSuiteState: AvailabilityTestSuiteState | undefined;
518
+ suiteToRename: AvailabilityTestSuite | undefined;
519
+ showCreateSuiteModal = false;
520
+ showCreateTestModal = false;
521
+
522
+ constructor(editorState: AvailabilityEditorState) {
523
+ this.editorState = editorState;
524
+
525
+ makeObservable(this, {
526
+ selectedSuiteState: observable,
527
+ suiteToRename: observable,
528
+ showCreateSuiteModal: observable,
529
+ showCreateTestModal: observable,
530
+ suites: computed,
531
+ suiteCount: computed,
532
+ testCount: computed,
533
+ initSuites: action,
534
+ changeSuite: action,
535
+ addSuite: action,
536
+ setSuiteToRename: action,
537
+ setShowCreateSuiteModal: action,
538
+ setShowCreateTestModal: action,
539
+ deleteSuite: action,
540
+ runSuite: flow,
541
+ runFailingSuiteTests: flow,
542
+ });
543
+
544
+ this.initSuites();
545
+ }
546
+
547
+ get availability(): Availability {
548
+ return this.editorState.availability;
549
+ }
550
+
551
+ initSuites(): void {
552
+ const suite = this.availability.tests[0];
553
+ this.selectedSuiteState =
554
+ suite instanceof AvailabilityTestSuite
555
+ ? new AvailabilityTestSuiteState(this, suite)
556
+ : undefined;
557
+ }
558
+
559
+ setSuiteToRename(suite: AvailabilityTestSuite | undefined): void {
560
+ this.suiteToRename = suite;
561
+ }
562
+
563
+ setShowCreateSuiteModal(val: boolean): void {
564
+ this.showCreateSuiteModal = val;
565
+ }
566
+
567
+ setShowCreateTestModal(val: boolean): void {
568
+ this.showCreateTestModal = val;
569
+ }
570
+
571
+ changeSuite(suite: AvailabilityTestSuite): void {
572
+ this.selectedSuiteState = new AvailabilityTestSuiteState(this, suite);
573
+ }
574
+
575
+ addSuite(
576
+ format: AVAILABILITY_WATERMARK_SERIALIZATION_FORMAT,
577
+ suiteId?: string,
578
+ testId?: string,
579
+ ): AvailabilityTestSuiteState {
580
+ const suite = createDefaultSuite(
581
+ this.availability,
582
+ format,
583
+ isString(suiteId) && suiteId ? suiteId : undefined,
584
+ isString(testId) && testId ? testId : undefined,
585
+ );
586
+ addUniqueEntry(this.availability.tests, suite);
587
+ const state = new AvailabilityTestSuiteState(this, suite);
588
+ this.selectedSuiteState = state;
589
+ return state;
590
+ }
591
+
592
+ deleteSuite(suite: AvailabilityTestSuite): void {
593
+ const idx = this.availability.tests.indexOf(suite);
594
+ if (idx !== -1) {
595
+ this.availability.tests.splice(idx, 1);
596
+ }
597
+ if (this.selectedSuiteState?.suite === suite) {
598
+ const nextSuite = this.availability.tests[0];
599
+ this.selectedSuiteState =
600
+ nextSuite instanceof AvailabilityTestSuite
601
+ ? new AvailabilityTestSuiteState(this, nextSuite)
602
+ : undefined;
603
+ }
604
+ }
605
+
606
+ *runSuite(): GeneratorFn<void> {
607
+ if (!this.selectedSuiteState) {
608
+ return;
609
+ }
610
+ yield* this.selectedSuiteState.runSuite();
611
+ }
612
+
613
+ *runFailingSuiteTests(): GeneratorFn<void> {
614
+ if (!this.selectedSuiteState) {
615
+ return;
616
+ }
617
+ yield* this.selectedSuiteState.runFailingTests();
618
+ }
619
+
620
+ get suites(): AvailabilityTestSuite[] {
621
+ return this.editorState.availability.tests;
622
+ }
623
+
624
+ get suiteCount(): number {
625
+ return this.suites.length;
626
+ }
627
+
628
+ get testCount(): number {
629
+ return this.suites.reduce((sum, suite) => sum + suite.tests.length, 0);
630
+ }
631
+ }
@@ -216,10 +216,14 @@ export class ModelStoreDataState extends EmbeddedDataState {
216
216
  export class RelationElementState {
217
217
  relationElement: RelationElement;
218
218
  supportsColumnEditing: boolean;
219
+ columnDefaults: Record<string, string>;
219
220
 
220
221
  constructor(
221
222
  relationElement: RelationElement,
222
- options?: { supportsColumnEditing?: boolean },
223
+ options?: {
224
+ supportsColumnEditing?: boolean;
225
+ columnDefaults?: Record<string, string>;
226
+ },
223
227
  ) {
224
228
  makeObservable(this, {
225
229
  relationElement: observable,
@@ -233,14 +237,20 @@ export class RelationElementState {
233
237
  importCSV: action,
234
238
  });
235
239
  this.supportsColumnEditing = options?.supportsColumnEditing ?? true;
240
+ this.columnDefaults = options?.columnDefaults ?? {};
236
241
  this.relationElement = relationElement;
237
242
  this.relationElement = observe_RelationElement(relationElement);
238
243
  }
239
244
 
245
+ private defaultValueForColumn(column: string): string {
246
+ return this.columnDefaults[column] ?? '';
247
+ }
248
+
240
249
  addColumn(name: string): void {
241
250
  this.relationElement.columns.push(name);
251
+ const defaultVal = this.defaultValueForColumn(name);
242
252
  this.relationElement.rows.forEach((row) => {
243
- row.values.push('');
253
+ row.values.push(defaultVal);
244
254
  });
245
255
  }
246
256
 
@@ -265,7 +275,7 @@ export class RelationElementState {
265
275
  row.values = [];
266
276
  const newRow = observe_RelationRowTestData(row);
267
277
  this.relationElement.columns.forEach((col) => {
268
- newRow.values.push('');
278
+ newRow.values.push(this.defaultValueForColumn(col));
269
279
  });
270
280
  this.relationElement.rows.push(newRow);
271
281
  }
@@ -80,6 +80,7 @@ export enum PACKAGEABLE_ELEMENT_TYPE {
80
80
  TEMPORARY__LOCAL_CONNECTION = 'LOCAL_CONNECTION',
81
81
  INTERNAL__UnknownElement = 'UNKNOWN',
82
82
  INGEST_DEFINITION = 'INGEST_DEFINITION',
83
+ AVAILABILITY = 'AVAILABILITY',
83
84
  }
84
85
 
85
86
  export enum PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY {
package/tsconfig.json CHANGED
@@ -125,6 +125,8 @@
125
125
  "./src/stores/editor/editor-state/element-editor-state/RuntimeEditorState.ts",
126
126
  "./src/stores/editor/editor-state/element-editor-state/ToDelete_FunctionActivatorBuilderState.ts",
127
127
  "./src/stores/editor/editor-state/element-editor-state/UMLEditorState.ts",
128
+ "./src/stores/editor/editor-state/element-editor-state/availability/AvailabilityEditorState.ts",
129
+ "./src/stores/editor/editor-state/element-editor-state/availability/testable/AvailabilityTestableState.ts",
128
130
  "./src/stores/editor/editor-state/element-editor-state/connection/ConnectionEditorState.ts",
129
131
  "./src/stores/editor/editor-state/element-editor-state/connection/DatabaseBuilderState.ts",
130
132
  "./src/stores/editor/editor-state/element-editor-state/connection/DatabaseBuilderWizardState.ts",
@@ -256,6 +258,8 @@
256
258
  "./src/components/editor/editor-group/UnsupportedElementEditor.tsx",
257
259
  "./src/components/editor/editor-group/accessor/AccessorQueryBuilder.tsx",
258
260
  "./src/components/editor/editor-group/accessor/AccessorQueryBuilderHelper.tsx",
261
+ "./src/components/editor/editor-group/availability/AvailabilityEditor.tsx",
262
+ "./src/components/editor/editor-group/availability/testable/AvailabilityTestableEditor.tsx",
259
263
  "./src/components/editor/editor-group/connection-editor/ConnectionEditor.tsx",
260
264
  "./src/components/editor/editor-group/connection-editor/DatabaseBuilderWizard.tsx",
261
265
  "./src/components/editor/editor-group/connection-editor/DatabaseEditorHelper.tsx",