@finos/legend-application-studio
Version:
Legend Studio application core
331 lines • 31.3 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 { observer } from 'mobx-react-lite';
import { useState } from 'react';
import { ChevronDownIcon, ChevronRightIcon, CompressIcon, CopyIcon, ExpandAllIcon, ExternalLinkIcon, EyeIcon, FilterIcon, KeyIcon, PURE_DatabaseIcon, PURE_DatabaseSchemaIcon, PURE_DatabaseTableIcon, PURE_DatabaseTableJoinIcon, PURE_DataProductIcon, SearchIcon, TimesIcon, clsx, } from '@finos/legend-art';
import {} from '@finos/legend-graph';
import { DatabaseAnnotationDisplay } from './DatabaseAnnotationDisplay.js';
import { getRelationNodeId, getSchemaNodeId, } from '../../../../stores/editor/editor-state/element-editor-state/DatabaseEditorState.js';
import { getColumnTypeLabel, getTableColumns, isCrossDatabaseJoin, isPrimaryKey, isSelfJoin, matchesSearch, resolveFilterFormula, resolveJoinFormula, resolveViewColumnFormula, resolveViewGroupByFormula, summarizeMilestoning, } from './DatabaseDiagramHelper.js';
/**
* Small inline copy-to-clipboard button rendered next to rendered Pure code
* (filter/join/view-column/groupBy formulas). Two-state visual: shows the
* default copy icon, briefly flips to a "Copied!" label after a successful
* write, then resets. Click stops propagation so the surrounding row's
* click handler (which usually selects/focuses the row) doesn't fire.
*/
const CopyFormulaButton = ({ value, label = 'Copy', }) => {
const [copied, setCopied] = useState(false);
return (_jsx("button", { type: "button", className: clsx('database-diagram__copy-btn', {
'database-diagram__copy-btn--copied': copied,
}), title: copied ? 'Copied!' : label, onClick: (event) => {
event.stopPropagation();
// `navigator.clipboard` may be unavailable in test/insecure contexts;
// fall through silently in that case rather than throwing.
navigator.clipboard
.writeText(value)
.then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
})
.catch(() => {
/* no-op \u2014 copy failures are non-fatal */
});
}, children: _jsx(CopyIcon, {}) }));
};
/**
* Pure-CSS shimmer placeholder shown in place of formula text while the
* batched engine call is still in flight. We swap from skeleton to text
* once the corresponding `is...Loading` flag flips false on the editor
* state. Compact one-line bar so layout doesn't shift on resolve.
*/
const FormulaSkeleton = () => (_jsx("span", { className: "database-diagram__skeleton", "aria-label": "Loading formula", "aria-busy": true }));
/**
* Empty-state row shown when a side-panel section has zero rows of its
* primary kind (e.g. a database with no joins). Lighter than rendering
* nothing because users sometimes wonder if the panel is broken when a
* section is silently absent.
*/
const EmptySectionRow = ({ message }) => (_jsx("div", { className: "database-diagram__side-panel__empty", children: message }));
/**
* Predicates that determine whether a tree node should be rendered under
* the current search filter. A node is shown when it OR any descendant
* matches the (already lowercased) query. Empty query short-circuits so
* everything is shown, which keeps the no-filter render path cheap.
*/
const relationMatchesSearch = (rel, query) => {
if (!query) {
return true;
}
if (matchesSearch(rel.name, query)) {
return true;
}
// For tables walk Column.name; for views walk ColumnMapping.columnName.
// Either way the comparison is a flat substring on visible identifiers.
if ('columnMappings' in rel) {
return rel.columnMappings.some((m) => matchesSearch(m.columnName, query));
}
return getTableColumns(rel).some((c) => matchesSearch(c.name, query));
};
const schemaMatchesSearch = (schema, query) => {
if (!query) {
return true;
}
if (matchesSearch(schema.name, query)) {
return true;
}
return (schema.tables.some((t) => relationMatchesSearch(t, query)) ||
schema.views.some((v) => relationMatchesSearch(v, query)));
};
/**
* Renders one column under an expanded Table. Clicking focuses the column,
* which (per `DatabaseEditorState.focusOnColumn`) selects the parent table,
* marks the specific column, and requests a canvas pan.
*
* Columns aren't expandable — they're leaves of the tree.
*/
const DatabaseTreeTableColumnRow = observer((props) => {
const { editorState, table, column } = props;
const isSelected = editorState.selectedColumn === column;
const isPK = isPrimaryKey(table, column.name);
return (_jsxs("button", { type: "button", className: clsx('database-diagram__tree__column', {
'database-diagram__tree__column--selected': isSelected,
'database-diagram__tree__column--pk': isPK,
}), onClick: () => editorState.focusOnColumn(table, column), title: `${table.schema.name}.${table.name}.${column.name}: ${getColumnTypeLabel(column)}`, children: [_jsx("span", { className: "database-diagram__tree__column__icon", children: isPK ? (_jsx(KeyIcon, {})) : (_jsx("span", { className: "database-diagram__tree__column__bullet", children: "\u2022" })) }), _jsxs("span", { className: "database-diagram__tree__column__name", children: [column.name, column.nullable === true && (_jsx("span", { className: "database-diagram__tree__column__nullable", title: "Nullable", children: "?" }))] }), _jsx("span", { className: "database-diagram__tree__column__type", children: getColumnTypeLabel(column) }), _jsx(DatabaseAnnotationDisplay, { stereotypes: column.stereotypes, taggedValues: column.taggedValues, layout: "compact" })] }));
});
/**
* Renders one column-mapping under an expanded View. Visually similar to a
* table-column row but the secondary text is the formula placeholder rather
* than a SQL type. Not selectable individually in the MVP — clicking just
* focuses the parent view.
*/
const DatabaseTreeViewColumnRow = observer((props) => {
const { editorState, view, columnMapping } = props;
const isPK = isPrimaryKey(view, columnMapping.columnName);
const isSelected = editorState.selectedRelation === view &&
editorState.selectedViewColumnName === columnMapping.columnName;
// Pull the live formula from the editor state. Re-renders when the
// batched engine call resolves and updates `viewColumnFormulas`.
const formula = resolveViewColumnFormula(editorState.viewColumnFormulas, view.schema.name, view.name, columnMapping.columnName);
const isLoading = editorState.isLoadingViewColumnFormulas &&
!editorState.viewColumnFormulas.has(`${view.schema.name}.${view.name}.${columnMapping.columnName}`);
return (_jsxs("button", { type: "button", className: clsx('database-diagram__tree__column', 'database-diagram__tree__column--view', {
'database-diagram__tree__column--pk': isPK,
'database-diagram__tree__column--selected': isSelected,
}), onClick: () => editorState.focusOnViewColumn(view, columnMapping.columnName), title: `${view.schema.name}.${view.name}.${columnMapping.columnName}: ${formula}`, children: [_jsx("span", { className: "database-diagram__tree__column__icon", children: isPK ? (_jsx(KeyIcon, {})) : (_jsx("span", { className: "database-diagram__tree__column__bullet", children: "\u2022" })) }), _jsx("span", { className: "database-diagram__tree__column__name", children: columnMapping.columnName }), _jsx("span", { className: "database-diagram__tree__column__type", children: isLoading ? _jsx(FormulaSkeleton, {}) : formula }), !isLoading && (_jsx(CopyFormulaButton, { value: formula, label: "Copy formula" }))] }));
});
/**
* Renders one Table row plus (when expanded) its Column children. Clicking
* the row both *selects the table on the canvas* (with a pan-to) AND toggles
* expansion. Bundling these is intentional — when a user navigates to a
* relation, they almost always want to see its columns too.
*/
const DatabaseTreeTableRow = observer((props) => {
const { editorState, table } = props;
const query = editorState.searchText.trim().toLowerCase();
if (!relationMatchesSearch(table, query)) {
return null;
}
const id = getRelationNodeId(table.schema.name, table.name);
// When a filter is active force-expand the row so matches deeper in
// the tree are immediately visible without an extra click.
const isExpanded = query !== '' || editorState.expandedRelationIds.has(id);
const isSelected = editorState.selectedRelation === table;
const columns = getTableColumns(table);
// When filtering, only show columns that match the query (or all
// columns if the table itself matched by name).
const tableNameMatches = matchesSearch(table.name, query);
const visibleColumns = query === '' || tableNameMatches
? columns
: columns.filter((c) => matchesSearch(c.name, query));
return (_jsxs("div", { className: "database-diagram__tree__table", children: [_jsxs("button", { type: "button", className: clsx('database-diagram__tree__table__row', {
'database-diagram__tree__table__row--selected': isSelected,
}), onClick: () => {
editorState.focusOnRelation(table);
editorState.toggleRelationExpanded(id);
}, title: `${table.schema.name}.${table.name}`, children: [_jsx("span", { className: "database-diagram__tree__caret", children: isExpanded ? _jsx(ChevronDownIcon, {}) : _jsx(ChevronRightIcon, {}) }), _jsx(PURE_DatabaseTableIcon, {}), _jsx("span", { className: "database-diagram__tree__table__name", children: table.name }), _jsx("span", { className: "database-diagram__tree__table__count", children: columns.length }), _jsx(DatabaseAnnotationDisplay, { stereotypes: table.stereotypes, taggedValues: table.taggedValues, layout: "compact" })] }), isExpanded && (_jsxs("div", { className: "database-diagram__tree__table__children", children: [table.milestoning.map((milestoning) => {
const summary = summarizeMilestoning(milestoning);
return (_jsxs("div", { className: clsx('database-diagram__tree__table__meta-row', `database-diagram__tree__table__meta-row--milestoning-${summary.kind}`), title: summary.description, children: [_jsx("span", { className: "database-diagram__tree__table__meta-row__label", children: "milestoning" }), _jsx("span", { className: "database-diagram__tree__table__meta-row__value", children: summary.label })] }, summary.label));
}), visibleColumns.map((column) => (_jsx(DatabaseTreeTableColumnRow, { editorState: editorState, table: table, column: column }, column.name)))] }))] }));
});
/**
* Renders the View → Filter mapping reference under an expanded view. Shows
* `<owning-database>.<filterName>`. When the referenced filter belongs to
* the database currently being edited, clicking navigates to it via
* `focusOnFilter`; cross-database references are non-interactive (filters
* from other databases aren't reachable from this editor).
*/
const DatabaseTreeViewFilterRow = observer((props) => {
const { editorState, view, filterMapping } = props;
const ownerPath = filterMapping.filter.ownerReference.valueForSerialization ?? '';
const filterValue = filterMapping.filter.value;
// Filters live on a Database; if the owning DB matches the one we're
// editing we can navigate. Otherwise the row is informational.
const isLocal = filterValue.owner === editorState.database;
const display = `${ownerPath}.${filterMapping.filterName}`;
const formula = isLocal
? resolveFilterFormula(editorState.filterFormulas, filterValue.name)
: undefined;
return (_jsxs("button", { type: "button", className: clsx('database-diagram__tree__column', 'database-diagram__tree__column--view', 'database-diagram__tree__column--view-meta'), onClick: () => {
if (isLocal) {
editorState.focusOnFilter(filterValue);
}
else {
editorState.focusOnRelation(view);
}
}, disabled: !isLocal, title: formula
? `${display}: ${formula}`
: `${display}${isLocal ? '' : ' (external)'}`, children: [_jsx("span", { className: "database-diagram__tree__column__icon", children: _jsx(FilterIcon, {}) }), _jsx("span", { className: "database-diagram__tree__column__name", children: "filter" }), _jsx("span", { className: "database-diagram__tree__column__type", children: display })] }));
});
/**
* Renders one row per `View.groupBy.columns[i]` Pure expression. Each row
* lazily reads its rendered formula from `viewGroupByFormulas` — until the
* batched engine call resolves it falls back to a static placeholder.
*
* Rows aren't clickable: the underlying RelationalOperationElement isn't a
* navigable graph node and the parent view is already focused via the
* surrounding tree row.
*/
const DatabaseTreeViewGroupByRow = observer((props) => {
const { editorState, view, index } = props;
const formula = resolveViewGroupByFormula(editorState.viewGroupByFormulas, view.schema.name, view.name, index);
const isLoading = editorState.isLoadingViewGroupByFormulas &&
!editorState.viewGroupByFormulas.has(`${view.schema.name}.${view.name}.groupBy[${index}]`);
return (_jsxs("div", { className: clsx('database-diagram__tree__column', 'database-diagram__tree__column--view', 'database-diagram__tree__column--view-meta', 'database-diagram__tree__column--readonly'), title: `group by [${index}]: ${formula}`, children: [_jsx("span", { className: "database-diagram__tree__column__icon", children: _jsx("span", { className: "database-diagram__tree__column__bullet", children: "\u00B7" }) }), _jsx("span", { className: "database-diagram__tree__column__name", children: `[${index}]` }), _jsx("span", { className: "database-diagram__tree__column__type", children: isLoading ? _jsx(FormulaSkeleton, {}) : formula }), !isLoading && (_jsx(CopyFormulaButton, { value: formula, label: "Copy expression" }))] }));
});
/**
* Renders one View row plus (when expanded) its column-mapping children.
* Visually distinct from the table row via the eye icon. Same click semantics
* as tables (select + expand).
*/
const DatabaseTreeViewRow = observer((props) => {
const { editorState, view } = props;
const query = editorState.searchText.trim().toLowerCase();
if (!relationMatchesSearch(view, query)) {
return null;
}
const id = getRelationNodeId(view.schema.name, view.name);
// Same auto-expand-on-filter behavior as tables.
const isExpanded = query !== '' || editorState.expandedRelationIds.has(id);
const isSelected = editorState.selectedRelation === view;
const groupBy = view.groupBy;
const groupByCount = groupBy?.columns.length ?? 0;
const viewNameMatches = matchesSearch(view.name, query);
const visibleMappings = query === '' || viewNameMatches
? view.columnMappings
: view.columnMappings.filter((m) => matchesSearch(m.columnName, query));
return (_jsxs("div", { className: "database-diagram__tree__table database-diagram__tree__table--view", children: [_jsxs("button", { type: "button", className: clsx('database-diagram__tree__table__row', {
'database-diagram__tree__table__row--selected': isSelected,
}), onClick: () => {
editorState.focusOnRelation(view);
editorState.toggleRelationExpanded(id);
}, title: `${view.schema.name}.${view.name} (view)`, children: [_jsx("span", { className: "database-diagram__tree__caret", children: isExpanded ? _jsx(ChevronDownIcon, {}) : _jsx(ChevronRightIcon, {}) }), _jsx(EyeIcon, {}), _jsx("span", { className: "database-diagram__tree__table__name", children: view.name }), _jsx("span", { className: "database-diagram__tree__table__count", children: view.columnMappings.length }), view.distinct === true && (_jsx("span", { className: "database-diagram__tree__table__view-tag database-diagram__tree__table__view-tag--distinct", title: "View applies DISTINCT", children: "DISTINCT" })), view.filter && (_jsx("span", { className: "database-diagram__tree__table__view-tag database-diagram__tree__table__view-tag--filtered", title: `Filtered by ${view.filter.filter.ownerReference.valueForSerialization ?? ''}.${view.filter.filterName}`, children: "FILTERED" })), groupByCount > 0 && (_jsx("span", { className: "database-diagram__tree__table__view-tag database-diagram__tree__table__view-tag--grouped", title: `GROUP BY ${groupByCount} expression${groupByCount === 1 ? '' : 's'}`, children: `GROUP BY (${groupByCount})` })), _jsx(DatabaseAnnotationDisplay, { stereotypes: view.stereotypes, taggedValues: view.taggedValues, layout: "compact" })] }), isExpanded && (_jsxs("div", { className: "database-diagram__tree__table__children", children: [visibleMappings.map((mapping) => (_jsx(DatabaseTreeViewColumnRow, { editorState: editorState, view: view, columnMapping: mapping }, mapping.columnName))), query === '' && view.filter && (_jsx(DatabaseTreeViewFilterRow, { editorState: editorState, view: view, filterMapping: view.filter })), query === '' &&
groupBy?.columns.map((_, index) => (_jsx(DatabaseTreeViewGroupByRow
// Ordering is the only identity for groupBy columns.
// eslint-disable-next-line react/no-array-index-key
, { editorState: editorState, view: view, index: index }, `groupBy:${index}`)))] }))] }));
});
/**
* Renders one Schema header plus (when expanded) its Table and View children.
* Tables come before views within each schema — both kinds are siblings in
* the tree, distinguished only by icon and the column-row content.
*
* Schemas don't have a "selected" state — they only toggle.
*/
const DatabaseTreeSchemaRow = observer((props) => {
const { editorState, schema } = props;
const query = editorState.searchText.trim().toLowerCase();
if (!schemaMatchesSearch(schema, query)) {
return null;
}
const schemaId = getSchemaNodeId(schema.name);
// Force-expand under search so matches are immediately visible.
const isExpanded = query !== '' || editorState.expandedSchemaIds.has(schemaId);
const totalChildren = schema.tables.length + schema.views.length;
return (_jsxs("div", { className: "database-diagram__tree__schema", children: [_jsxs("button", { type: "button", className: "database-diagram__tree__schema__row", onClick: () => editorState.toggleSchemaExpanded(schemaId), children: [_jsx("span", { className: "database-diagram__tree__caret", children: isExpanded ? _jsx(ChevronDownIcon, {}) : _jsx(ChevronRightIcon, {}) }), _jsx(PURE_DatabaseSchemaIcon, {}), _jsx("span", { className: "database-diagram__tree__schema__name", children: schema.name }), _jsx("span", { className: "database-diagram__tree__schema__count", children: totalChildren }), _jsx(DatabaseAnnotationDisplay, { stereotypes: schema.stereotypes, taggedValues: schema.taggedValues, layout: "compact" })] }), isExpanded && (_jsxs("div", { className: "database-diagram__tree__schema__children", children: [schema.tables.map((table) => (_jsx(DatabaseTreeTableRow, { editorState: editorState, table: table }, `table:${table.name}`))), schema.views.map((view) => (_jsx(DatabaseTreeViewRow, { editorState: editorState, view: view }, `view:${view.name}`)))] }))] }));
});
/**
* Renders one Lakehouse `IncludeStore` reference: an `ExternalLinkIcon`-
* suffixed button whose body shows the generator element path and a small
* `storeType` badge to differentiate the kinds of generator (DataProduct vs
* IngestDefinition vs whatever future types appear). Clicking opens the
* generator element in a new tab — same affordance as the classic
* "Included Stores" rows above. Kept as a named sub-component (not an
* inline IIFE) so the JSX in `DatabaseSchemaTree` stays readable.
*/
const LakehouseStoreRow = observer((props) => {
const { editorState, spec } = props;
const path = spec.packageableElementPointer.valueForSerialization ??
spec.packageableElementPointer.value.path;
return (_jsxs("button", { type: "button", className: "database-diagram__side-panel__included-store", title: `${path}\n(generator: ${spec.storeType})\n\n(click to open)`, onClick: () => editorState.editorStore.graphEditorMode.openElement(spec.packageableElementPointer.value), children: [_jsx(PURE_DataProductIcon, {}), _jsx("span", { className: "database-diagram__side-panel__included-store__path", children: path }), _jsx("span", { className: "database-diagram__side-panel__included-store__type", children: spec.storeType }), _jsx("span", { className: "database-diagram__side-panel__included-store__open", children: _jsx(ExternalLinkIcon, {}) })] }));
});
/**
* The full side-panel tree. Renders schemas → (tables, views) → columns plus
* a flat "Joins" section at the bottom (joins are cross-relation
* relationships and don't naturally fit in the tree hierarchy).
*
* State (selection, expansion) lives entirely in `DatabaseEditorState` so it
* survives reprocessing and so other components (the canvas) can react to it.
*/
export const DatabaseSchemaTree = observer((props) => {
const { editorState } = props;
const { database } = editorState;
const query = editorState.searchText.trim().toLowerCase();
// Pre-compute filtered counts so each section header surfaces "showing
// N of M" feedback when the user is filtering. Cheap walks on the
// already-typed query \u2014 we don't memoize because the per-render cost
// is dominated by row JSX, not these filters.
const visibleSchemas = query === ''
? database.schemas
: database.schemas.filter((s) => schemaMatchesSearch(s, query));
const visibleJoins = query === ''
? database.joins
: database.joins.filter((j) => matchesSearch(j.name, query));
const visibleFilters = query === ''
? database.filters
: database.filters.filter((f) => matchesSearch(f.name, query));
return (_jsxs("div", { className: "database-diagram__side-panel", children: [_jsxs("div", { className: "database-diagram__side-panel__toolbar", children: [_jsxs("div", { className: "database-diagram__side-panel__search", children: [_jsx(SearchIcon, {}), _jsx("input", { type: "text", className: "database-diagram__side-panel__search__input", placeholder: "Filter schemas, tables, columns...", value: editorState.searchText, onChange: (e) => editorState.setSearchText(e.target.value), spellCheck: false }), editorState.searchText !== '' && (_jsx("button", { type: "button", className: "database-diagram__side-panel__search__clear", title: "Clear filter", onClick: () => editorState.setSearchText(''), children: _jsx(TimesIcon, {}) }))] }), _jsx("button", { type: "button", className: "database-diagram__side-panel__toolbar__btn", title: "Expand all schemas", onClick: () => editorState.expandAllSchemas(), children: _jsx(ExpandAllIcon, {}) }), _jsx("button", { type: "button", className: "database-diagram__side-panel__toolbar__btn", title: "Collapse all", onClick: () => editorState.collapseAll(), children: _jsx(CompressIcon, {}) })] }), (database.stereotypes.length > 0 ||
database.taggedValues.length > 0) && (_jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsx("div", { className: "database-diagram__side-panel__section__header", children: "Annotations" }), _jsx("div", { className: "database-diagram__side-panel__annotations", children: _jsx(DatabaseAnnotationDisplay, { stereotypes: database.stereotypes, taggedValues: database.taggedValues, layout: "block" }) })] })), _jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsxs("div", { className: "database-diagram__side-panel__section__header", children: ["Schemas", ' ', _jsx("span", { className: "database-diagram__side-panel__section__count", children: query === ''
? `(${database.schemas.length})`
: `(${visibleSchemas.length}/${database.schemas.length})` })] }), database.schemas.length === 0 ? (_jsx(EmptySectionRow, { message: "No schemas defined." })) : visibleSchemas.length === 0 ? (_jsx(EmptySectionRow, { message: "No schemas match the current filter." })) : (database.schemas.map((schema) => (_jsx(DatabaseTreeSchemaRow, { editorState: editorState, schema: schema }, schema.name))))] }), _jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsxs("div", { className: "database-diagram__side-panel__section__header", children: ["Joins", ' ', _jsx("span", { className: "database-diagram__side-panel__section__count", children: query === ''
? `(${database.joins.length})`
: `(${visibleJoins.length}/${database.joins.length})` })] }), database.joins.length === 0 ? (_jsx(EmptySectionRow, { message: "No joins defined." })) : visibleJoins.length === 0 ? (_jsx(EmptySectionRow, { message: "No joins match the current filter." })) : (visibleJoins.map((join) => {
const isSelected = editorState.selectedJoin === join;
const formula = resolveJoinFormula(editorState.joinFormulas, join.name);
const isLoading = editorState.isLoadingJoinFormulas &&
!editorState.joinFormulas.has(join.name);
const selfJoin = isSelfJoin(join);
const crossDb = !selfJoin && isCrossDatabaseJoin(join, database);
return (_jsxs("button", { type: "button", className: clsx('database-diagram__side-panel__join', {
'database-diagram__side-panel__join--selected': isSelected,
}), onClick: () => editorState.focusOnJoin(join), title: `${join.name}: ${formula}`, children: [_jsx("span", { className: "database-diagram__side-panel__join__icon", children: _jsx(PURE_DatabaseTableJoinIcon, {}) }), _jsxs("span", { className: "database-diagram__side-panel__join__body", children: [_jsxs("span", { className: "database-diagram__side-panel__join__name", children: [join.name, selfJoin && (_jsx("span", { className: "database-diagram__side-panel__join__marker database-diagram__side-panel__join__marker--self", title: "Self-join (source and target are the same relation)", children: "SELF" })), crossDb && (_jsx("span", { className: "database-diagram__side-panel__join__marker database-diagram__side-panel__join__marker--cross-db", title: "Cross-database join (one or both endpoints live in an included store)", children: "CROSS-DB" }))] }), _jsx("span", { className: "database-diagram__side-panel__join__formula", children: isLoading ? _jsx(FormulaSkeleton, {}) : formula })] }), !isLoading && (_jsx(CopyFormulaButton, { value: formula, label: "Copy join formula" }))] }, join.name));
}))] }), _jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsxs("div", { className: "database-diagram__side-panel__section__header", children: ["Filters", ' ', _jsx("span", { className: "database-diagram__side-panel__section__count", children: query === ''
? `(${database.filters.length})`
: `(${visibleFilters.length}/${database.filters.length})` })] }), database.filters.length === 0 ? (_jsx(EmptySectionRow, { message: "No filters defined." })) : visibleFilters.length === 0 ? (_jsx(EmptySectionRow, { message: "No filters match the current filter." })) : (visibleFilters.map((filter) => {
const isSelected = editorState.selectedFilter === filter;
const formula = resolveFilterFormula(editorState.filterFormulas, filter.name);
const isLoading = editorState.isLoadingFilterFormulas &&
!editorState.filterFormulas.has(filter.name);
return (_jsxs("button", { type: "button", className: clsx('database-diagram__side-panel__filter', {
'database-diagram__side-panel__filter--selected': isSelected,
}), onClick: () => editorState.focusOnFilter(filter), title: `${filter.name}: ${formula}`, children: [_jsx("span", { className: "database-diagram__side-panel__filter__icon", children: _jsx(FilterIcon, {}) }), _jsxs("span", { className: "database-diagram__side-panel__filter__body", children: [_jsx("span", { className: "database-diagram__side-panel__filter__name", children: filter.name }), _jsx("span", { className: "database-diagram__side-panel__filter__formula", children: isLoading ? _jsx(FormulaSkeleton, {}) : formula })] }), !isLoading && (_jsx(CopyFormulaButton, { value: formula, label: "Copy filter formula" }))] }, filter.name));
}))] }), database.includes.length > 0 && (_jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsxs("div", { className: "database-diagram__side-panel__section__header", children: ["Included Stores", ' ', _jsxs("span", { className: "database-diagram__side-panel__section__count", children: ["(", database.includes.length, ")"] })] }), database.includes.map((ref) => {
const path = ref.valueForSerialization ?? ref.value.path;
return (_jsxs("button", { type: "button", className: "database-diagram__side-panel__included-store", title: `${path}\n\n(click to open)`, onClick: () => editorState.editorStore.graphEditorMode.openElement(ref.value), children: [_jsx(PURE_DatabaseIcon, {}), _jsx("span", { className: "database-diagram__side-panel__included-store__path", children: path }), _jsx("span", { className: "database-diagram__side-panel__included-store__open", children: _jsx(ExternalLinkIcon, {}) })] }, path));
})] })), database.includedStoreSpecifications.length > 0 && (_jsxs("div", { className: "database-diagram__side-panel__section", children: [_jsxs("div", { className: "database-diagram__side-panel__section__header", children: ["Lakehouse Stores", ' ', _jsxs("span", { className: "database-diagram__side-panel__section__count", children: ["(", database.includedStoreSpecifications.length, ")"] })] }), database.includedStoreSpecifications.map((spec) => (_jsx(LakehouseStoreRow, { editorState: editorState, spec: spec }, `${spec.storeType}:${spec.packageableElementPointer.value.path}`)))] }))] }));
});
//# sourceMappingURL=DatabaseSchemaTree.js.map