@finos/legend-application-studio
Version:
Legend Studio application core
404 lines • 23.9 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 { ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, CustomSelectorInput, createFilter, PURE_ConnectionIcon, BlankPanelPlaceholder, PanelDropZone, ResizablePanelSplitterLine, PlayIcon, PanelLoadingIndicator, BlankPanelContent, PURE_DatabaseIcon, SyncIcon, clsx, CheckSquareIcon, SquareIcon, } from '@finos/legend-art';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useApplicationStore, useCommands, useConditionedApplicationNavigationContext, } from '@finos/legend-application';
import { flowResult } from 'mobx';
import { CODE_EDITOR_LANGUAGE, CODE_EDITOR_THEME, getBaseCodeEditorOptions, } from '@finos/legend-code-editor';
import { editor as monacoEditorAPI, languages as monacoLanguagesAPI, } from 'monaco-editor';
import { PackageableConnection, RelationalDatabaseConnection, } from '@finos/legend-graph';
import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../__lib__/LegendStudioApplicationNavigationContext.js';
import {} from '../../../stores/editor/panel-group/SQLPlaygroundPanelState.js';
import { useEditorStore } from '../EditorStoreProvider.js';
import { PANEL_MODE } from '../../../stores/editor/EditorConfig.js';
import { useDrag, useDrop } from 'react-dnd';
import { CORE_DND_TYPE, } from '../../../stores/editor/utils/DnDUtils.js';
import { DataGrid, } from '@finos/legend-lego/data-grid';
import { at, isNonNullable, isNumber, isString, isValidURL, parseCSVString, prettyDuration, uniqBy, } from '@finos/legend-shared';
import { DatabaseSchemaExplorer, DatabaseSchemaExplorerTreeNodeContainer, } from '../editor-group/connection-editor/DatabaseSchemaExplorer.js';
import { DatabaseSchemaExplorerTreeTableNodeData } from '../../../stores/editor/editor-state/element-editor-state/connection/DatabaseBuilderState.js';
import { buildRelationalDatabaseConnectionOption, } from '../editor-group/connection-editor/RelationalDatabaseConnectionEditor.js';
const DATABASE_NODE_DND_TYPE = 'DATABASE_NODE_DND_TYPE';
const SQLPlaygroundDatabaseSchemaExplorerTreeNodeContainer = observer((props) => {
const { node } = props;
const ref = useRef(null);
const [, dragConnector] = useDrag(() => ({
type: DATABASE_NODE_DND_TYPE,
item: {
text: node instanceof DatabaseSchemaExplorerTreeTableNodeData
? `${node.owner.name}.${node.label}`
: node.label,
},
}), [node]);
dragConnector(ref);
return _jsx(DatabaseSchemaExplorerTreeNodeContainer, { ...props, ref: ref });
});
// List of most popular SQL keywords
// See https://www.w3schools.com/sql/sql_ref_keywords.asp
const SQL_KEYWORDS = [
'AND',
'AS',
'ASC',
'BETWEEN',
'DESC',
'DISTINCT',
'EXEC',
'EXISTS',
'FROM',
'FULL OUTER JOIN',
'GROUP BY',
'HAVING',
'IN',
'INNER JOIN',
'IS NULL',
'IS NOT NULL',
'JOIN',
'LEFT JOIN',
'LIKE',
'LIMIT',
'NOT',
'NOT NULL',
'OR',
'ORDER BY',
'OUTER JOIN',
'RIGHT JOIN',
'SELECT',
'SELECT DISTINCT',
'SELECT INTO',
'SELECT TOP',
'TOP',
'UNION',
'UNION ALL',
'UNIQUE',
'WHERE',
];
const getKeywordSuggestions = async (position, model) => SQL_KEYWORDS.map((keyword) => ({
label: keyword,
kind: monacoLanguagesAPI.CompletionItemKind.Keyword,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText: `${keyword} `,
}));
const getDatabaseSchemaEntities = async (position, model, playgroundState) => {
if (playgroundState.schemaExplorerState?.treeData) {
return uniqBy(Array.from(playgroundState.schemaExplorerState.treeData.nodes.values()).map((value) => ({
label: value.label,
kind: monacoLanguagesAPI.CompletionItemKind.Field,
insertTextRules: monacoLanguagesAPI.CompletionItemInsertTextRule.InsertAsSnippet,
insertText: `${value.label} `,
})), (val) => val.label);
}
return [];
};
const PlaygroundSQLCodeEditor = observer(() => {
const editorStore = useEditorStore();
const playgroundState = editorStore.sqlPlaygroundState;
const applicationStore = useApplicationStore();
const codeEditorRef = useRef(null);
const [editor, setEditor] = useState();
const sqlIdentifierSuggestionProviderDisposer = useRef(undefined);
useEffect(() => {
if (!editor && codeEditorRef.current) {
const element = codeEditorRef.current;
const newEditor = monacoEditorAPI.create(element, {
...getBaseCodeEditorOptions(),
theme: CODE_EDITOR_THEME.DEFAULT_DARK,
language: CODE_EDITOR_LANGUAGE.SQL,
padding: {
top: 10,
},
});
newEditor.onDidChangeModelContent(() => {
const currentVal = newEditor.getValue();
playgroundState.setSQLText(currentVal);
});
// Restore the editor model and view state
newEditor.setModel(playgroundState.sqlEditorTextModel);
if (playgroundState.sqlEditorViewState) {
newEditor.restoreViewState(playgroundState.sqlEditorViewState);
}
newEditor.focus(); // focus on the editor initially
playgroundState.setSQLEditor(newEditor);
setEditor(newEditor);
}
}, [playgroundState, applicationStore, editor]);
useCommands(playgroundState);
if (editor) {
sqlIdentifierSuggestionProviderDisposer.current?.dispose();
sqlIdentifierSuggestionProviderDisposer.current =
monacoLanguagesAPI.registerCompletionItemProvider(CODE_EDITOR_LANGUAGE.SQL, {
triggerCharacters: [],
provideCompletionItems: async (model, position, context) => {
let suggestions = [];
if (context.triggerKind ===
monacoLanguagesAPI.CompletionTriggerKind.Invoke) {
// keywords
suggestions = suggestions.concat(await getKeywordSuggestions(position, model));
// database schema entities
suggestions = suggestions.concat(await getDatabaseSchemaEntities(position, model, playgroundState));
}
return { suggestions };
},
});
}
// clean up
useEffect(() => () => {
if (editor) {
// persist editor view state (cursor, scroll, etc.) to restore on re-open
playgroundState.setSQLEditorViewState(editor.saveViewState() ?? undefined);
editor.dispose();
// Dispose the providers properly to avoid ending up with duplicated suggestions
sqlIdentifierSuggestionProviderDisposer.current?.dispose();
}
}, [playgroundState, editor]);
const handleDatabaseNodeDrop = useCallback((item) => {
if (isString(item.text)) {
if (playgroundState.sqlEditor) {
const currentValue = playgroundState.sqlEditorTextModel.getValue();
const lines = currentValue.split('\n');
const position = playgroundState.sqlEditor.getPosition() ?? {
lineNumber: lines.length,
column: lines.at(-1)?.length ?? 0,
};
playgroundState.sqlEditor.executeEdits('', [
{
range: {
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column,
},
text: item.text,
forceMoveMarkers: true,
},
]);
playgroundState.setSQLText(playgroundState.sqlEditorTextModel.getValue());
}
}
}, [playgroundState]);
const [{ isDatabaseNodeDragOver }, dropConnector] = useDrop(() => ({
accept: DATABASE_NODE_DND_TYPE,
drop: (item) => handleDatabaseNodeDrop(item),
collect: (monitor) => ({
isDatabaseNodeDragOver: monitor.isOver({ shallow: true }),
}),
}), [handleDatabaseNodeDrop]);
return (_jsx("div", { className: "sql-playground__code-editor", children: _jsx(PanelDropZone, { className: "sql-playground__code-editor__content", isDragOver: isDatabaseNodeDragOver, dropTargetConnector: dropConnector, children: _jsx("div", { className: "code-editor__container", children: _jsx("div", { className: "code-editor__body", ref: codeEditorRef }) }) }) }));
});
const parseExecutionResultData = (data) => {
const lines = data.split('\n').filter((line) => line.trim().length);
if (lines.length) {
const columns = parseCSVString(at(lines, 0)) ?? [];
const rowData = lines
.slice(1)
.map((item) => {
const rowItems = parseCSVString(item);
if (!rowItems) {
return undefined;
}
const row = {};
columns.forEach((column, idx) => {
row[column] = rowItems[idx] ?? '';
});
return row;
})
.filter(isNonNullable);
return { rowData, columns };
}
return undefined;
};
const TDSResultCellRenderer = observer((params) => {
const cellValue = params.value;
const formattedCellValue = () => {
if (isNumber(cellValue)) {
return Intl.NumberFormat('en-US', {
maximumFractionDigits: 4,
}).format(Number(cellValue));
}
return cellValue;
};
const cellValueUrlLink = isString(cellValue) && isValidURL(cellValue) ? cellValue : undefined;
return (_jsx("div", { className: clsx('query-builder__result__values__table__cell'), children: cellValueUrlLink ? (_jsx("a", { href: cellValueUrlLink, target: "_blank", rel: "noreferrer", children: cellValueUrlLink })) : (_jsx("span", { children: formattedCellValue() })) }));
});
const PlayGroundSQLExecutionResultGrid = observer((props) => {
const { result, useAdvancedGrid, useLocalMode } = props;
const data = parseExecutionResultData(result);
const applicationStore = useApplicationStore();
const darkMode = !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
if (!data) {
return (_jsx(BlankPanelContent, { children: `Can't parse result, displaying raw form:\n${result}` }));
}
if (useAdvancedGrid) {
if (useLocalMode) {
const localcolDefs = data.columns.map((colName) => ({
minWidth: 50,
sortable: true,
resizable: true,
field: colName,
flex: 1,
enablePivot: true,
enableRowGroup: true,
enableValue: true,
allowedAggFuncs: ['count'],
}));
return (_jsx("div", { className: clsx('sql-playground__result__grid', {
'ag-theme-balham': !darkMode,
'ag-theme-balham-dark': darkMode,
}), children: _jsx(DataGrid, { rowData: data.rowData, gridOptions: {
suppressScrollOnNewData: true,
rowSelection: {
mode: 'multiRow',
checkboxes: false,
headerCheckbox: false,
},
pivotPanelShow: 'always',
rowGroupPanelShow: 'always',
cellSelection: true,
},
// NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
// See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
onRowDataUpdated: (params) => {
params.api.refreshCells({ force: true });
}, suppressFieldDotNotation: true, suppressContextMenu: false, columnDefs: localcolDefs, sideBar: ['columns', 'filters'] }) }));
}
const colDefs = data.columns.map((colName) => ({
minWidth: 50,
sortable: true,
resizable: true,
field: colName,
flex: 1,
cellRenderer: TDSResultCellRenderer,
filter: true,
}));
const getContextMenuItems = useCallback((params) => [
'copy',
'copyWithHeaders',
{
name: 'Copy Row Value',
action: () => {
params.api.copySelectedRowsToClipboard();
},
},
], []);
return (_jsx("div", { className: clsx('sql-playground__result__grid', {
'ag-theme-balham': !darkMode,
'ag-theme-balham-dark': darkMode,
}), children: _jsx(DataGrid, { rowData: data.rowData, overlayNoRowsTemplate: `<div class="sql-playground__result__grid--empty">No results</div>`, gridOptions: {
suppressScrollOnNewData: true,
rowSelection: {
mode: 'multiRow',
checkboxes: false,
headerCheckbox: false,
},
cellSelection: true,
}, onRowDataUpdated: (params) => {
params.api.refreshCells({ force: true });
}, suppressFieldDotNotation: true, suppressClipboardPaste: false, suppressContextMenu: false, columnDefs: colDefs, getContextMenuItems: (params) => getContextMenuItems(params) }) }));
}
return (_jsx("div", { className: clsx('sql-playground__result__grid', {
'ag-theme-balham': !darkMode,
'ag-theme-balham-dark': darkMode,
}), children: _jsx(DataGrid, { rowData: data.rowData, overlayNoRowsTemplate: `<div class="sql-playground__result__grid--empty">No results</div>`, alwaysShowVerticalScroll: true, suppressFieldDotNotation: true, columnDefs: data.columns.map((column) => ({
minWidth: 50,
sortable: true,
resizable: true,
headerName: column,
field: column,
flex: 1,
})) }) }));
});
export const SQLPlaygroundPanel = observer(() => {
const editorStore = useEditorStore();
const playgroundState = editorStore.sqlPlaygroundState;
const applicationStore = useApplicationStore();
// connection
const connectionSelectorRef = useRef(null);
const connectionFilterOption = createFilter({
ignoreCase: true,
ignoreAccents: false,
stringify: (option) => option.data.value.path,
});
const connectionOptions = editorStore.graphManagerState.usableConnections
.filter((connection) => connection.connectionValue instanceof RelationalDatabaseConnection)
.map(buildRelationalDatabaseConnectionOption);
const selectedConnectionOption = playgroundState.connection
? buildRelationalDatabaseConnectionOption(playgroundState.connection)
: null;
const changeConnection = (val) => {
if (val.value === playgroundState.connection) {
return;
}
playgroundState.setConnection(val.value);
};
const onPickConnection = () => {
editorStore.setQuickInputState({
title: 'Connection picker',
placeholder: 'Select a connection...',
options: connectionOptions,
getSearchValue: (option) => option.value.path,
onSelect: changeConnection,
});
};
const handleConnectionDrop = useCallback((item) => {
if (item.data.packageableElement instanceof PackageableConnection) {
if (item.data.packageableElement.connectionValue instanceof
RelationalDatabaseConnection) {
playgroundState.setConnection(item.data.packageableElement);
}
else {
applicationStore.notificationService.notifyWarning(`Can't use SQL playground with non-relational database connection`);
}
}
}, [playgroundState, applicationStore]);
const [{ isConnectionDragOver }, dropConnector] = useDrop(() => ({
accept: CORE_DND_TYPE.PROJECT_EXPLORER_CONNECTION,
drop: (item) => handleConnectionDrop(item),
collect: (monitor) => ({
isConnectionDragOver: monitor.isOver({ shallow: true }),
}),
}), [handleConnectionDrop]);
const updateDatabase = () => {
if (playgroundState.schemaExplorerState) {
flowResult(playgroundState.schemaExplorerState.updateDatabase()).catch(applicationStore.alertUnhandledError);
}
};
const executeRawSQL = () => {
flowResult(playgroundState.executeRawSQL()).catch(applicationStore.alertUnhandledError);
};
const advancedMode = Boolean(editorStore.applicationStore.config.options.queryBuilderConfig
?.TEMPORARY__enableGridEnterpriseMode);
const resultDescription = playgroundState.sqlExecutionResult
? `query ran in ${prettyDuration(playgroundState.sqlExecutionResult.sqlDuration, {
ms: true,
})}`
: undefined;
const toggleocalMode = () => {
playgroundState.toggleIsLocalModeEnabled();
};
useEffect(() => {
if (playgroundState.schemaExplorerState) {
flowResult(playgroundState.schemaExplorerState.fetchDatabaseMetadata()).catch(applicationStore.alertUnhandledError);
}
}, [playgroundState, applicationStore, playgroundState.schemaExplorerState]);
useConditionedApplicationNavigationContext(LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.SQL_PLAYGROUND, editorStore.activePanelMode === PANEL_MODE.SQL_PLAYGROUND);
return (_jsx(PanelDropZone, { isDragOver: isConnectionDragOver, dropTargetConnector: dropConnector, children: _jsxs("div", { className: "sql-playground", children: [playgroundState.connection && (_jsxs(ResizablePanelGroup, { orientation: "vertical", children: [_jsx(ResizablePanel, { size: 300, children: _jsxs("div", { className: "sql-playground__config", children: [_jsxs("div", { className: "sql-playground__config__setup", children: [_jsxs("div", { className: "sql-playground__config__connection-selector", children: [_jsx("div", { className: "sql-playground__config__connection-selector__icon", children: _jsx(PURE_ConnectionIcon, {}) }), _jsx(CustomSelectorInput, { inputRef: connectionSelectorRef, className: "sql-playground__config__connection-selector__input", options: connectionOptions, onChange: changeConnection, value: selectedConnectionOption, darkMode: !applicationStore.layoutService
.TEMPORARY__isLightColorThemeEnabled, placeholder: "Choose a connection...", filterOption: connectionFilterOption })] }), _jsxs("div", { className: "sql-playground__config__database-selector", children: [_jsx("div", { className: "sql-playground__config__database-selector__icon", children: _jsx(PURE_DatabaseIcon, {}) }), _jsx(CustomSelectorInput, { inputRef: connectionSelectorRef, className: "sql-playground__config__database-selector__input", options: connectionOptions, onChange: changeConnection, value: selectedConnectionOption, darkMode: !applicationStore.layoutService
.TEMPORARY__isLightColorThemeEnabled, placeholder: "Choose a connection...", filterOption: connectionFilterOption }), _jsx("button", { className: "sql-playground__config__database-selector__update-btn btn--sm btn--dark", disabled: !playgroundState.database, onClick: updateDatabase, title: "Update database", children: _jsx(SyncIcon, {}) })] })] }), _jsxs("div", { className: "sql-playground__config__schema-explorer", children: [_jsx(PanelLoadingIndicator, { isLoading: Boolean(playgroundState.schemaExplorerState?.isGeneratingDatabase) }), playgroundState.schemaExplorerState?.treeData && (_jsx(DatabaseSchemaExplorer, { treeData: playgroundState.schemaExplorerState.treeData, schemaExplorerState: playgroundState.schemaExplorerState, treeNodeContainerComponent: SQLPlaygroundDatabaseSchemaExplorerTreeNodeContainer }))] })] }) }), _jsx(ResizablePanelSplitter, {}), _jsx(ResizablePanel, { children: _jsx("div", { className: "panel sql-playground__sql-editor", children: _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsx(ResizablePanel, { children: _jsx(PlaygroundSQLCodeEditor, {}) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-250)" }) }), _jsxs(ResizablePanel, { size: 300, children: [_jsxs("div", { className: "panel__header", children: [_jsxs("div", { className: "panel__header__title", children: [_jsx("div", { className: "panel__header__title__label", children: "result" }), playgroundState.executeRawSQLState.isInProgress && (_jsx("div", { className: "panel__header__title__label__status", children: "Running SQL..." })), _jsx("div", { className: "query-builder__result__analytics", children: resultDescription ?? '' })] }), _jsxs("div", { className: "panel__header__actions query-builder__result__header__actions", children: [advancedMode && (_jsxs("div", { className: "query-builder__result__advanced__mode", children: [_jsx("div", { className: "query-builder__result__advanced__mode__label", children: "Local Mode" }), _jsx("button", { className: clsx('query-builder__result__advanced__mode__toggler__btn', {
'query-builder__result__advanced__mode__toggler__btn--toggled': playgroundState.isLocalModeEnabled,
}), onClick: toggleocalMode, tabIndex: -1, children: playgroundState.isLocalModeEnabled ? (_jsx(CheckSquareIcon, {})) : (_jsx(SquareIcon, {})) })] })), _jsx("div", { className: "query-builder__result__execute-btn btn__dropdown-combo btn__dropdown-combo--primary", children: _jsxs("button", { className: "btn__dropdown-combo__label", onClick: executeRawSQL, disabled: playgroundState.executeRawSQLState.isInProgress, tabIndex: -1, children: [_jsx(PlayIcon, { className: "btn__dropdown-combo__label__icon" }), _jsx("div", { className: "btn__dropdown-combo__label__title", children: "Run Query" })] }) })] })] }), playgroundState.sqlExecutionResult !== undefined && (_jsx(PlayGroundSQLExecutionResultGrid, { result: playgroundState.sqlExecutionResult.value, useAdvancedGrid: advancedMode, useLocalMode: playgroundState.isLocalModeEnabled })), playgroundState.sqlExecutionResult === undefined && (_jsx("div", {}))] })] }) }) })] })), !playgroundState.connection && (_jsx(BlankPanelPlaceholder, { onClick: onPickConnection, clickActionType: "add", text: "Pick a connection to start", tooltipText: "Drop a connection to start...", isDropZoneActive: isConnectionDragOver }))] }) }));
});
//# sourceMappingURL=SQLPlaygroundPanel.js.map