UNPKG

@finos/legend-extension-dsl-data-quality

Version:
239 lines 25.8 kB
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