diff --git a/webapp/packages/core-blocks/src/Split/index.ts b/webapp/packages/core-blocks/src/Split/index.ts index 1de3dfb393..a8a76878c6 100644 --- a/webapp/packages/core-blocks/src/Split/index.ts +++ b/webapp/packages/core-blocks/src/Split/index.ts @@ -3,3 +3,4 @@ export * from './SplitControls'; export * from './Pane'; export * from './ResizerControls'; export * from './Split'; +export * from './useSplit'; diff --git a/webapp/packages/core-blocks/src/Split/useSplit.tsx b/webapp/packages/core-blocks/src/Split/useSplit.tsx new file mode 100644 index 0000000000..cdf9f038dd --- /dev/null +++ b/webapp/packages/core-blocks/src/Split/useSplit.tsx @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { ISplitState, SplitContext } from 'go-split'; +import { useContext } from 'react'; + +export function useSplit(): ISplitState { + return useContext(SplitContext); +} diff --git a/webapp/packages/core-cli/configs/webpack.config.js b/webapp/packages/core-cli/configs/webpack.config.js index e26b29b582..945069da3b 100644 --- a/webapp/packages/core-cli/configs/webpack.config.js +++ b/webapp/packages/core-cli/configs/webpack.config.js @@ -1,24 +1,24 @@ -const { resolve, join } = require('path'); -const ModuleDependencyWarning = require("webpack/lib/ModuleDependencyWarning"); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const { resolve, join } = require('path') +const ModuleDependencyWarning = require('webpack/lib/ModuleDependencyWarning') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') // const ESLintPlugin = require('eslint-webpack-plugin'); class IgnoreNotFoundExportPlugin { - apply(compiler) { - const messageRegExp = /export '.*'( \(imported as '.*'\))? was not found in/ - function doneHook(stats) { - stats.compilation.warnings = stats.compilation.warnings.filter(function (warn) { + apply (compiler) { + const messageRegExp = /export '.*'( \(imported as '.*'\))? was not found in/; + function doneHook (stats) { + stats.compilation.warnings = stats.compilation.warnings.filter(warn => { if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) { - return false + return false; } - return true; - }) + return true + }); } if (compiler.hooks) { - compiler.hooks.done.tap("IgnoreNotFoundExportPlugin", doneHook) + compiler.hooks.done.tap('IgnoreNotFoundExportPlugin', doneHook); } else { - compiler.plugin("done", doneHook) + compiler.plugin('done', doneHook); } } } @@ -26,38 +26,38 @@ class IgnoreNotFoundExportPlugin { const nodeModules = [ resolve('node_modules'), // product resolve('../../node_modules'), // workspace - resolve('../../node_modules/@cloudbeaver/core-cli/node_modules') // core-cli -] + resolve('../../node_modules/@cloudbeaver/core-cli/node_modules'), // core-cli +]; module.exports = (env, argv) => { - process.env.NODE_ENV = argv.mode; - const devTool = 'source-map' in env && 'source-map'; - const devMode = argv.mode !== 'production'; - function getBaseStyleLoaders() { - const loaders = []; + process.env.NODE_ENV = argv.mode + const devTool = 'source-map' in env && 'source-map' + const devMode = argv.mode !== 'production' + function getBaseStyleLoaders () { + const loaders = [] // Broke styles order in dev mode - // if(devMode) { - // loaders.push('style-loader') - // }else{ - loaders.push(MiniCssExtractPlugin.loader); + // if (devMode) { + // loaders.push('style-loader'); + // } else { + loaders.push(MiniCssExtractPlugin.loader) // } - return loaders; + return loaders } - function generateStyleLoaders(options = { hasModule: false }) { - const moduleScope = options.hasModule ? 'local' : 'global'; + function generateStyleLoaders (options = { hasModule: false }) { + const moduleScope = options.hasModule ? 'local' : 'global' const modules = { mode: moduleScope, localIdentName: '[local]___[hash:base64:5]', - }; + } const postCssPlugins = [ require('postcss-preset-env')({ stage: 0 }), // require('postcss-discard-comments'), - require('reshadow/postcss')({ scopeBehaviour: moduleScope }) - ]; + require('reshadow/postcss')({ scopeBehaviour: moduleScope }), + ] return [ ...getBaseStyleLoaders(), @@ -66,15 +66,16 @@ module.exports = (env, argv) => { options: { modules: modules, sourceMap: true, - } + }, }, + { loader: 'postcss-loader', options: { postcssOptions: { plugins: postCssPlugins, sourceMap: true, - } + }, }, }, { @@ -83,11 +84,11 @@ module.exports = (env, argv) => { sourceMap: true, sassOptions: { implementation: require('node-sass'), - includePaths: nodeModules + includePaths: nodeModules, }, - } - } - ]; + }, + }, + ] } var babelLoader = { @@ -95,7 +96,7 @@ module.exports = (env, argv) => { options: { configFile: join(__dirname, 'babel.config.js'), }, - } + }; return { // target: !devMode ? "web" : "browserslist", @@ -111,7 +112,7 @@ module.exports = (env, argv) => { }, }, resolveLoader: { - modules: nodeModules + modules: nodeModules, }, module: { rules: [ @@ -124,8 +125,8 @@ module.exports = (env, argv) => { test: /\.(ts|js)x?$/, exclude: /node_modules/, use: [ - babelLoader - ] + babelLoader, + ], }, { test: /\.(css|s[ac]ss)$/, @@ -139,15 +140,23 @@ module.exports = (env, argv) => { use: [ ...getBaseStyleLoaders(), 'css-loader', - 'sass-loader' - ] + 'sass-loader', + ], }, { use: generateStyleLoaders({ hasModule: false }), - } - ] + }, + ], }, - ] + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + }, + ], + }, + ], }, devtool: devTool, plugins: [ @@ -168,6 +177,6 @@ module.exports = (env, argv) => { chunkFilename: devMode ? '[id].css' : '[id].[contenthash].css', ignoreOrder: false, // Enable to remove warnings about conflicting order }), - ] - }; -}; + ], + } +} diff --git a/webapp/packages/core-cli/package.json b/webapp/packages/core-cli/package.json index e6d4880418..b5f97716f8 100644 --- a/webapp/packages/core-cli/package.json +++ b/webapp/packages/core-cli/package.json @@ -44,10 +44,11 @@ "source-map-loader": "^2.0.0", "style-loader": "^2.0.0", "ts-loader": "^8.0.14", + "url-loader": "^4.1.1", "webpack": "^5.15.0", "webpack-bundle-analyzer": "^4.3.0", "webpack-cli": "^4.3.1", "webpack-dev-server": "^3.11.2", "webpack-merge": "^5.7.3" } -} \ No newline at end of file +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index 806b832dff..5778bceec0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -24,6 +24,7 @@ export const CellRenderer: React.FC> = observer(function const selectionContext = useContext(DataGridSelectionContext); const editingContext = useContext(EditingContext); const editor = dataGridContext?.model.source.getEditor(dataGridContext.resultIndex); + const dataGridApi = dataGridContext?.getDataGridApi(); const classes: string[] = []; const { rowIdx, column } = props; @@ -41,6 +42,10 @@ export const CellRenderer: React.FC> = observer(function } const handleMouseDown = useCallback((event: React.MouseEvent) => { + if (dataGridApi) { + dataGridApi.selectCell({ idx: column.idx, rowIdx }); + } + selectionContext?.select( { colIdx: column.idx, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index 1df1047c3a..53017b49a8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -7,6 +7,7 @@ */ import { createContext } from 'react'; +import type { DataGridHandle } from 'react-data-grid'; import type { IExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; @@ -21,6 +22,7 @@ export interface IDataGridContext { resultIndex: number; columnResize: IExecutor; getEditorPortal: () => HTMLDivElement | null; + getDataGridApi: () => DataGridHandle | null; } export const DataGridContext = createContext(null); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts new file mode 100644 index 0000000000..307b3524f8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { IDatabaseDataAction } from '../IDatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IResultSetElementKey } from './ResultSet/IResultSetElementKey'; + +export interface IDatabaseResultDataAction + extends IDatabaseDataAction { + getCellValue: (cell: IResultSetElementKey) => any; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts new file mode 100644 index 0000000000..e0fb9f986a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -0,0 +1,29 @@ +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { databaseDataAction } from '../DatabaseDataActionDecorator'; +import type { IDatabaseResultDataAction } from '../IDatabaseDataAction'; +import type { IResultSetElementKey } from './IResultSetElementKey'; + +@databaseDataAction() +export class ResultSetDataAction implements IDatabaseResultDataAction { + static dataFormat = ResultDataFormat.Resultset; + result: IDatabaseResultSet; + + constructor( + result: IDatabaseResultSet) { + this.result = result; + } + + getCellValue(cell: IResultSetElementKey): any { + if (cell.row === undefined || cell.column === undefined || !this.result.data?.rows) { + return undefined; + } + + return this.result.data.rows[cell.row][cell.column]; + } + + updateResult(result: IDatabaseResultSet) { + this.result = result; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index 2ad7df332d..c2617d677b 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -7,6 +7,7 @@ */ import { observer } from 'mobx-react-lite'; +import { useRef, useState } from 'react'; import styled, { css } from 'reshadow'; import { TabList, TabPanelList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-blocks'; @@ -69,9 +70,29 @@ export const ValuePanel: DataPresentationComponent = ob resultIndex, }) { const service = useService(DataValuePanelService); + const [currentTabId, setCurrentTabId] = useState(''); + const lastTabId = useRef(''); + + const displayed = service.getDisplayed({ dataFormat, model, resultIndex }); + + if (displayed.length > 0) { + const firstTabId = displayed[0].key; + if (firstTabId !== lastTabId.current) { + setCurrentTabId(firstTabId); + lastTabId.current = firstTabId; + } + } return styled(useStyles(styles, UNDERLINE_TAB_STYLES))( - + setCurrentTabId(tab.tabId)} + > diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts index 376b349a8c..f1e60a69dd 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts @@ -23,9 +23,10 @@ export class TextValuePresentationBootstrap extends Bootstrap { key: 'text-presentation', options: { dataFormat: [ResultDataFormat.Resultset] }, name: 'data_viewer_presentation_value_text_title', + order: Number.MAX_SAFE_INTEGER, panel: () => TextValuePresentation, }); } - load(): void {} + load(): void { } } diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index f2fa42de8c..c63d044d45 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -5,6 +5,7 @@ export default manifest; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; @@ -23,6 +24,7 @@ export * from './DatabaseDataModel/IDatabaseResultSet'; // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService'; +export * from './TableViewer/ValuePanel/DataValuePanelService'; export * from './TableViewer/TableViewer'; export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; diff --git a/webapp/packages/plugin-gis-viewer/.gitignore b/webapp/packages/plugin-gis-viewer/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json new file mode 100644 index 0000000000..7cac418ca7 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cloudbeaver/plugin-gis-viewer", + "sideEffects": false, + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "src/index.ts", + "scripts": { + "prebuild": "rimraf lib/*", + "build": "webpack --config ../../configs/webpack.plugin.config.js", + "postbuild": "tsc", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" + }, + "peerDependencies": { + "@cloudbeaver/core-app": "^0.1.0", + "@cloudbeaver/core-blocks": "^0.1.0", + "@cloudbeaver/core-di": "^0.1.0", + "@cloudbeaver/core-dialogs": "^0.1.0", + "@cloudbeaver/core-events": "^0.1.0", + "@cloudbeaver/core-extensions": "^0.1.0", + "@cloudbeaver/core-localization": "^0.1.0", + "@cloudbeaver/core-plugin": "^0.1.0", + "@cloudbeaver/core-product": "^0.1.0", + "@cloudbeaver/core-root": "^0.1.0", + "@cloudbeaver/core-sdk": "^0.1.0", + "@cloudbeaver/core-settings": "^0.1.0", + "@cloudbeaver/core-theming": "^0.1.0", + "@cloudbeaver/core-utils": "^0.1.0", + "@cloudbeaver/core-view": "^0.1.0", + "@cloudbeaver/plugin-data-viewer": "^0.1.0", + "mobx": "^5.x.x", + "mobx-react-lite": "^3.x.x", + "react": "^16.x.x", + "reshadow": "^0.x.x", + "rxjs": "^6.x.x" + }, + "dependencies": { + "leaflet": "^1.7.1", + "react-leaflet": "^3.1.0", + "terraformer-wkt-parser": "^1.2.1" + }, + "devDependencies": { + "@types/react-leaflet": "^2.8.1" + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx new file mode 100644 index 0000000000..3b97037d35 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useCallback, useMemo } from 'react'; +import wkt from 'terraformer-wkt-parser'; + +import { TabContainerPanelComponent, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { IDataValuePanelProps, IDatabaseResultSet, ResultSetSelectAction, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; + +import { IGeoJSONFeature, IAssociatedValue, LeafletMap } from './LeafletMap'; +import { ResultSetGISAction } from './ResultSetGISAction'; + +export const GISValuePresentation: TabContainerPanelComponent> = observer(function GISValuePresentation({ + model, + resultIndex, +}) { + const translate = useTranslate(); + + const modelResultData = model.getResult(resultIndex); + const selection = model.source.getAction(resultIndex, ResultSetSelectAction); + const gis = model.source.getAction(resultIndex, ResultSetGISAction); + + const focusedCell = selection.getFocusedElement() as Required | null; + const selectedCells = selection.getSelectedElements(); + + if (selectedCells.length === 0 && focusedCell) { + selectedCells.push(focusedCell); + } + + const parsedGISData = useMemo(() => { + const result: IGeoJSONFeature[] = []; + + for (const cell of selectedCells) { + const cellValue = gis.getCellValue(cell); + + if (!cellValue) { + continue; + } + + const parsedCellValue = wkt.parse(cellValue.mapText || cellValue.text); + result.push({ type: 'Feature', geometry: parsedCellValue, properties: { associatedCell: cell, srid: cellValue.srid } }); + } + + return result; + }, [selectedCells, gis]); + + const getAssociatedValues = useCallback((cell: Required): IAssociatedValue[] => { + if (!modelResultData?.data?.columns || !modelResultData?.data?.rows) { + return []; + } + + const { column: columnIndex, row: rowIndex } = cell; + + return modelResultData.data.columns.reduce((result: IAssociatedValue[], column, i) => { + if (i !== columnIndex) { + result.push({ + key: column.name!, + value: model.source + .getEditor(resultIndex) + .getCell(rowIndex, i), + }); + } + + return result; + }, []); + }, [modelResultData, model, resultIndex]); + + if (!parsedGISData.length) { + return {translate('gis_presentation_placeholder')}; + } + + return ( + + ); +}); diff --git a/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts b/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts new file mode 100644 index 0000000000..64be251a3e --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts @@ -0,0 +1,57 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { DataValuePanelService, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; + +import { GISValuePresentation } from './GISValuePresentation'; +import { ResultSetGISAction } from './ResultSetGISAction'; + +@injectable() +export class GISViewerBootstrap extends Bootstrap { + constructor(private readonly dataValuePanelService: DataValuePanelService) { + super(); + } + + register(): void | Promise { + this.dataValuePanelService.add({ + key: 'gis-presentation', + options: { dataFormat: [ResultDataFormat.Resultset] }, + name: 'gis_presentation_title', + order: 10, + panel: () => GISValuePresentation, + isHidden: (_, context) => { + if (!context || !context.model.source.hasResult(context.resultIndex)) { + return true; + } + + const selection = context.model.source.getAction(context.resultIndex, ResultSetSelectAction); + const gis = context.model.source.getAction(context.resultIndex, ResultSetGISAction); + + const focusedElement = selection.getFocusedElement(); + + if (focusedElement) { + return !gis.isGISFormat(focusedElement); + } + + const selectedCells = selection.getSelectedElements(); + + if (selectedCells.length > 0) { + const firstSelectedCell = selectedCells[0]; + + return !gis.isGISFormat(firstSelectedCell); + } + + return true; + }, + }); + } + + load(): void { } +} diff --git a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts new file mode 100644 index 0000000000..f3a471968a --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import type { IDatabaseDataResult, IDatabaseDataAction, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; + +import type { IGISType } from './ResultSetGISAction'; + +export interface IDatabaseDataGISAction + extends IDatabaseDataAction { + getGISDataFor: (selectedCells: Array>) => Array>; + getCellValue: (cell: IResultSetElementKey) => IGISType | undefined; + isGISFormat: (cell: IResultSetElementKey) => boolean; +} diff --git a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx new file mode 100644 index 0000000000..04ee532e77 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx @@ -0,0 +1,215 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import 'leaflet/dist/leaflet.css'; + +import type geojson from 'geojson'; +import leaflet from 'leaflet'; +import { useCallback, useEffect, useState } from 'react'; +import { MapContainer, GeoJSON, LayersControl, TileLayer } from 'react-leaflet'; +import type { TileLayerProps } from 'react-leaflet'; +import styled, { css } from 'reshadow'; + +import { useSplit } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; + +export interface IAssociatedValue { + key: string; + value: any; +} + +interface IFeatureProperties { + srid: number; + associatedCell: Required; +} + +export interface IGeoJSONFeature extends GeoJSON.Feature { + type: 'Feature'; + bbox?: geojson.BBox; +} + +interface IBaseTile extends TileLayerProps { + name: string; + checked?: boolean; +} + +interface Props { + geoJSON: IGeoJSONFeature[]; + getAssociatedValues: (cell: Required) => IAssociatedValue[]; +} + +const baseTiles: Record<'street' | 'topography', IBaseTile> = { + street: { + name: 'gis_presentation_base_tile_street_name', + checked: true, + attribution: '© OpenStreetMap contributors', + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + maxZoom: 18, + id: 'osm.streets', + }, + topography: { + name: 'gis_presentation_base_tile_topography_name', + attribution: '© OpenStreetMap,' + + ' © SRTM,' + + ' © OpenTopoMap', + url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', + maxZoom: 17, + }, +}; + +const geojsonMarkerOptions: leaflet.CircleMarkerOptions = { + radius: 4, + weight: 3, +}; + +const popupOption: leaflet.PopupOptions = { + closeButton: true, + minWidth: 260, + maxWidth: 800, + maxHeight: 500, +}; + +function polyStyle() { + const objColor = 'blue'; + + return { + weight: 2, + color: objColor, + }; +} + +function getCRS(feature?: IGeoJSONFeature): leaflet.CRS { + switch (feature?.properties?.srid) { + case 0: + return leaflet.CRS.Simple; + case 3857: + return leaflet.CRS.EPSG3857; + case 4326: + return leaflet.CRS.EPSG4326; + case 3395: + return leaflet.CRS.EPSG3395; + case 900913: + return leaflet.CRS.EPSG900913; + default: + return leaflet.CRS.EPSG3857; + } +} + +const styles = css` + MapContainer { + width: 100%; + height: 100%; + } +`; + +export const LeafletMap: React.FC = function LeafletMap({ geoJSON, getAssociatedValues }) { + const splitContext = useSplit(); + const translate = useTranslate(); + + const [mapRef, setMapRef] = useState(null); + const [geoJSONLayerRef, setGeoJSONLayerRef] = useState(null); + + const crs = getCRS(geoJSON[0]); + + const onEachFeature = useCallback((feature: IGeoJSONFeature, layer: leaflet.Layer) => { + const associatedValues = getAssociatedValues(feature.properties.associatedCell); + if (associatedValues.length > 0) { + let popupContent = ''; + + popupContent += ''; + for (let i = 0; i < associatedValues.length; i++) { + const { key, value } = associatedValues[i]; + + if (value === null || value === undefined) { + continue; + } + + popupContent += ''; + } + popupContent += '
' + key + '' + value + '
'; + layer.bindPopup(popupContent, popupOption); + } + }, [getAssociatedValues]); + + useEffect(() => { + if (geoJSONLayerRef) { + geoJSONLayerRef.clearLayers(); + + for (let i = 0; i < geoJSON.length; i++) { + geoJSONLayerRef.addData(geoJSON[i]); + } + } + }, [geoJSON, geoJSONLayerRef]); + + useEffect(() => { + if (mapRef && geoJSONLayerRef) { + mapRef.invalidateSize(); + + const bounds = geoJSONLayerRef.getBounds(); + + if (Object.keys(bounds).length <= 0) { + return; + } + + if (crs === leaflet.CRS.Simple) { + const maxDimension = Math.max(bounds.getNorth() - bounds.getSouth(), bounds.getEast() - bounds.getWest()); + mapRef.setMinZoom(-5); + if (maxDimension > 0) { + mapRef.fitBounds(bounds); + } else { + mapRef.setView(bounds.getCenter(), mapRef.getZoom()); + } + } else { + mapRef.fitBounds(bounds); + } + } + }, [splitContext.isResizing, splitContext.mode, mapRef, geoJSONLayerRef, crs, geoJSON]); + + return styled(styles)( + + leaflet.circleMarker(latlng, geojsonMarkerOptions)} + onEachFeature={onEachFeature} + /> + + + + + + + + + + + ); +} +; diff --git a/webapp/packages/plugin-gis-viewer/src/LocaleService.ts b/webapp/packages/plugin-gis-viewer/src/LocaleService.ts new file mode 100644 index 0000000000..efc6d123bd --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/LocaleService.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise { } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts new file mode 100644 index 0000000000..45136cb524 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts @@ -0,0 +1,72 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { databaseDataAction, IResultSetElementKey, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; + +import type { IDatabaseDataGISAction } from './IDatabaseDataGISAction'; + +export interface IGISType { + $type: string; + srid: number; + text: string; + mapText: string | null; + properties: Record | null; +} +@databaseDataAction() +export class ResultSetGISAction implements IDatabaseDataGISAction { + private readonly GISValueType = 'geometry'; + + static dataFormat = ResultDataFormat.Resultset; + result: IDatabaseResultSet; + + constructor( + result: IDatabaseResultSet) { + this.result = result; + } + + isGISFormat(cell: IResultSetElementKey): boolean { + if (cell.row === undefined || cell.column === undefined) { + return false; + } + + const value = this.result?.data?.rows?.[cell.row][cell.column]; + + if (value !== null && typeof value === 'object' && '$type' in value) { + return value.$type === this.GISValueType; + } + + return false; + } + + getGISDataFor(cells: Array>): Array> { + const result: Array> = []; + if (!this.result.data?.rows) { + return result; + } + + for (const cell of cells) { + if (this.isGISFormat(cell)) { + result.push(cell); + } + } + return result; + } + + getCellValue(cell: IResultSetElementKey): IGISType | undefined { + if (cell.row === undefined || cell.column === undefined || !this.result.data?.rows || !this.isGISFormat(cell)) { + return undefined; + } + + return this.result.data.rows[cell.row][cell.column]; + } + + updateResult(result: IDatabaseResultSet) { + this.result = result; + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/index.ts b/webapp/packages/plugin-gis-viewer/src/index.ts new file mode 100644 index 0000000000..899d1e1836 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/index.ts @@ -0,0 +1,5 @@ +import { manifest } from './manifest'; + +export default manifest; + +export * from './ResultSetGISAction'; diff --git a/webapp/packages/plugin-gis-viewer/src/locales/en.ts b/webapp/packages/plugin-gis-viewer/src/locales/en.ts new file mode 100644 index 0000000000..ee6ba63543 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/locales/en.ts @@ -0,0 +1,6 @@ +export default [ + ['gis_presentation_title', 'Map'], + ['gis_presentation_placeholder', 'No data available'], + ['gis_presentation_base_tile_street_name', 'Street'], + ['gis_presentation_base_tile_topography_name', 'Topography'], +]; diff --git a/webapp/packages/plugin-gis-viewer/src/locales/ru.ts b/webapp/packages/plugin-gis-viewer/src/locales/ru.ts new file mode 100644 index 0000000000..0a57aee1c2 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/locales/ru.ts @@ -0,0 +1,6 @@ +export default [ + ['gis_presentation_title', 'Карта'], + ['gis_presentation_placeholder', 'Нет доступных данных'], + ['gis_presentation_base_tile_street_name', 'Схема'], + ['gis_presentation_base_tile_topography_name', 'Топографическая'], +]; diff --git a/webapp/packages/plugin-gis-viewer/src/manifest.ts b/webapp/packages/plugin-gis-viewer/src/manifest.ts new file mode 100644 index 0000000000..a0a8113b5b --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/manifest.ts @@ -0,0 +1,14 @@ +import type { PluginManifest } from '@cloudbeaver/core-di'; + +import { GISViewerBootstrap } from './GISViewerBootstrap'; +import { LocaleService } from './LocaleService'; + +export const manifest: PluginManifest = { + info: { + name: 'GIS Viewer plugin', + }, + providers: [ + GISViewerBootstrap, + LocaleService, + ], +}; diff --git a/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts b/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts new file mode 100644 index 0000000000..1c7349fb82 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type geojson from 'geojson'; +import * as ReactLeaflet from 'react-leaflet'; + +declare module 'react-leaflet' { + interface GeoJSONProps { + data?: geojson.GeoJsonObject; + } +} diff --git a/webapp/packages/plugin-gis-viewer/tsconfig.json b/webapp/packages/plugin-gis-viewer/tsconfig.json new file mode 100644 index 0000000000..50f9f59516 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "src" + ], + "exclude": [ + "**/node_modules", + "**/.*/" + ], +} \ No newline at end of file diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index a931237728..56577486d8 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -10,6 +10,7 @@ import dataExport from '@cloudbeaver/plugin-data-export'; import spreadsheetNew from '@cloudbeaver/plugin-data-spreadsheet-new'; import dataViewer from '@cloudbeaver/plugin-data-viewer'; import ddlViewer from '@cloudbeaver/plugin-ddl-viewer'; +import gisViewer from '@cloudbeaver/plugin-gis-viewer'; import objectViewer from '@cloudbeaver/plugin-object-viewer'; import productPlugin from '@cloudbeaver/plugin-product'; import sqlEditor from '@cloudbeaver/plugin-sql-editor'; @@ -25,6 +26,7 @@ const PLUGINS = [ connectionTemplate, dataExport, dataViewer, + gisViewer, ddlViewer, objectViewer, sqlEditor, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 38cd8301c4..5a400866d4 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2557,6 +2557,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f" integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw== +"@react-leaflet/core@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-1.0.2.tgz#39c6a73f61c666d5dcf673741cea2672fa4bbae1" + integrity sha512-QbleYZTMcgujAEyWGki8Lx6cXQqWkNtQlqf5c7NImlIp8bKW66bFpez/6EVatW7+p9WKBOEOVci/9W7WW70EZg== + "@reshadow/babel@^0.0.1-alpha.76", "@reshadow/babel@^0.0.1-issue-119.0": version "0.0.1-issue-119.0" resolved "https://registry.yarnpkg.com/@reshadow/babel/-/babel-0.0.1-issue-119.0.tgz#ef0e9228e06f821e3d2287f01569fb076153dbb5" @@ -2745,6 +2750,16 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== +"@types/geojson@*", "@types/geojson@^7946.0.0 || ^1.0.0": + version "7946.0.7" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" + integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== + +"@types/geojson@^1.0.0": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.6.tgz#3e02972728c69248c2af08d60a48cbb8680fffdf" + integrity sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w== + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -2792,6 +2807,13 @@ dependencies: "@types/node" "*" +"@types/leaflet@*": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.0.tgz#3700fb5d29a8214fbd496565b08ec28e40cee808" + integrity sha512-ltv5jR+VjKSMtoDkxH61Rsbo0zLU7iqyOXpVPkAX4F+79fg2eymC7t0msWsfNaEZO1FGTIQATCCCQe+ijWoicg== + dependencies: + "@types/geojson" "*" + "@types/md5@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.2.1.tgz#0e2d65d8f6cc91aafbc9be5a4c08fee9250406ce" @@ -2841,6 +2863,23 @@ dependencies: "@types/react" "^16" +"@types/react-leaflet@^2.8.1": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@types/react-leaflet/-/react-leaflet-2.8.1.tgz#acda592cf1b7dfe20ac81071c21c1e5521fe71a4" + integrity sha512-BbFyH9R/MecfXaz1Yt29oGBG8AxIKILgAniO61/kv9/sRe5/qj9yDxf3PW7M+nDl5Wn3V7Mp9inKF0LflV+KkQ== + dependencies: + "@types/leaflet" "*" + "@types/react" "*" + +"@types/react@*": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" + integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@^16", "@types/react@^16.9.49": version "16.14.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" @@ -2849,6 +2888,11 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/tern@*": version "0.23.3" resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460" @@ -8247,6 +8291,11 @@ latest-version@5.1.0: dependencies: package-json "^6.3.0" +leaflet@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== + lerna@^3.22.1: version "3.22.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" @@ -11102,6 +11151,13 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-leaflet@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-3.1.0.tgz#42deb5e454518016eff8bc85511ae58812f910f5" + integrity sha512-kdZS8NYbYFPmkQr7zSDR2gkKGFeWvkxqoqcmZEckzHL4d5c85dJ2gbbqhaPDpmWWgaRw9O29uA/77qpKmK4mTQ== + dependencies: + "@react-leaflet/core" "^1.0.2" + react-popper@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22" @@ -12769,6 +12825,21 @@ temp-write@^3.4.0: temp-dir "^1.0.0" uuid "^3.0.1" +terraformer-wkt-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/terraformer-wkt-parser/-/terraformer-wkt-parser-1.2.1.tgz#8041e2aeb0c9f2b4cbbec8ec2c5c00c45ddfee02" + integrity sha512-+CJyNLWb3lJ9RsZMTM66BY0MT3yIo4l4l22Jd9CrZuwzk54fsu4Sc7zejuS9fCITTuTQy3p06d4MZMVI7v5wSg== + dependencies: + "@types/geojson" "^1.0.0" + terraformer "~1.0.5" + +terraformer@~1.0.5: + version "1.0.12" + resolved "https://registry.yarnpkg.com/terraformer/-/terraformer-1.0.12.tgz#39e08f9c753606421acce02e122440c72dfa12d3" + integrity sha512-MokUp0+MFal4CmJDVL6VAO1bKegeXcBM2RnPVfqcFIp2IIv8EbPAjG0j/vEy/vuKB8NVMMSF2vfpVS/QLe4DBg== + optionalDependencies: + "@types/geojson" "^7946.0.0 || ^1.0.0" + terser-webpack-plugin@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.1.tgz#7effadee06f7ecfa093dbbd3e9ab23f5f3ed8673" @@ -13249,6 +13320,15 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-loader@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"