/** * Copyright (c) 2026-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Shared React components for Lakehouse test editors (DataProduct and Ingest). * * Exports: * - LakehouseElementTestDataItem — sidebar row for a single element's test data * - LakehouseElementTestDataEditor — editor for a selected element's RelationElementsData * - LakehouseAddElementModal — modal to add a new element to suite test data * - LakehouseTestDataEditor — full test-data panel (element list + editor) * - LakehouseTestEditor — assertion viewer for a single test * - LakehouseTestsEditor — tests list + test-detail panel * - LakehouseTestSuiteEditor — horizontal split: test data + tests * - LakehouseSuiteStateForEditor — interface for suite state passed to shared components * - LakehouseTestableStateForEditor — interface for top-level testable state */ import { observer } from 'mobx-react-lite'; import { flowResult } from 'mobx'; import { BlankPanelContent, BlankPanelPlaceholder, clsx, CustomSelectorInput, Dialog, ErrorWarnIcon, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalHeader, ModalTitle, PlusIcon, PlayIcon, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, ResizablePanelSplitterLine, RunAllIcon, TimesIcon, } from '@finos/legend-art'; import type { AtomicTest, TestSuite } from '@finos/legend-graph'; import type { ActionState, GeneratorFn } from '@finos/legend-shared'; import { useState, type ReactNode } from 'react'; import type { TestableTestEditorState } from '../../../../stores/editor/editor-state/element-editor-state/testable/TestableEditorState.js'; import { TESTABLE_RESULT, getTestableResultFromTestResult, } from '../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js'; import { getTestableResultIcon } from '../../side-bar/testable/GlobalTestRunner.js'; import type { LakehouseElementTestDataState, LakehouseTestDataStateBase, } from '../../../../stores/editor/editor-state/element-editor-state/testable/LakehouseTestableUtils.js'; import { RelationElementsDataEditor } from '../data-editor/RelationElementsDataEditor.js'; import { TestAssertionEditor } from './TestableSharedComponents.js'; import type { EditorStore } from '../../../../stores/editor/EditorStore.js'; // ─── Public interfaces ──────────────────────────────────────────────────────── /** * Minimal interface for a suite state object accepted by LakehouseTestsEditor * and LakehouseTestSuiteEditor. Satisfied by both IngestTestSuiteState and * DataProductTestSuiteState. */ export interface LakehouseSuiteStateForEditor { testDataState: LakehouseTestDataStateBase; testStates: TestableTestEditorState[]; selectTestState: TestableTestEditorState | undefined; runningSuiteState: ActionState; runSuite(): GeneratorFn; changeTest(test: AtomicTest): void; deleteTest(test: AtomicTest): void; suite: TestSuite; editorStore: EditorStore; } /** * Minimal interface for the top-level testable state passed to the tests editor. * Satisfied by both IngestTestableState and DataProductTestableState. */ export interface LakehouseTestableStateForEditor { setShowCreateTestModal(val: boolean): void; editorStore: EditorStore; } // ─── Element test data: list item ───────────────────────────────────────────── export const LakehouseElementTestDataItem = observer( (props: { elementState: LakehouseElementTestDataState; testDataState: LakehouseTestDataStateBase; isReadOnly: boolean; }) => { const { elementState, testDataState, isReadOnly } = props; const isActive = testDataState.selectedElementTestDataState === elementState; const select = (): void => testDataState.setSelectedElementTestDataState(elementState); return (
{elementState.element.name}
{!isReadOnly && (
)}
); }, ); // ─── Element test data: editor ──────────────────────────────────────────────── export const LakehouseElementTestDataEditor = observer( (props: { elementState: LakehouseElementTestDataState; isReadOnly: boolean; }) => { const { elementState, isReadOnly } = props; const dataState = elementState.relationElementsDataState; if (!dataState) { return ( No relation data for this element ); } return ( ); }, ); // ─── Add element modal ──────────────────────────────────────────────────────── export const LakehouseAddElementModal = observer( (props: { testDataState: LakehouseTestDataStateBase }) => { const { testDataState } = props; const applicationStore = testDataState.editorStore.applicationStore; const options = testDataState.availableElementsToAdd.map((e) => ({ value: e.path, label: e.path, })); const [selectedPath, setSelectedPath] = useState( options[0]?.value, ); const close = (): void => testDataState.setShowAddElementModal(false); const add = (): void => { if (selectedPath) { testDataState.addElement(selectedPath); close(); } }; const onChange = (val: { label: string; value: string } | null): void => { setSelectedPath(val?.value); }; return ( ); }, ); // ─── Test data editor (full panel: element list + per-element editor) ───────── export const LakehouseTestDataEditor = observer( (props: { testDataState: LakehouseTestDataStateBase; isReadOnly: boolean; }) => { const { testDataState, isReadOnly } = props; const addElement = (): void => { if (testDataState.availableElementsToAdd.length === 0) { testDataState.editorStore.applicationStore.notificationService.notifyWarning( 'No elements available to add', ); return; } testDataState.setShowAddElementModal(true); }; const hasTestData = testDataState.elementTestDataStates.length > 0; return (
Test Data
{!isReadOnly && (
)}
{!hasTestData ? (
Add an element to configure test data
) : (
{testDataState.elementTestDataStates.map((elementState) => ( ))}
)}
{testDataState.selectedElementTestDataState ? ( ) : ( Select an element to configure its test data )}
{testDataState.showAddElementModal && ( )}
); }, ); // ─── Test editor: assertion viewer ──────────────────────────────────────────── export const LakehouseTestEditor = observer( (props: { testState: TestableTestEditorState; isReadOnly: boolean; renderTestStateExtension?: ( testState: TestableTestEditorState, ) => ReactNode; }) => { const { testState, renderTestStateExtension } = props; const selectedAssertion = testState.selectedAsertionState; return (
Assertion
{renderTestStateExtension?.(testState)} {selectedAssertion && ( )} {!selectedAssertion && ( )}
); }, ); // ─── Test item: list row ────────────────────────────────────────────────────── const LakehouseTestItem = observer( (props: { testState: TestableTestEditorState; suiteState: LakehouseSuiteStateForEditor; isReadOnly: boolean; }) => { const { testState, suiteState, isReadOnly } = props; const isActive = suiteState.selectTestState === testState; const isRunning = testState.runningTestAction.isInProgress; const _testableResult = getTestableResultFromTestResult( testState.testResultState.result, ); const testResult = isRunning ? TESTABLE_RESULT.IN_PROGRESS : _testableResult; const select = (): void => suiteState.changeTest(testState.test); const runTest = (): void => { flowResult(testState.runTest()).catch( testState.editorStore.applicationStore.alertUnhandledError, ); }; const deleteTest = (): void => { if (!isReadOnly) { suiteState.deleteTest(testState.test); } }; return (
{getTestableResultIcon(testResult)}
{testState.test.id}
{!isReadOnly && ( )}
); }, ); // ─── Tests editor: test list + test detail ──────────────────────────────────── export const LakehouseTestsEditor = observer( (props: { suiteState: LakehouseSuiteStateForEditor; testableState: LakehouseTestableStateForEditor; isReadOnly: boolean; renderTestStateExtension?: ( testState: TestableTestEditorState, ) => ReactNode; }) => { const { suiteState, testableState, isReadOnly, renderTestStateExtension } = props; const selectedTest = suiteState.selectTestState; return (
Tests
{!isReadOnly && ( )}
{suiteState.testStates.map((testState) => ( ))}
{selectedTest ? ( ) : ( )}
); }, ); // ─── Suite editor: horizontal split (test data + tests) ─────────────────────── export const LakehouseTestSuiteEditor = observer( (props: { suiteState: LakehouseSuiteStateForEditor; testableState: LakehouseTestableStateForEditor; isReadOnly: boolean; renderTestStateExtension?: ( testState: TestableTestEditorState, ) => ReactNode; }) => { const { suiteState, testableState, isReadOnly, renderTestStateExtension } = props; return (
); }, );