/**
* 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 (
);
},
);
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 (
);
},
);
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 && (
)}
>
);
},
);
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 && (
)}
>
);
},
);
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 && (
)}
>
);
},
);
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 && (
)}
>
) : 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 (
);
},
);