@finos/legend-extension-dsl-data-quality
Version:
Legend extension for Data Quality
186 lines • 17.3 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* 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 { useEffect, useRef, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useDrag } from 'react-dnd';
import { useApplicationStore } from '@finos/legend-application';
import { guaranteeNonNullable, prettyCONSTName } from '@finos/legend-shared';
import { flowResult } from 'mobx';
import { DATA_QUALITY_VALIDATION_TEST_ID } from './constants/DataQualityConstants.js';
import { QueryBuilderExplorerTreeNodeData, QUERY_BUILDER_EXPLORER_TREE_DND_TYPE, QueryBuilderExplorerTreePropertyNodeData, QueryBuilderExplorerTreeRootNodeData, QueryBuilderExplorerTreeSubTypeNodeData, checkForDeprecatedNode, getQueryBuilderPropertyNodeData, getQueryBuilderSubTypeNodeData, QueryBuilderPropertyInfoTooltip, QueryBuilderRootClassInfoTooltip, QueryBuilderSubclassInfoTooltip, renderPropertyTypeIcon, getQueryBuilderExplorerTreeNodeSortRank, } from '@finos/legend-query-builder';
import { CheckIcon, ChevronDownIcon, ChevronRightIcon, clsx, CompressIcon, InfoCircleIcon, MenuContentItemIcon, MenuContentItemLabel, MoreVerticalIcon, PanelHeaderActions, BlankPanelContent, DragPreviewLayer, MenuContent, MenuContentItem, PanelHeader, PanelHeaderActionItem, PanelLoadingIndicator, PURE_ClassIcon, TreeView, useDragPreviewLayer, ControlledDropdownMenu, } from '@finos/legend-art';
import { Class, DerivedProperty, Enumeration, PrimitiveType, TYPE_CAST_TOKEN, getAllClassDerivedProperties, getAllClassProperties, getAllOwnClassProperties, } from '@finos/legend-graph';
export const QueryBuilderExplorerTreeNodeContainer = observer((props) => {
const { node, level, stepPaddingInRem, onNodeSelect, innerProps } = props;
const { dataQualityState } = innerProps;
const { dataQualityQueryBuilderState } = dataQualityState;
const [isSelectedFromContextMenu] = useState(false);
const explorerState = dataQualityQueryBuilderState.explorerState;
const [, dragConnector, dragPreviewConnector] = useDrag(() => ({
type: node.type instanceof Enumeration
? QUERY_BUILDER_EXPLORER_TREE_DND_TYPE.ENUM_PROPERTY
: node.type instanceof Class
? QUERY_BUILDER_EXPLORER_TREE_DND_TYPE.CLASS_PROPERTY
: QUERY_BUILDER_EXPLORER_TREE_DND_TYPE.PRIMITIVE_PROPERTY,
item: () => node instanceof QueryBuilderExplorerTreeNodeData ? { node } : {},
}), [node]);
const ref = useRef(null);
dragConnector(ref);
useDragPreviewLayer(dragPreviewConnector);
const isExpandable = Boolean(node.childrenIds.length);
const isDerivedProperty = node instanceof QueryBuilderExplorerTreePropertyNodeData &&
node.property instanceof DerivedProperty;
const isMultiple = (node instanceof QueryBuilderExplorerTreePropertyNodeData &&
(node.property.multiplicity.upperBound === undefined ||
node.property.multiplicity.upperBound > 1)) ||
(node instanceof QueryBuilderExplorerTreeSubTypeNodeData &&
(node.multiplicity.upperBound === undefined ||
node.multiplicity.upperBound > 1));
const allowPreview = node.mappingData.mapped &&
node instanceof QueryBuilderExplorerTreePropertyNodeData &&
node.type instanceof PrimitiveType &&
!node.isPartOfDerivedPropertyBranch;
const nodeExpandIcon = isExpandable ? (node.isOpen ? (_jsx(ChevronDownIcon, {})) : (_jsx(ChevronRightIcon, {}))) : (_jsx("div", {}));
const propertyName = explorerState.humanizePropertyName
? node instanceof QueryBuilderExplorerTreeSubTypeNodeData
? TYPE_CAST_TOKEN + prettyCONSTName(node.label)
: prettyCONSTName(node.label)
: node instanceof QueryBuilderExplorerTreeSubTypeNodeData
? TYPE_CAST_TOKEN + node.label
: node.label;
const selectNode = () => onNodeSelect?.(node);
if (!node.mappingData.mapped &&
// NOTE: we always want to show at least the root node
!(node instanceof QueryBuilderExplorerTreeRootNodeData) &&
!explorerState.showUnmappedProperties) {
return null;
}
return (_jsxs("div", { className: clsx('data-quality-tree-view__node__container data-quality-validation-explorer-tree__node__container', {
'data-quality-validation-explorer-tree__node__container--selected-from-context-menu': isSelectedFromContextMenu,
'data-quality-validation-explorer-tree__node__container--unmapped': !node.mappingData.mapped,
'data-quality-validation-explorer-tree__node__container--selected': node.isSelected,
}), title: !node.mappingData.mapped
? node instanceof QueryBuilderExplorerTreeRootNodeData
? 'Root class is not mapped'
: 'Property is not mapped'
: undefined, onClick: selectNode, ref: node.mappingData.mapped ? ref : undefined, style: {
paddingLeft: `${(level - 1) * (stepPaddingInRem ?? 1) + 0.5}rem`,
display: 'flex',
}, children: [node instanceof QueryBuilderExplorerTreeRootNodeData && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "data-quality-tree-view__node__icon data-quality-validation-explorer-tree__node__icon", children: [_jsx("div", { className: "data-quality-validation-explorer-tree__expand-icon", children: nodeExpandIcon }), _jsx("div", { className: "data-quality-validation-explorer-tree__type-icon", children: _jsx(PURE_ClassIcon, {}) })] }), _jsx("div", { className: "data-quality-tree-view__node__label data-quality-validation-explorer-tree__node__label data-quality-validation-explorer-tree__node__label--with-action", children: node.label }), _jsx("div", { className: "data-quality-validation-explorer-tree__node__actions", children: _jsx(QueryBuilderRootClassInfoTooltip, { _class: guaranteeNonNullable(dataQualityQueryBuilderState.class), children: _jsx("div", { className: "data-quality-validation-explorer-tree__node__action data-quality-validation-explorer-tree__node__info", "data-testid": DATA_QUALITY_VALIDATION_TEST_ID.DATA_QUALITY_VALIDATION_TOOLTIP_ICON, children: _jsx(InfoCircleIcon, {}) }) }) })] })), (node instanceof QueryBuilderExplorerTreePropertyNodeData ||
node instanceof QueryBuilderExplorerTreeSubTypeNodeData) && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "data-quality-tree-view__node__icon data-quality-validation-explorer-tree__node__icon", ref: node.elementRef, children: [_jsx("div", { className: "data-quality-validation-explorer-tree__expand-icon", children: nodeExpandIcon }), _jsx("div", { className: "data-quality-validation-explorer-tree__type-icon", children: renderPropertyTypeIcon(node.type) })] }), _jsxs("div", { className: clsx('data-quality-tree-view__node__label data-quality-validation-explorer-tree__node__label data-quality-validation-explorer-tree__node__label--with-action', {
'data-quality-validation-explorer-tree__node__label--with-preview': allowPreview,
}, {
'data-quality-validation-explorer-tree__node__label--highlight': node.isHighlighting,
}), onAnimationEnd: () => node.setIsHighlighting(false), children: [_jsx("div", { className: clsx('data-quality-validation-explorer-tree__node__label--property__name', {
'data-quality-validation-explorer-tree__node__label--deprecated': checkForDeprecatedNode(node, explorerState.queryBuilderState.graphManagerState.graph, explorerState.nonNullableTreeData),
}), children: propertyName }), isDerivedProperty && (_jsx("div", { className: "data-quality-validation-explorer-tree__node__label__derived-property", title: "Property is derived and may require user to specify parameter values", children: "(...)" })), isMultiple && (_jsx("div", { className: "data-quality-validation-explorer-tree__node__label__multiple", title: "Multiple values of this property can cause row explosion", children: "*" }))] }), _jsxs("div", { className: "data-quality-validation-explorer-tree__node__actions", children: [node instanceof QueryBuilderExplorerTreePropertyNodeData && (_jsx(QueryBuilderPropertyInfoTooltip, { title: propertyName, property: node.property, path: node.id, isMapped: node.mappingData.mapped, type: node.type, children: _jsx("div", { className: "data-quality-validation-explorer-tree__node__action data-quality-validation-explorer-tree__node__info", "data-testid": DATA_QUALITY_VALIDATION_TEST_ID.DATA_QUALITY_VALIDATION_TOOLTIP_ICON, children: _jsx(InfoCircleIcon, {}) }) })), node instanceof QueryBuilderExplorerTreeSubTypeNodeData && (_jsx(QueryBuilderSubclassInfoTooltip, { subclass: node.subclass, path: node.id, isMapped: node.mappingData.mapped, multiplicity: node.multiplicity, children: _jsx("div", { className: "data-quality-validation-explorer-tree__node__action data-quality-validation-explorer-tree__node__info", "data-testid": DATA_QUALITY_VALIDATION_TEST_ID.DATA_QUALITY_VALIDATION_TOOLTIP_ICON, children: _jsx(InfoCircleIcon, {}) }) }))] })] }))] }));
});
const QueryBuilderExplorerTreeNodeView = observer((props) => {
const { node, level, onNodeSelect, getChildNodes, stepPaddingInRem, innerProps, } = props;
const { dataQualityState } = innerProps;
const { dataQualityQueryBuilderState } = dataQualityState;
if (!node.mappingData.mapped &&
// NOTE: we always want to show at least the root node
!(node instanceof QueryBuilderExplorerTreeRootNodeData) &&
!dataQualityQueryBuilderState.explorerState.showUnmappedProperties) {
return null;
}
return (_jsxs("div", { className: "data-quality-tree-view__node__block", children: [_jsx(QueryBuilderExplorerTreeNodeContainer, { node: node, level: level + 1, stepPaddingInRem: stepPaddingInRem, onNodeSelect: onNodeSelect, innerProps: innerProps }), node.isOpen &&
getChildNodes(node).map((childNode) => (_jsx(QueryBuilderExplorerTreeNodeView, { node: childNode, level: level + 1, onNodeSelect: onNodeSelect, getChildNodes: getChildNodes, innerProps: innerProps }, childNode.id)))] }));
});
const QueryBuilderExplorerTree = observer((props) => {
const { dataQualityState } = props;
const { dataQualityQueryBuilderState } = dataQualityState;
const explorerState = dataQualityQueryBuilderState.explorerState;
const treeData = explorerState.nonNullableTreeData;
const onNodeSelect = (node) => {
if (node.childrenIds.length) {
node.isOpen = !node.isOpen;
if (node.isOpen &&
(node instanceof QueryBuilderExplorerTreePropertyNodeData ||
node instanceof QueryBuilderExplorerTreeSubTypeNodeData) &&
node.type instanceof Class) {
(node instanceof QueryBuilderExplorerTreeSubTypeNodeData
? getAllOwnClassProperties(node.type)
: getAllClassProperties(node.type).concat(getAllClassDerivedProperties(node.type))).forEach((property) => {
const propertyTreeNodeData = getQueryBuilderPropertyNodeData(property, node, guaranteeNonNullable(explorerState.mappingModelCoverageAnalysisResult));
if (propertyTreeNodeData) {
treeData.nodes.set(propertyTreeNodeData.id, propertyTreeNodeData);
}
});
node.type._subclasses.forEach((subclass) => {
const subTypeTreeNodeData = getQueryBuilderSubTypeNodeData(subclass, node, guaranteeNonNullable(explorerState.mappingModelCoverageAnalysisResult));
treeData.nodes.set(subTypeTreeNodeData.id, subTypeTreeNodeData);
});
}
}
explorerState.refreshTree();
};
const getChildNodes = (node) => {
const dataToReturn = node.childrenIds
.map((id) => treeData.nodes.get(id))
.filter((childNode) => childNode instanceof QueryBuilderExplorerTreeSubTypeNodeData ||
childNode instanceof QueryBuilderExplorerTreePropertyNodeData)
// simple properties come first
.sort((a, b) => a.label.localeCompare(b.label))
.sort((a, b) => getQueryBuilderExplorerTreeNodeSortRank(b) -
getQueryBuilderExplorerTreeNodeSortRank(a));
return dataToReturn;
};
return (_jsx(TreeView, { components: {
TreeNodeContainer: QueryBuilderExplorerTreeNodeContainer,
TreeNodeView: QueryBuilderExplorerTreeNodeView,
}, className: "data-quality-validation-explorer-tree__root", treeData: treeData, onNodeSelect: onNodeSelect, getChildNodes: getChildNodes, innerProps: {
dataQualityState,
} }));
});
export const DataQualityExplorerPanel = observer((props) => {
const { dataQualityState } = props;
const { dataQualityQueryBuilderState } = dataQualityState;
const explorerState = dataQualityQueryBuilderState.explorerState;
const applicationStore = useApplicationStore();
const collapseTree = () => {
if (explorerState.treeData) {
Array.from(explorerState.treeData.nodes.values()).forEach((node) => {
node.isOpen = false;
});
explorerState.refreshTree();
}
};
const toggleShowUnmappedProperties = () => explorerState.setShowUnmappedProperties(!explorerState.showUnmappedProperties);
const toggleHumanizePropertyName = () => explorerState.setHumanizePropertyName(!explorerState.humanizePropertyName);
const toggleHighlightUsedProperties = () => explorerState.setHighlightUsedProperties(!explorerState.highlightUsedProperties);
useEffect(() => {
flowResult(explorerState.analyzeMappingModelCoverage()).catch((error) => {
applicationStore.alertUnhandledError(error);
});
}, [
applicationStore,
explorerState,
dataQualityQueryBuilderState.executionContextState.mapping,
]);
return (_jsxs("div", { "data-testid": DATA_QUALITY_VALIDATION_TEST_ID.DATA_QUALITY_VALIDATION_EXPLORER, className: clsx('panel data-quality-validation__explorer', {
backdrop__element: applicationStore.layoutService.showBackdrop,
}), children: [_jsx(PanelHeader, { title: "explorer", children: _jsxs(PanelHeaderActions, { children: [_jsx(PanelHeaderActionItem, { onClick: collapseTree, title: "Collapse Tree", children: _jsx(CompressIcon, {}) }), _jsx(ControlledDropdownMenu, { className: "panel__header__action", title: "Show Options Menu...", content: _jsxs(MenuContent, { children: [_jsxs(MenuContentItem, { onClick: toggleShowUnmappedProperties, children: [_jsx(MenuContentItemIcon, { children: explorerState.showUnmappedProperties ? (_jsx(CheckIcon, {})) : null }), _jsx(MenuContentItemLabel, { children: "Show Unmapped Properties" })] }), _jsxs(MenuContentItem, { onClick: toggleHumanizePropertyName, children: [_jsx(MenuContentItemIcon, { children: explorerState.humanizePropertyName ? (_jsx(CheckIcon, {})) : null }), _jsx(MenuContentItemLabel, { children: "Humanize Property Name" })] }), _jsxs(MenuContentItem, { onClick: toggleHighlightUsedProperties, children: [_jsx(MenuContentItemIcon, { children: explorerState.highlightUsedProperties ? (_jsx(CheckIcon, {})) : null }), _jsx(MenuContentItemLabel, { children: "Highlight already used properties" })] })] }), menuProps: {
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformOrigin: { vertical: 'top', horizontal: 'left' },
elevation: 7,
}, children: _jsx(MoreVerticalIcon, { className: "data-quality-validation__icon__more-options" }) })] }) }), _jsxs("div", { className: "panel__content data-quality-validation-explorer-tree__content", children: [_jsx(PanelLoadingIndicator, { isLoading: explorerState.mappingModelCoverageAnalysisState.isInProgress }), _jsx(DragPreviewLayer, { labelGetter: (item) => explorerState.humanizePropertyName
? prettyCONSTName(item.node.label)
: item.node.label, types: Object.values(QUERY_BUILDER_EXPLORER_TREE_DND_TYPE) }), explorerState.mappingModelCoverageAnalysisState.isInProgress ? (_jsx(BlankPanelContent, { children: explorerState.mappingModelCoverageAnalysisState.message })) : (_jsxs(_Fragment, { children: [!explorerState.treeData && (_jsx(BlankPanelContent, { children: "Specify the class, mapping, and runtime to start building query" })), explorerState.treeData && (_jsx(QueryBuilderExplorerTree, { dataQualityState: dataQualityState }))] }))] })] }));
});
//# sourceMappingURL=DataQualityExplorerPanel.js.map