UNPKG

@finos/legend-application-studio

Version:
207 lines 9.85 kB
import { jsx as _jsx, jsxs as _jsxs } 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 { useState, useEffect, useRef } from 'react'; import { useDrag } from 'react-dnd'; import { PURE_DatabaseTableJoinIcon, TreeView, ChevronDownIcon, ChevronRightIcon, } from '@finos/legend-art'; import { addUniqueEntry, assertTrue, filterByType, guaranteeType, isNonNullable, } from '@finos/legend-shared'; import { renderColumnTypeIcon } from '../../connection-editor/DatabaseEditorHelper.js'; import { Column, stringifyDataType, } from '@finos/legend-graph'; export const TABLE_ELEMENT_DND_TYPE = 'TABLE_ELEMENT_DND_TYPE'; const JOIN_OPERATOR = '>'; const JOIN_AT_SYMBOL = '@'; const JOIN_PIPE_SYMBOL = '|'; const generateDatabasePointerText = (database) => `[${database}]`; export class TableOrViewTreeNodeData { id; label; isSelected; isOpen; childrenIds; relation; constructor(id, label, relation) { this.id = id; this.label = label; this.relation = relation; } } export class ColumnNodeData extends TableOrViewTreeNodeData { column; constructor(id, label, relation, column) { super(id, label, relation); this.column = column; } } export class JoinNodeData extends TableOrViewTreeNodeData { join; constructor(id, label, relation, join) { super(id, label, relation); this.join = join; } } export class TableOrViewTreeNodeDragSource { data; constructor(data) { this.data = data; } } const generateColumnTreeNodeId = (column, relation, parentNode) => parentNode ? parentNode instanceof JoinNodeData ? `${parentNode.id} ${JOIN_PIPE_SYMBOL} ${relation.name}.${column.name}` : `${parentNode.id}.${column.name}` : `${generateDatabasePointerText(relation.schema._OWNER.path)}${relation.schema.name}.${relation.name}.${column.name}`; const getColumnTreeNodeData = (column, relation, parentNode) => { const columnNode = new ColumnNodeData(generateColumnTreeNodeId(column, relation, parentNode), column.name, relation, column); return columnNode; }; // TODO: support more complex join feature (with operation, direction, etc.) const generateJoinTreeNodeId = (join, parentNode) => parentNode ? `${parentNode.id} ${JOIN_OPERATOR} ${JOIN_AT_SYMBOL}${join.name}` : `${generateDatabasePointerText(join.owner.path)}${JOIN_AT_SYMBOL}${join.name}`; const resolveJoinTargetRelation = (join, sourceRelation) => { const potentialTargetRelations = new Set(); join.aliases.forEach((alias) => { if (alias.first.relation.value !== sourceRelation) { potentialTargetRelations.add(alias.first.relation.value); } if (alias.second.relation.value !== sourceRelation) { potentialTargetRelations.add(alias.second.relation.value); } }); assertTrue(potentialTargetRelations.size < 2, `Can't resolve target relation for join`); return potentialTargetRelations.size === 0 ? sourceRelation : Array.from(potentialTargetRelations.values())[0]; }; const getJoinTreeNodeData = (join, relation, parentNode) => { const joinNode = new JoinNodeData(generateJoinTreeNodeId(join, parentNode), join.name, relation, join); const childrenIds = []; // columns relation.columns .slice() .filter(filterByType(Column)) .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) .forEach((col) => { addUniqueEntry(childrenIds, generateColumnTreeNodeId(col, relation, joinNode)); }); // joins relation.schema._OWNER.joins .slice() .filter((_join) => _join.aliases.filter((alias) => alias.first.relation.value === relation || alias.second.relation.value === relation).length > 0) .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) .forEach((childJoin) => { addUniqueEntry(childrenIds, generateJoinTreeNodeId(childJoin, joinNode)); }); joinNode.childrenIds = childrenIds; return joinNode; }; const getRelationTreeData = (relation) => { const rootIds = []; const nodes = new Map(); // columns relation.columns .slice() .filter(filterByType(Column)) .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) .forEach((col) => { const columnNode = getColumnTreeNodeData(col, relation, undefined); addUniqueEntry(rootIds, columnNode.id); nodes.set(columnNode.id, columnNode); }); // joins relation.schema._OWNER.joins .slice() .filter((join) => join.aliases.filter((alias) => alias.first.relation.value === relation || alias.second.relation.value === relation).length > 0) .sort((a, b) => a.name.toString().localeCompare(b.name.toString())) .forEach((join) => { const joinNode = getJoinTreeNodeData(join, resolveJoinTargetRelation(join, relation), undefined); addUniqueEntry(rootIds, joinNode.id); nodes.set(joinNode.id, joinNode); }); return { rootIds, nodes }; }; const RelationalOperationElementTreeNodeContainer = (props) => { const { node, level, stepPaddingInRem, onNodeSelect } = props; const [, dragConnector] = useDrag(() => ({ type: TABLE_ELEMENT_DND_TYPE, item: new TableOrViewTreeNodeDragSource(node), }), [node]); const ref = useRef(null); dragConnector(ref); const isExpandable = Boolean(node.childrenIds?.length); const nodeTypeIcon = node instanceof ColumnNodeData ? (renderColumnTypeIcon(node.column.type)) : (_jsx(PURE_DatabaseTableJoinIcon, {})); const selectNode = () => onNodeSelect?.(node); const nodeExpandIcon = isExpandable ? (node.isOpen ? (_jsx(ChevronDownIcon, {})) : (_jsx(ChevronRightIcon, {}))) : (_jsx("div", {})); return (_jsxs("div", { className: "tree-view__node__container", onClick: selectNode, ref: ref, style: { paddingLeft: `${(level - 1) * (stepPaddingInRem ?? 1)}rem`, display: 'flex', }, children: [_jsxs("div", { className: "tree-view__node__icon", children: [_jsx("div", { className: "tree-view__node__expand-icon", children: nodeExpandIcon }), _jsx("div", { className: "type-tree__type-icon", children: nodeTypeIcon })] }), _jsxs("div", { className: "tree-view__node__label type-tree__node__label", children: [_jsx("button", { tabIndex: -1, title: `${node.id}`, children: node.label }), node instanceof ColumnNodeData && (_jsx("div", { className: "type-tree__node__type", children: _jsx("button", { className: "type-tree__node__type__label", // TODO: match type // className={clsx('type-tree__node__type__label', { // 'type-tree__node__type__label--highlighted': // primitiveType && primitiveType === selectedType, // })} tabIndex: -1, title: "Column Type", children: stringifyDataType(guaranteeType(node.column, Column).type) }) }))] })] })); }; export const TableOrViewSourceTree = (props) => { const { relation, selectedType } = props; // NOTE: We only need to compute this once so we use lazy initial state syntax // See https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [treeData, setTreeData] = useState(() => getRelationTreeData(relation)); const onNodeSelect = (node) => { if (node.childrenIds?.length) { node.isOpen = !node.isOpen; // columns node.relation.columns.filter(filterByType(Column)).forEach((col) => { const columnNode = getColumnTreeNodeData(col, node.relation, node); treeData.nodes.set(columnNode.id, columnNode); }); // joins node.relation.schema._OWNER.joins .filter((join) => join.aliases.filter((alias) => alias.first.relation.value === node.relation || alias.second.relation.value === node.relation).length > 0) .forEach((join) => { const joinNode = getJoinTreeNodeData(join, resolveJoinTargetRelation(join, node.relation), node); treeData.nodes.set(joinNode.id, joinNode); }); } setTreeData({ ...treeData }); }; const getChildNodes = (node) => { if (!node.childrenIds) { return []; } const childrenNodes = node.childrenIds .map((id) => treeData.nodes.get(id)) .filter(isNonNullable) // sort so that column nodes come before join nodes .sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => (b instanceof ColumnNodeData ? 1 : 0) - (a instanceof ColumnNodeData ? 1 : 0)); return childrenNodes; }; useEffect(() => { setTreeData(() => getRelationTreeData(relation)); }, [relation]); return (_jsx(TreeView, { components: { TreeNodeContainer: RelationalOperationElementTreeNodeContainer, }, treeData: treeData, getChildNodes: getChildNodes, onNodeSelect: onNodeSelect, innerProps: { selectedType, } })); }; //# sourceMappingURL=TableOrViewSourceTree.js.map