@finos/legend-application-studio
Version:
Legend Studio application core
151 lines • 11.7 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 { Handle, Position } from '@xyflow/react';
import { EyeIcon, KeyIcon, PURE_DatabaseIcon, PURE_DatabaseTableIcon, clsx, } from '@finos/legend-art';
import { observer } from 'mobx-react-lite';
import { getColumnTypeLabel, getTableColumns, isPrimaryKey, resolveViewColumnFormula, resolveViewGroupByFormula, summarizeMilestoning, } from './DatabaseDiagramHelper.js';
import { DatabaseAnnotationDisplay } from './DatabaseAnnotationDisplay.js';
/**
* View-only React Flow node representing a single relation (Table or View).
*
* Layout: header (icon + relation name + schema badge + optional VIEW tag) +
* a list of column rows. Each row is a fixed-height grid:
* - Tables: [PK key icon | column name | column type]
* - Views: [bullet | column name | formula placeholder]
*
* Two invisible Handles (left/right) let React Flow route edges into either
* side of the box without committing to a specific column anchor.
*/
export const DatabaseTableNode = observer((props) => {
const { relation, kind, schemaName, isSelected, isJoinEndpoint, fkColumns, selectedColumn, selectedViewColumnName, viewColumnFormulas, viewGroupByFormulas, } = props.data;
const isViewKind = kind === 'view';
return (_jsxs("div", { className: clsx('database-diagram__table-node', {
'database-diagram__table-node--selected': isSelected,
'database-diagram__table-node--join-endpoint': isJoinEndpoint,
'database-diagram__table-node--view': isViewKind,
}), children: [_jsx(Handle, { type: "target", position: Position.Left, className: "database-diagram__table-node__handle", isConnectable: false }), _jsx(Handle, { type: "source", position: Position.Right, className: "database-diagram__table-node__handle", isConnectable: false }), _jsxs("div", { className: "database-diagram__table-node__header", children: [_jsx("div", { className: "database-diagram__table-node__header__icon", children: isViewKind ? _jsx(EyeIcon, {}) : _jsx(PURE_DatabaseTableIcon, {}) }), _jsx("div", { className: "database-diagram__table-node__header__name", children: relation.name }), isViewKind && (_jsx("div", { className: "database-diagram__table-node__header__kind-tag", children: "VIEW" })), isViewKind && renderViewMetadataTags(relation), !isViewKind && renderMilestoningTags(relation), _jsx("div", { className: "database-diagram__table-node__header__schema", children: schemaName }), _jsx(DatabaseAnnotationDisplay, { stereotypes: relation.stereotypes, taggedValues: relation.taggedValues, layout: "compact" })] }), _jsx("div", { className: "database-diagram__table-node__columns", children: isViewKind
? renderViewColumns(relation, viewColumnFormulas, selectedViewColumnName)
: renderTableColumns(relation, fkColumns, selectedColumn) }), isViewKind &&
renderViewGroupBySection(relation, viewGroupByFormulas)] }));
});
// Function declarations (not const arrow) so they're hoisted and the React
// component above can call them without triggering `no-use-before-define`.
function renderTableColumns(table, fkColumns, selectedColumn) {
return getTableColumns(table).map((column) => {
const isPk = isPrimaryKey(table, column.name);
const isFk = fkColumns.has(column);
const isFocused = selectedColumn === column;
return (_jsxs("div", { className: clsx('database-diagram__table-node__column', {
'database-diagram__table-node__column--pk': isPk,
'database-diagram__table-node__column--fk': isFk,
'database-diagram__table-node__column--nullable': column.nullable === true,
'database-diagram__table-node__column--focused': isFocused,
}), title: column.nullable ? `${column.name} (nullable)` : column.name, children: [_jsx("div", { className: "database-diagram__table-node__column__key", children: isPk ? _jsx(KeyIcon, {}) : null }), _jsx("div", { className: "database-diagram__table-node__column__name", children: column.name }), _jsx("div", { className: "database-diagram__table-node__column__type", children: getColumnTypeLabel(column) })] }, column.name));
});
}
/**
* View columns are defined by `view.columnMappings` rather than `Column[]`.
* Each mapping carries the column name and a relational operation that
* computes the value. We render the Pure code returned by the engine (cached
* in `DatabaseEditorState.viewColumnFormulas`); while it's still loading we
* fall back to a placeholder so the layout stays stable.
*
* The `title` attribute also surfaces the formula for tooltip-on-hover, since
* complex formulas may not fit in the 1-line display.
*/
function renderViewColumns(view, formulas, selectedViewColumnName) {
return view.columnMappings.map((mapping) => {
const isPk = isPrimaryKey(view, mapping.columnName);
const isFocused = selectedViewColumnName === mapping.columnName;
const formula = resolveViewColumnFormula(formulas, view.schema.name, view.name, mapping.columnName);
return (_jsxs("div", { className: clsx('database-diagram__table-node__column', {
'database-diagram__table-node__column--pk': isPk,
'database-diagram__table-node__column--view': true,
'database-diagram__table-node__column--focused': isFocused,
}), title: `${mapping.columnName}: ${formula}`, children: [_jsx("div", { className: "database-diagram__table-node__column__key", children: isPk ? _jsx(KeyIcon, {}) : null }), _jsx("div", { className: "database-diagram__table-node__column__name", children: mapping.columnName }), _jsx("div", { className: "database-diagram__table-node__column__type", children: formula })] }, mapping.columnName));
});
}
/**
* Render the secondary view-metadata tags shown next to the primary "VIEW"
* tag in the canvas node header. These mirror the tags rendered in the
* schema tree's view row (see `DatabaseTreeViewRow`) so the two surfaces
* stay in sync.
*
* Returns `null` when the view has none of these features set, so the
* header stays compact for "plain" views.
*/
function renderViewMetadataTags(view) {
const groupByCount = view.groupBy?.columns.length ?? 0;
const hasDistinct = view.distinct === true;
const hasFilter = Boolean(view.filter);
const hasGroupBy = groupByCount > 0;
if (!hasDistinct && !hasFilter && !hasGroupBy) {
return null;
}
return (_jsxs(_Fragment, { children: [hasDistinct && (_jsx("div", { className: "database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--distinct", title: "View applies DISTINCT", children: "DISTINCT" })), hasFilter && view.filter && (_jsx("div", { className: "database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--filtered", title: `Filtered by ${view.filter.filter.ownerReference.valueForSerialization ?? ''}.${view.filter.filterName}`, children: "FILTERED" })), hasGroupBy && (_jsx("div", { className: "database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--grouped", title: `GROUP BY ${groupByCount} expression${groupByCount === 1 ? '' : 's'}`, children: `GROUP BY (${groupByCount})` }))] }));
}
/**
* Compact header tags surfacing a table's `milestoning` configuration.
* One tag per Milestoning entry (a table can declare both business and
* processing milestoning). The tag color follows the kind classifier from
* `summarizeMilestoning` so business and processing read distinctly.
*
* Returns `null` for non-milestoned tables to keep the header compact \u2014
* the vast majority of tables are not milestoned.
*/
function renderMilestoningTags(table) {
if (table.milestoning.length === 0) {
return null;
}
return (_jsx(_Fragment, { children: table.milestoning.map((milestoning) => {
const summary = summarizeMilestoning(milestoning);
return (_jsx("div", { className: clsx('database-diagram__table-node__header__kind-tag', `database-diagram__table-node__header__kind-tag--milestoned-${summary.kind}`), title: summary.description, children: summary.label }, summary.label));
}) }));
}
/**
* Footer rendered under the column rows when a view declares `groupBy`.
* Lists each grouping expression as one row of Pure code (lazy-resolved
* from `viewGroupByFormulas` — falls back to a placeholder while the
* batched engine call is in flight).
*
* Returns `null` for views with no groupBy and for table-kind nodes, so the
* footer doesn't add visual weight to nodes that don't need it.
*/
function renderViewGroupBySection(view, formulas) {
const columns = view.groupBy?.columns ?? [];
if (columns.length === 0) {
return null;
}
return (_jsxs("div", { className: "database-diagram__table-node__group-by", title: `GROUP BY (${columns.length})`, children: [_jsx("div", { className: "database-diagram__table-node__group-by__header", children: "GROUP BY" }), columns.map((_, index) => {
const formula = resolveViewGroupByFormula(formulas, view.schema.name, view.name, index);
return (_jsxs("div", { className: "database-diagram__table-node__group-by__row", title: formula, children: [_jsx("span", { className: "database-diagram__table-node__group-by__index", children: `[${index}]` }), _jsx("span", { className: "database-diagram__table-node__group-by__formula", children: formula })] }, formula));
})] }));
}
/**
* Compact stub rendered when a join references a relation in another
* database. Shows the schema/name plus the owning database path, with a
* dashed border to distinguish it from real in-database table nodes. Two
* invisible Handles let React Flow attach edges to either side. Not
* selectable — the canvas swallows clicks on stubs.
*/
export const DatabaseForeignRelationStubNode = observer((props) => {
const { schemaName, relationName, ownerPath, isJoinEndpoint } = props.data;
return (_jsxs("div", { className: clsx('database-diagram__table-node', 'database-diagram__table-node--foreign-stub', {
'database-diagram__table-node--join-endpoint': isJoinEndpoint,
}), title: `${schemaName}.${relationName}\n(in database: ${ownerPath})`, children: [_jsx(Handle, { type: "target", position: Position.Left, className: "database-diagram__table-node__handle", isConnectable: false }), _jsx(Handle, { type: "source", position: Position.Right, className: "database-diagram__table-node__handle", isConnectable: false }), _jsxs("div", { className: "database-diagram__table-node__header", children: [_jsx("div", { className: "database-diagram__table-node__header__icon", children: _jsx(PURE_DatabaseIcon, {}) }), _jsx("div", { className: "database-diagram__table-node__header__name", children: `${schemaName}.${relationName}` })] }), _jsx("div", { className: "database-diagram__table-node__foreign-stub__owner", children: ownerPath })] }));
});
//# sourceMappingURL=DatabaseTableNode.js.map