@finos/legend-application-studio
Version:
Legend Studio application core
207 lines • 9.85 kB
JavaScript
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