@finos/legend-extension-dsl-data-quality
Version:
Legend extension for Data Quality
239 lines • 25.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* 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.
*/
import { observer } from 'mobx-react-lite';
import React, { useEffect, useMemo } from 'react';
import { BlankPanelContent, CaretDownIcon, clsx, ControlledDropdownMenu, CustomSelectorInput, Dialog, ExclamationTriangleIcon, MenuContent, MenuContentItem, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalFooterStatus, ModalHeader, Panel, PanelContent, PanelFormBooleanField, PanelFormSection, PanelLoadingIndicator, PauseCircleIcon, PlayIcon, RefreshIcon, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, ResizablePanelSplitterLine, } from '@finos/legend-art';
import { MD5HashStrategy } from '../graph/metamodel/pure/packageableElements/data-quality/DataQualityValidationConfiguration.js';
import { DataQualityRelationComparisonConfigurationState, DEFAULT_LIMIT, RECONCILIATION_EXECUTION_TYPE, } from './states/DataQualityRelationComparisonConfigurationState.js';
import { useEditorStore } from '@finos/legend-application-studio';
import { DEFAULT_TAB_SIZE, useApplicationStore, } from '@finos/legend-application';
import { BasicValueSpecificationEditor, LambdaEditor, LambdaParameterValuesEditor, getTDSColumnCustomizations, getFilterTDSColumnCustomizations, } from '@finos/legend-query-builder';
import { flowResult } from 'mobx';
import { DataQualityMultiCustomSelector } from './DataQualityCustomSelector.js';
import { PrimitiveType, TDSExecutionResult, RawExecutionResult, extractExecutionResultValues, } from '@finos/legend-graph';
import { guaranteeType, prettyDuration, prettyCONSTName, returnUndefOnError, } from '@finos/legend-shared';
import { CodeEditor } from '@finos/legend-lego/code-editor';
import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
import { DataQualityResultCellRenderer, getRowDataFromExecutionResult, } from './DataQualityRelationGridResult.js';
import { DataGrid, } from '@finos/legend-lego/data-grid';
const ComparisonParameterSection = observer((props) => {
const { title, graph, observerContext, parameterStates } = props;
if (!parameterStates.length) {
return null;
}
return (_jsxs("div", { className: "data-quality-relation-comparison-editor__parameters-modal__section", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__parameters-modal__section__title", children: title }), _jsx("div", { className: "data-quality-relation-comparison-editor__parameters-modal__section__body", children: parameterStates.map((paramState) => {
const variableType = paramState.variableType ?? PrimitiveType.STRING;
return (_jsxs("div", { className: "panel__content__form__section", children: [_jsxs("div", { className: "lambda-parameter-values__value__label", children: [_jsx("div", { className: "lambda-parameter-values__value__label__name", children: paramState.parameter.name }), _jsx("div", { className: "lambda-parameter-values__value__label__type", children: variableType.name })] }), paramState.value && (_jsx(BasicValueSpecificationEditor, { valueSpecification: paramState.value, setValueSpecification: (val) => {
paramState.setValue(val);
}, graph: graph, observerContext: observerContext, typeCheckOption: {
expectedType: variableType,
match: variableType === PrimitiveType.DATETIME,
}, className: "query-builder__parameters__value__editor", resetValue: () => undefined }))] }, paramState.uuid));
}) })] }));
});
const DataQualityRelationComparisonParametersEditor = observer((props) => {
const { state } = props;
const applicationStore = useApplicationStore();
const [isSubmitAction, setIsSubmitAction] = React.useState(false);
const [isClosingAction, setIsClosingAction] = React.useState(false);
const valuesEditorState = state.comparisonParametersEditorState;
const submitAction = valuesEditorState.submitAction;
const close = () => {
setIsClosingAction(true);
valuesEditorState.close();
};
const submit = applicationStore.guardUnhandledError(async () => {
if (submitAction) {
setIsSubmitAction(true);
close();
await submitAction.handler();
}
});
return (_jsx(Dialog, { open: Boolean(valuesEditorState.showModal), onClose: close, classes: {
root: 'editor-modal__root-container',
container: 'editor-modal__container',
paper: 'editor-modal__content',
}, children: _jsxs(Modal, { darkMode: !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled, className: "editor-modal lambda-parameter-values__modal data-quality-relation-comparison-editor__parameters-modal", children: [_jsx(ModalHeader, { title: "Set Comparison Parameter Values" }), _jsxs(ModalBody, { className: "lambda-parameter-values__modal__body data-quality-relation-comparison-editor__parameters-modal__body", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__parameters-modal__description", children: "Source and target parameters are submitted independently for the reconciliation run." }), _jsxs("div", { className: "data-quality-relation-comparison-editor__parameters-modal__sections", children: [_jsx(ComparisonParameterSection, { title: "Source Query", graph: state.editorStore.graphManagerState.graph, observerContext: state.editorStore.changeDetectionState.observerContext, parameterStates: state.sourceParametersState.parameterStates }), _jsx(ComparisonParameterSection, { title: "Target Query", graph: state.editorStore.graphManagerState.graph, observerContext: state.editorStore.changeDetectionState.observerContext, parameterStates: state.targetParametersState.parameterStates })] })] }), _jsxs(ModalFooter, { children: [isClosingAction && (_jsx(ModalFooterStatus, { children: "Closing..." })), submitAction && (_jsx(ModalFooterButton, { inProgressText: isSubmitAction ? `${submitAction.label}...` : undefined, onClick: submit, text: prettyCONSTName(submitAction.label) })), _jsx(ModalFooterButton, { inProgressText: isClosingAction ? 'Closing...' : undefined, onClick: close, text: "Close", type: "secondary" })] })] }) }));
});
export const DataQualityRelationComparisonEditor = observer(() => {
const editorStore = useEditorStore();
const applicationStore = useApplicationStore();
const state = editorStore.tabManagerState.getCurrentEditorState(DataQualityRelationComparisonConfigurationState);
const comparison = state.element;
const md5Strategy = guaranteeType(comparison.strategy, MD5HashStrategy);
const sourceColumnOptions = state.sourceColumnOptions;
const targetColumnOptions = state.targetColumnOptions;
const combinedColumnOptions = state.combinedColumnOptions;
const isRunning = state.isRunning;
const executionResult = state.executionResult;
const isFetchingColumns = state.fetchColumnsState.isInProgress;
const hasColumnFetchError = state.hasColumnFetchError;
const columnFetchError = state.columnFetchError;
const hasNoOverlappingColumns = state.hasNoOverlappingColumns;
const columnsDisabled = hasColumnFetchError || isFetchingColumns;
// Execution handlers
const cancelRun = applicationStore.guardUnhandledError(() => flowResult(state.cancelRun()));
const runReconciliation = applicationStore.guardUnhandledError(() => flowResult(state.handleRun(RECONCILIATION_EXECUTION_TYPE.RECONCILIATION)));
const runSourceQuery = applicationStore.guardUnhandledError(() => flowResult(state.handleRun(RECONCILIATION_EXECUTION_TYPE.SOURCE_QUERY)));
const runTargetQuery = applicationStore.guardUnhandledError(() => flowResult(state.handleRun(RECONCILIATION_EXECUTION_TYPE.TARGET_QUERY)));
const retryFetchColumns = applicationStore.guardUnhandledError(() => flowResult(state.retryFetchColumns()));
const getResultSetDescription = (_executionResult) => {
const queryDuration = state.executionDuration
? prettyDuration(state.executionDuration)
: undefined;
if (!queryDuration) {
return undefined;
}
const executionName = state.lastExecutionType === RECONCILIATION_EXECUTION_TYPE.RECONCILIATION
? 'Data Comparison'
: state.lastExecutionType === RECONCILIATION_EXECUTION_TYPE.SOURCE_QUERY
? 'Source Query'
: 'Target Query';
return `${executionName} ran in ${queryDuration}`;
};
const resultDescription = !isRunning && executionResult
? getResultSetDescription(executionResult)
: undefined;
const darkMode = !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
const [limitValue, setLimitValue] = React.useState(state.limit);
const inputRef = React.useRef(null);
const changeLimit = (event) => {
setLimitValue(parseInt(event.target.value, 10));
};
const getLimit = () => {
if (isNaN(limitValue) || limitValue === 0) {
setLimitValue(1000);
state.setLimit(1000);
}
else {
state.setLimit(limitValue);
}
};
const onLimitKeyDown = (event) => {
if (event.code === 'Enter') {
getLimit();
inputRef.current?.focus();
}
else if (event.code === 'Escape') {
setLimitValue(state.limit);
inputRef.current?.select();
}
};
useEffect(() => {
setLimitValue(state.limit);
}, [state.limit]);
const renderResult = () => {
if (executionResult instanceof TDSExecutionResult) {
const colDefs = executionResult.result.columns.map((colName) => ({
minWidth: 50,
sortable: true,
resizable: true,
field: colName,
flex: 1,
...getTDSColumnCustomizations(executionResult, colName),
...getFilterTDSColumnCustomizations(executionResult, colName),
cellRenderer: DataQualityResultCellRenderer,
}));
return (_jsx("div", { className: "data-quality-validation__result__values__table", children: _jsx("div", { className: clsx('data-quality-validation__result__tds-grid', {
'ag-theme-balham': !darkMode,
'ag-theme-balham-dark': darkMode,
}), children: _jsx(DataGrid, { rowData: getRowDataFromExecutionResult(executionResult), gridOptions: {
suppressScrollOnNewData: true,
getRowId: (data) => `${data.data.rowNumber}`,
rowSelection: {
mode: 'multiRow',
checkboxes: false,
headerCheckbox: false,
},
}, onRowDataUpdated: (params) => {
params.api.refreshCells({ force: true });
}, suppressFieldDotNotation: true, suppressContextMenu: false, columnDefs: colDefs }) }) }));
}
if (executionResult instanceof RawExecutionResult) {
const val = executionResult.value === null
? 'null'
: executionResult.value.toString();
return (_jsx(CodeEditor, { language: CODE_EDITOR_LANGUAGE.TEXT, inputValue: val, isReadOnly: true }));
}
else if (executionResult !== undefined) {
const json = returnUndefOnError(() => JSON.stringify(extractExecutionResultValues(executionResult), null, DEFAULT_TAB_SIZE)) ?? JSON.stringify(executionResult);
return (_jsx(CodeEditor, { language: CODE_EDITOR_LANGUAGE.JSON, inputValue: json, isReadOnly: true }));
}
return _jsx(BlankPanelContent, { children: "No Data to Display" });
};
useEffect(() => {
flowResult(state.sourceLambdaEditorState.convertLambdaObjectToGrammarString({
pretty: true,
firstLoad: true,
})).catch(applicationStore.alertUnhandledError);
flowResult(state.targetLambdaEditorState.convertLambdaObjectToGrammarString({
pretty: true,
firstLoad: true,
})).catch(applicationStore.alertUnhandledError);
}, [
applicationStore,
state.sourceLambdaEditorState,
state.targetLambdaEditorState,
]);
const sourceHashColumnValue = md5Strategy.sourceHashColumn
? {
value: md5Strategy.sourceHashColumn,
label: md5Strategy.sourceHashColumn,
}
: undefined;
const targetHashColumnValue = md5Strategy.targetHashColumn
? {
value: md5Strategy.targetHashColumn,
label: md5Strategy.targetHashColumn,
}
: undefined;
const selectedKeyOptions = useMemo(() => {
const selectedKeys = new Set(comparison.keys);
return combinedColumnOptions.filter(({ value }) => selectedKeys.has(value));
}, [combinedColumnOptions, comparison.keys]);
const selectedColumnsToCompareOptions = useMemo(() => {
const selectedColumns = new Set(comparison.columnsToCompare);
return combinedColumnOptions.filter(({ value }) => selectedColumns.has(value));
}, [combinedColumnOptions, comparison.columnsToCompare]);
return (_jsxs("div", { className: "data-quality-relation-comparison-editor", children: [_jsxs(Panel, { children: [_jsx(PanelLoadingIndicator, { isLoading: isRunning }), _jsx("div", { className: "panel__header", children: _jsxs("div", { className: "panel__header__title", children: [_jsx("div", { className: "panel__header__title__label", children: "dataQualityRelationComparison" }), _jsx("div", { className: "panel__header__title__content", children: comparison.name })] }) }), _jsxs("div", { className: "data-quality-relation-comparison-editor__actions-bar", children: [_jsxs("div", { className: "data-quality-relation-comparison-editor__actions-bar__limit", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__actions-bar__limit__label", children: "preview row limit" }), _jsx("input", { ref: inputRef, className: "input--dark data-quality-relation-comparison-editor__actions-bar__limit__input", spellCheck: false, type: "number", value: Number.isNaN(limitValue) ? '' : limitValue, min: 1, placeholder: DEFAULT_LIMIT.toString(), onChange: changeLimit, onBlur: getLimit, onKeyDown: onLimitKeyDown })] }), _jsx("div", { className: "btn__dropdown-combo btn__dropdown-combo--primary", children: state.isRunning ? (_jsx("button", { className: "btn__dropdown-combo__canceler data-quality-relation-comparison-editor__actions-bar__cancel-btn", onClick: cancelRun, tabIndex: -1, children: _jsxs("div", { className: "btn--dark btn--caution btn__dropdown-combo__canceler__label data-quality-relation-comparison-editor__actions-bar__cancel-label", children: [_jsx(PauseCircleIcon, { className: "btn__dropdown-combo__canceler__label__icon" }), _jsx("div", { className: "btn__dropdown-combo__canceler__label__title", children: "Stop" })] }) })) : (_jsxs("div", { className: "data-quality-relation-comparison-editor__actions-bar__run-group", children: [_jsxs("button", { className: "btn__dropdown-combo__label data-quality-relation-comparison-editor__actions-bar__run-btn", onClick: runReconciliation, title: "Run Data Comparison", disabled: isRunning, tabIndex: -1, children: [_jsx(PlayIcon, { className: "btn__dropdown-combo__label__icon" }), _jsx("div", { className: "btn__dropdown-combo__label__title", children: "Run Data Comparison" })] }), _jsx(ControlledDropdownMenu, { className: "btn__dropdown-combo__dropdown-btn data-quality-relation-comparison-editor__actions-bar__dropdown-btn", disabled: isRunning, content: _jsxs(MenuContent, { children: [_jsx(MenuContentItem, { className: "btn__dropdown-combo__option", onClick: runSourceQuery, children: "Run Source Query" }), _jsx(MenuContentItem, { className: "btn__dropdown-combo__option", onClick: runTargetQuery, children: "Run Target Query" })] }), menuProps: {
anchorOrigin: {
vertical: 'bottom',
horizontal: 'right',
},
transformOrigin: {
vertical: 'top',
horizontal: 'right',
},
}, children: _jsx(CaretDownIcon, {}) })] })) })] }), _jsxs(PanelContent, { children: [_jsx("div", { className: "data-quality-relation-comparison-editor__queries", children: _jsxs(ResizablePanelGroup, { orientation: "vertical", children: [_jsx(ResizablePanel, { minSize: 200, children: _jsxs("div", { className: "data-quality-relation-comparison-editor__query-panel", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__query-panel__header", children: _jsx("div", { className: "data-quality-relation-comparison-editor__query-panel__title", children: "SOURCE QUERY" }) }), _jsx("div", { className: clsx('data-quality-relation-comparison-editor__query-panel__content', {
backdrop__element: Boolean(state.sourceLambdaEditorState.parserError),
}), children: _jsx(LambdaEditor, { className: "data-quality-relation-comparison-editor__lambda-editor lambda-editor--dark", disabled: state.sourceLambdaEditorState
.isConvertingFunctionBodyToString, lambdaEditorState: state.sourceLambdaEditorState, forceBackdrop: false, autoFocus: false }) })] }) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-250)" }) }), _jsx(ResizablePanel, { minSize: 200, children: _jsxs("div", { className: "data-quality-relation-comparison-editor__query-panel", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__query-panel__header", children: _jsx("div", { className: "data-quality-relation-comparison-editor__query-panel__title", children: "TARGET QUERY" }) }), _jsx("div", { className: clsx('data-quality-relation-comparison-editor__query-panel__content', {
backdrop__element: Boolean(state.targetLambdaEditorState.parserError),
}), children: _jsx(LambdaEditor, { className: "data-quality-relation-comparison-editor__lambda-editor lambda-editor--dark", disabled: state.targetLambdaEditorState
.isConvertingFunctionBodyToString, lambdaEditorState: state.targetLambdaEditorState, forceBackdrop: false, autoFocus: false }) })] }) })] }) }), _jsxs("div", { className: "data-quality-relation-comparison-editor__panel__content__form", children: [hasColumnFetchError && (_jsxs("div", { className: "data-quality-relation-comparison-editor__column-fetch-error", children: [_jsx(ExclamationTriangleIcon, { className: "data-quality-relation-comparison-editor__column-fetch-error__icon" }), _jsx("span", { className: "data-quality-relation-comparison-editor__column-fetch-error__message", children: columnFetchError }), _jsxs("button", { className: "data-quality-relation-comparison-editor__column-fetch-error__retry-btn btn--dark btn--sm", onClick: retryFetchColumns, disabled: isFetchingColumns, tabIndex: -1, children: [_jsx(RefreshIcon, {}), _jsx("span", { children: "Retry" })] })] })), hasNoOverlappingColumns && (_jsxs("div", { className: "data-quality-relation-comparison-editor__column-overlap-warning", children: [_jsx(ExclamationTriangleIcon, { className: "data-quality-relation-comparison-editor__column-overlap-warning__icon" }), _jsx("span", { className: "data-quality-relation-comparison-editor__column-overlap-warning__message", children: "No overlapping columns found between source and target queries. The Keys and Columns to Compare selectors require at least one common column name across both queries." })] })), _jsxs(PanelFormSection, { children: [_jsx("div", { className: "panel__content__form__section__header__label", children: "Keys" }), _jsx("div", { className: "panel__content__form__section__header__prompt", children: "Columns used as join keys between source and target" }), _jsx(DataQualityMultiCustomSelector, { value: selectedKeyOptions, onChange: (values) => state.setKeys(values.map((option) => option.value)), options: combinedColumnOptions, placeholder: "Select keys...", disabled: columnsDisabled, darkMode: darkMode })] }), _jsxs(PanelFormSection, { children: [_jsx("div", { className: "panel__content__form__section__header__label", children: "Columns to Compare" }), _jsx(DataQualityMultiCustomSelector, { value: selectedColumnsToCompareOptions, onChange: (values) => state.setColumnsToCompare(values.map((option) => option.value)), options: combinedColumnOptions, placeholder: "Select columns to compare...", disabled: columnsDisabled, darkMode: darkMode })] }), _jsxs(PanelFormSection, { children: [_jsx("div", { className: "panel__content__form__section__header__label", children: "Source Hash Column" }), _jsx("div", { className: "panel__content__form__section__header__prompt", children: "If a source hash column already exists you can specify it here (optional)" }), _jsx(CustomSelectorInput, { value: sourceHashColumnValue ?? null, options: sourceColumnOptions, onChange: (opt) => {
state.setSourceHashColumn(opt?.value);
}, placeholder: "Select source hash column...", isClearable: true, darkMode: darkMode, disabled: columnsDisabled })] }), _jsxs(PanelFormSection, { children: [_jsx("div", { className: "panel__content__form__section__header__label", children: "Target Hash Column" }), _jsx("div", { className: "panel__content__form__section__header__prompt", children: "If a target hash column already exists you can specify it here (optional)" }), _jsx(CustomSelectorInput, { value: targetHashColumnValue ?? null, options: targetColumnOptions, onChange: (opt) => {
state.setTargetHashColumn(opt?.value);
}, placeholder: "Select target hash column...", isClearable: true, darkMode: darkMode, disabled: columnsDisabled })] }), _jsx(PanelFormBooleanField, { name: "Aggregated Hash", prompt: "Compare data at a group level using keys, or compare entire datasets as a whole when no keys are provided.", value: md5Strategy.aggregatedHash, isReadOnly: false, update: (value) => state.setAggregatedHash(value) })] }), _jsxs("div", { className: "data-quality-relation-comparison-editor__result", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__result__header", children: _jsxs("div", { className: "data-quality-relation-comparison-editor__result__header-group", children: [_jsx("div", { className: "data-quality-relation-comparison-editor__result__title", children: "RESULT" }), isRunning && (_jsx("div", { className: "data-quality-relation-comparison-editor__result__status", children: state.currentExecutionType ===
RECONCILIATION_EXECUTION_TYPE.RECONCILIATION
? 'Running Data Comparison...'
: state.currentExecutionType ===
RECONCILIATION_EXECUTION_TYPE.SOURCE_QUERY
? 'Running Source Query...'
: 'Running Target Query...' })), _jsx("div", { className: "data-quality-relation-comparison-editor__result__analytics", children: resultDescription ?? '' })] }) }), _jsx("div", { className: "data-quality-relation-comparison-editor__result__content", children: _jsx("div", { className: "data-quality-relation-comparison-editor__result__viewer", children: renderResult() }) })] })] })] }), state.sourceParametersState.parameterValuesEditorState.showModal && (_jsx(LambdaParameterValuesEditor, { graph: editorStore.graphManagerState.graph, observerContext: editorStore.changeDetectionState.observerContext, lambdaParametersState: state.sourceParametersState })), state.targetParametersState.parameterValuesEditorState.showModal && (_jsx(LambdaParameterValuesEditor, { graph: editorStore.graphManagerState.graph, observerContext: editorStore.changeDetectionState.observerContext, lambdaParametersState: state.targetParametersState })), state.comparisonParametersEditorState.showModal && (_jsx(DataQualityRelationComparisonParametersEditor, { state: state }))] }));
});
//# sourceMappingURL=DataQualityRelationComparisonEditor.js.map