/** * Copyright (c) 2020-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. */ import { BlankPanelPlaceholder, clsx, CompareIcon, ContextMenu, CustomSelectorInput, Dialog, MenuContent, MenuContentItem, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalHeader, ModalTitle, PanelContent, PanelFormTextField, PanelHeader, PanelHeaderActions, PanelLoadingIndicator, RefreshIcon, WrenchIcon, } from '@finos/legend-art'; import { type DataElement, DataProduct, IngestDefinition, type ValueSpecification, type VariableExpression, type PrimitiveInstanceValue, TestError, PrimitiveType, } from '@finos/legend-graph'; import { ContentType, prettyCONSTName, tryToFormatLosslessJSONString, } from '@finos/legend-shared'; import { observer } from 'mobx-react-lite'; import { AssertFailState, EqualToJsonAssertFailState, EqualToJsonAssertionState, TestAssertionStatusState, TEST_ASSERTION_TAB, type TestAssertionResultState, type TestAssertionEditorState, type TestAssertionState, EqualToAssertionState, EqualToAssertFailState, EqualToRelationAssertionState, EqualToRelationAssertFailState, } from '../../../../stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js'; import { externalFormatData_setData } from '../../../../stores/graph-modifier/DSL_Data_GraphModifierHelper.js'; import { TESTABLE_RESULT } from '../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js'; import { UnsupportedEditorPanel } from '../UnsupportedElementEditor.js'; import { CodeEditor, JSONDiffView } from '@finos/legend-lego/code-editor'; import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor'; import { forwardRef, useState } from 'react'; import type { TestableTestEditorState } from '../../../../stores/editor/editor-state/element-editor-state/testable/TestableEditorState.js'; import { getTestableResultIcon } from '../../side-bar/testable/GlobalTestRunner.js'; import type { EditorStore } from '../../../../stores/editor/EditorStore.js'; import { buildElementOption, getPackageableElementOptionFormatter, } from '@finos/legend-lego/graph-editor'; import type { TestParamContentType } from '../../../../stores/editor/utils/TestableUtils.js'; import { RelationElementEditor } from '../data-editor/RelationElementsDataEditor.js'; import { BasicValueSpecificationEditor, buildDefaultInstanceValue, } from '@finos/legend-query-builder'; import { useApplicationStore } from '@finos/legend-application'; import type { DataProductTestState } from '../../../../stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js'; import type { IngestTestState } from '../../../../stores/editor/editor-state/element-editor-state/ingest/IngestTestableState.js'; export const SharedDataElementModal = observer( (props: { filterBy?: (val: DataElement) => boolean; handler: (val: DataElement) => void; editorStore: EditorStore; isReadOnly: boolean; close: () => void; }) => { const { filterBy, isReadOnly, close, editorStore, handler } = props; const applicationStore = editorStore.applicationStore; const dataElements = editorStore.graphManagerState.graph.dataElements.filter((e) => filterBy ? filterBy(e) : true, ); const [dataElement, setDataElement] = useState(dataElements[0]); const dataElementOptions = editorStore.graphManagerState.usableDataElements.map(buildElementOption); const selectedDataElement = dataElement ? buildElementOption(dataElement) : null; const onDataElementChange = (val: { label: string; value?: DataElement; }): void => { if (val.value !== selectedDataElement?.value && val.value) { setDataElement(val.value); } }; const change = (): void => { if (dataElement) { handler(dataElement); } close(); }; return (
Data Element
Change
); }, ); export const RenameModal = observer( (props: { val: string; isReadOnly: boolean; setValue: (val: string) => void; showModal: boolean; closeModal: () => void; errorMessageFunc?: (val: string) => string | undefined; }) => { const { val, isReadOnly, showModal, closeModal, setValue, errorMessageFunc, } = props; const [inputValue, setInputValue] = useState(val); const changeValue = (_val: string | undefined): void => { setInputValue(_val ?? ''); }; const errorMessage = errorMessageFunc?.(inputValue); return (
{ event.preventDefault(); setValue(inputValue); closeModal(); }} className="modal modal--dark search-modal" >
); }, ); const EqualToJsonAsssertionEditor = observer( (props: { testAssertionEditorState: TestAssertionEditorState; equalToJsonAssertionState: EqualToJsonAssertionState; }) => { const { equalToJsonAssertionState, testAssertionEditorState } = props; const assertion = equalToJsonAssertionState.assertion; const formatExpectedResultJSONString = (): void => { externalFormatData_setData( assertion.expected, tryToFormatLosslessJSONString(assertion.expected.data), ); }; return ( <>
{ equalToJsonAssertionState.setExpectedValue(val); }} hideGutter={true} />
); }, ); const EqualToAsssertionEditor = observer( (props: { testAssertionEditorState: TestAssertionEditorState; equalToAssertionState: EqualToAssertionState; }) => { const { equalToAssertionState, testAssertionEditorState } = props; const editorStore = testAssertionEditorState.editorStore; const resetNode = (): void => { const type = equalToAssertionState.valueSpec.genericType?.value.rawType; if (type) { const valSpec = buildDefaultInstanceValue( testAssertionEditorState.editorStore.graphManagerState.graph, type, testAssertionEditorState.editorStore.changeDetectionState .observerContext, true, ); equalToAssertionState.updateValueSpec(valSpec); } }; return ( <>
{ equalToAssertionState.updateValueSpec(val); }} graph={editorStore.graphManagerState.graph} observerContext={editorStore.changeDetectionState.observerContext} resetValue={resetNode} typeCheckOption={{ expectedType: equalToAssertionState.valueSpec.genericType?.value.rawType ?? PrimitiveType.STRING, }} />
); }, ); const EqualToJsonAssertFailViewer = observer( (props: { equalToJsonAssertFailState: EqualToJsonAssertFailState }) => { const { equalToJsonAssertFailState } = props; const applicationStore = equalToJsonAssertFailState.resultState.editorStore.applicationStore; const open = (): void => equalToJsonAssertFailState.setDiffModal(true); const close = (): void => equalToJsonAssertFailState.setDiffModal(false); const expected = equalToJsonAssertFailState.status.expected; const actual = equalToJsonAssertFailState.status.actual; const acceptActualValue = (): void => { try { const assertionState = equalToJsonAssertFailState.resultState.assertionState; if ( assertionState.assertionState instanceof EqualToJsonAssertionState ) { assertionState.assertionState.setExpectedValue(actual); applicationStore.notificationService.notifySuccess( 'Expected value updated successfully!', ); close(); } } catch (error) { applicationStore.notificationService.notifyError( `Failed to update expected value: ${error instanceof Error ? error.message : String(error)}`, ); } }; return ( <>
{``}
{equalToJsonAssertFailState.diffModal && (
expected
actual
)} ); }, ); const EqualToAssertFailViewer = observer( (props: { equalToAssertFailState: EqualToAssertFailState }) => { const { equalToAssertFailState } = props; const applicationStore = equalToAssertFailState.resultState.editorStore.applicationStore; const open = (): void => equalToAssertFailState.setDiffModal(true); const close = (): void => equalToAssertFailState.setDiffModal(false); const statusMessage = equalToAssertFailState.status.message ?? ''; const extractValues = (message: string) => { const expectedMatch = message.match( /expected:\s*(?.*?)(?:\s+Found\s*:|$)/s, ); const actualMatch = message.match(/Found\s*:\s*(?.*?)$/s); return { expected: expectedMatch?.groups?.expected?.trim() ?? 'N/A', actual: actualMatch?.groups?.actual?.trim() ?? 'N/A', }; }; const { expected, actual } = extractValues(statusMessage); const acceptActualValue = (): void => { const assertionState = equalToAssertFailState.resultState.assertionState; if (assertionState.assertionState instanceof EqualToAssertionState) { try { const editorStore = assertionState.editorStore; const graph = editorStore.graphManagerState.graph; const currentVS = assertionState.assertionState.valueSpec; const rawType = currentVS.genericType?.value.rawType ?? PrimitiveType.STRING; const newValueSpec = buildDefaultInstanceValue( graph, rawType, editorStore.changeDetectionState.observerContext, true, ) as PrimitiveInstanceValue; const coerce = (txt: string, type: PrimitiveType): unknown => { const trimmed = txt.trim(); switch (type) { case PrimitiveType.INTEGER: { const n = Number(trimmed); if (!Number.isFinite(n) || !Number.isInteger(n)) { throw new Error(`Actual "${trimmed}" is not an integer`); } return n; } case PrimitiveType.FLOAT: case PrimitiveType.DECIMAL: case PrimitiveType.NUMBER: { const n = Number(trimmed); if (!Number.isFinite(n)) { throw new Error(`Actual "${trimmed}" is not a number`); } return n; } case PrimitiveType.BOOLEAN: { if (trimmed.toLowerCase() === 'true') { return true; } if (trimmed.toLowerCase() === 'false') { return false; } throw new Error(`Actual "${trimmed}" is not a boolean`); } default: return trimmed; } }; const expectedType: PrimitiveType = rawType instanceof PrimitiveType ? rawType : PrimitiveType.STRING; newValueSpec.values = [coerce(actual, expectedType)]; assertionState.assertionState.updateValueSpec(newValueSpec); applicationStore.notificationService.notifySuccess( 'Expected value updated successfully!', ); close(); } catch (error) { applicationStore.notificationService.notifyError( `Failed to update expected value: ${error instanceof Error ? error.message : String(error)}`, ); } } else { applicationStore.notificationService.notifyError( 'Unable to access assertion state for this assertion type', ); } }; return ( <>
{``}
{equalToAssertFailState.diffModal && (
expected
actual
)} ); }, ); const TestErrorViewer = observer((props: { testError: TestError }) => { const { testError } = props; return ( <>
{testError.error}
); }); const EqualToRelationAsssertionEditor = observer( (props: { testAssertionEditorState: TestAssertionEditorState; equalToRelationAssertionState: EqualToRelationAssertionState; }) => { const { equalToRelationAssertionState, testAssertionEditorState } = props; const isReadOnly = testAssertionEditorState.testState.isReadOnly; return ( ); }, ); const LakehouseEqualToRelationAssertionEditor = observer( (props: { testAssertionEditorState: TestAssertionEditorState }) => { const { testAssertionEditorState } = props; const isDataProductTest = testAssertionEditorState.testState.testable instanceof DataProduct; const testState = isDataProductTest ? (testAssertionEditorState.testState as DataProductTestState) : (testAssertionEditorState.testState as IngestTestState); const isReadOnly = testAssertionEditorState.testState.isReadOnly; const relationElementState = testState.testDataRelationState; return (
{isDataProductTest ? `Access Point: ${(testState as DataProductTestState).accessPointLabel}` : `Dataset: ${(testState as IngestTestState).datasetLabel}`}
{relationElementState ? ( ) : ( )}
); }, ); const EqualToRelationAssertFailViewer = observer( (props: { equalToRelationAssertFailState: EqualToRelationAssertFailState; }) => { const { equalToRelationAssertFailState } = props; const applicationStore = equalToRelationAssertFailState.resultState.editorStore.applicationStore; const open = (): void => equalToRelationAssertFailState.setDiffModal(true); const close = (): void => equalToRelationAssertFailState.setDiffModal(false); const expected = equalToRelationAssertFailState.status.expected; const actual = equalToRelationAssertFailState.status.actual; return ( <>
{``}
{equalToRelationAssertFailState.diffModal && (
expected
actual
)} ); }, ); const AssertFailViewer = observer( (props: { assertFailState: AssertFailState }) => { const { assertFailState } = props; const applicationStore = useApplicationStore(); const [showGenericDiffModal, setShowGenericDiffModal] = useState(false); const message = assertFailState.status.message ?? ''; const isGenericEqualToFail = message.includes('expected:') && message.includes('Found :'); const extractValues = (msg: string) => { const expectedMatch = msg.match( /expected:\s*(?.*?)(?:\s+Found\s*:|$)/s, ); const actualMatch = msg.match(/Found\s*:\s*(?.*?)$/s); return { expected: expectedMatch?.groups?.expected?.trim() ?? 'N/A', actual: actualMatch?.groups?.actual?.trim() ?? 'N/A', }; }; const { expected, actual } = extractValues(message); return ( <>
{assertFailState.status.message}
{assertFailState instanceof EqualToJsonAssertFailState ? ( ) : assertFailState instanceof EqualToRelationAssertFailState ? ( ) : assertFailState instanceof EqualToAssertFailState ? ( ) : isGenericEqualToFail ? ( <>
setShowGenericDiffModal(true)} > {``}
{showGenericDiffModal && ( setShowGenericDiffModal(false)} classes={{ container: 'search-modal__container' }} slotProps={{ paper: { classes: { root: 'search-modal__inner-container' }, }, }} > setShowGenericDiffModal(false)} /> )} ) : null} ); }, ); export const TestAssertionResultViewer = observer( (props: { testAssertionEditorState: TestAssertionEditorState }) => { const { testAssertionEditorState } = props; const parentAssertionResultState = testAssertionEditorState.assertionResultState; const assertionResult = testAssertionEditorState.assertionResultState.result; const renderAssertionResult = ( assertionResultState: TestAssertionResultState, ): React.ReactNode => { const _statusState = assertionResultState.statusState; if (assertionResultState.testResult instanceof TestError) { return ; } else if (_statusState instanceof TestAssertionStatusState) { return _statusState instanceof AssertFailState ? ( ) : null; } return null; }; const renderResultView = ( assertionResultState: TestAssertionResultState, ): React.ReactNode => { const _statusState = assertionResultState.statusState; if ( _statusState === undefined || _statusState instanceof TestAssertionStatusState ) { return ( <>
Result: {prettyCONSTName(assertionResult)}
{renderAssertionResult(assertionResultState)} ); } else { return Array.from(_statusState.entries()).map((state) => { const _key = state[0]; const resultState = state[1]; return (
{_key}
Result: {prettyCONSTName(resultState.result)}
{renderAssertionResult(resultState)}
); }); } }; return ( <>
Assertion Result Summary
{assertionResult === TESTABLE_RESULT.IN_PROGRESS && (
Running assertion...
)} {assertionResult !== TESTABLE_RESULT.IN_PROGRESS && ( <>
Id: {testAssertionEditorState.assertion.id}
Type: {testAssertionEditorState.assertionState.label()}
{renderResultView(parentAssertionResultState)} )}
); }, ); export const TestAssertionContextMenu = observer( forwardRef< HTMLDivElement, { testableTestState: TestableTestEditorState; testAssertionState: TestAssertionEditorState; } >(function TestContainerContextMenu(props, ref) { const { testableTestState: testableTestState, testAssertionState } = props; const rename = (): void => testableTestState.setAssertionToRename(testAssertionState.assertion); const remove = (): void => testableTestState.deleteAssertion(testAssertionState); const add = (): void => testableTestState.addAssertion(); return ( Rename Delete Create a new assert ); }), ); export const TestAssertionItem = observer( (props: { testAssertionEditorState: TestAssertionEditorState; testableTestState: TestableTestEditorState; isReadOnly: boolean; }) => { const { testAssertionEditorState, isReadOnly, testableTestState } = props; const [isSelectedFromContextMenu, setIsSelectedFromContextMenu] = useState(false); const isRunning = testableTestState.runningTestAction.isInProgress; const testAssertion = testAssertionEditorState.assertion; const isActive = testableTestState.selectedAsertionState?.assertion === testAssertion; const _testableResult = testAssertionEditorState.assertionResultState.result; const testableResult = isRunning ? TESTABLE_RESULT.IN_PROGRESS : _testableResult; const resultIcon = getTestableResultIcon(testableResult); const openTestAssertion = (): void => testableTestState.openAssertion(testAssertion); const onContextMenuOpen = (): void => setIsSelectedFromContextMenu(true); const onContextMenuClose = (): void => setIsSelectedFromContextMenu(false); return ( } menuProps={{ elevation: 7 }} onOpen={onContextMenuOpen} onClose={onContextMenuClose} > ); }, ); export const TestAssertionEditor = observer( (props: { testAssertionState: TestAssertionEditorState }) => { const { testAssertionState } = props; const selectedTab = testAssertionState.selectedTab; const isReadOnly = testAssertionState.testState.isReadOnly; const isLakehouseTest = testAssertionState.testState.testable instanceof DataProduct || testAssertionState.testState.testable instanceof IngestDefinition; const isDisabled = isReadOnly || !testAssertionState.assertionState.supportsGeneratingAssertion || testAssertionState.generatingExpectedAction.isInProgress; const changeTab = (val: TEST_ASSERTION_TAB): void => testAssertionState.setSelectedTab(val); const renderContent = (state: TestAssertionState): React.ReactNode => { if (state instanceof EqualToJsonAssertionState) { return ( ); } else if (state instanceof EqualToAssertionState) { return ( ); } else if (state instanceof EqualToRelationAssertionState) { if (isLakehouseTest) { return ( ); } return ( ); } return ( ); }; const generate = (): void => { testAssertionState.generateExpected(); }; const isRunning = testAssertionState.generatingExpectedAction.isInProgress; return (
{Object.values(TEST_ASSERTION_TAB).map((tab) => (
changeTab(tab)} className={clsx('testable-test-assertion-editor__header__tab', { 'testable-test-assertion-editor__header__tab--active': tab === selectedTab, })} > {prettyCONSTName(tab)}
))}
{!isLakehouseTest && (
)}
{selectedTab === TEST_ASSERTION_TAB.EXPECTED && (
{renderContent(testAssertionState.assertionState)}
)} {selectedTab === TEST_ASSERTION_TAB.RESULT && ( )}
); }, ); export const ExternalFormatParameterEditorModal = observer( (props: { valueSpec: ValueSpecification; varExpression: VariableExpression; isReadOnly: boolean; onClose: () => void; updateParamValue: (val: string) => void; contentTypeParamPair: TestParamContentType; }) => { const { valueSpec, varExpression, isReadOnly, onClose, updateParamValue, contentTypeParamPair, } = props; const applicationStore = useApplicationStore(); const paramValue = varExpression.genericType?.value.rawType === PrimitiveType.BYTE ? atob((valueSpec as PrimitiveInstanceValue).values[0] as string) : ((valueSpec as PrimitiveInstanceValue).values[0] as string); return (
); }, );