mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-22 22:46:01 +00:00
Merge branch 'develop'
This commit is contained in:
@@ -297,4 +297,33 @@ describe('Deploy database', () => {
|
||||
expect(res.rows[0].val.toString()).toEqual('5');
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.enginesPostgre.map(engine => [engine.label, engine]))(
|
||||
'Current timestamp default value - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testDatabaseDeploy(conn, driver, [
|
||||
[
|
||||
{
|
||||
name: 't1.table.yaml',
|
||||
json: {
|
||||
name: 't1',
|
||||
columns: [
|
||||
{ name: 'id', type: 'int' },
|
||||
{
|
||||
name: 'val',
|
||||
type: 'timestamp',
|
||||
default: 'current_timestamp',
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
await driver.query(conn, `insert into t1 (id) values (1)`);
|
||||
const res = await driver.query(conn, ` select val from t1 where id = 1`);
|
||||
expect(res.rows[0].val.toString().substring(0, 2)).toEqual('20');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -141,6 +141,10 @@ const filterLocal = [
|
||||
'-CockroachDB',
|
||||
];
|
||||
|
||||
const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL');
|
||||
|
||||
module.exports = process.env.CITEST
|
||||
? engines.filter(x => !x.skipOnCI)
|
||||
: engines.filter(x => filterLocal.find(y => x.label == y));
|
||||
|
||||
module.exports.enginesPostgre = enginesPostgre;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "5.0.3",
|
||||
"version": "5.0.4-beta.1",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -8,6 +8,7 @@ const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
const apps = require('./apps');
|
||||
const getMapExport = require('../utility/getMapExport');
|
||||
|
||||
function serialize(format, data) {
|
||||
if (format == 'text') return data;
|
||||
@@ -187,6 +188,12 @@ module.exports = {
|
||||
return true;
|
||||
},
|
||||
|
||||
exportMap_meta: true,
|
||||
async exportMap({ filePath, geoJson }) {
|
||||
await fs.writeFile(filePath, getMapExport(geoJson));
|
||||
return true;
|
||||
},
|
||||
|
||||
exportDiagram_meta: true,
|
||||
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
||||
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
||||
|
||||
77
packages/api/src/utility/getMapExport.js
Normal file
77
packages/api/src/utility/getMapExport.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const getMapExport = (geoJson) => {
|
||||
return `<html>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
|
||||
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
|
||||
crossorigin=""/>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
|
||||
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script>
|
||||
function createMap() {
|
||||
map = leaflet.map('map').setView([50, 15], 13);
|
||||
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '<a href="https://dbgate.org" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
const geoJsonObj = leaflet
|
||||
.geoJSON(${JSON.stringify(geoJson)}, {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4,
|
||||
};
|
||||
},
|
||||
pointToLayer: (feature, latlng) => {
|
||||
return leaflet.circleMarker(latlng, {
|
||||
radius: 7,
|
||||
weight: 2,
|
||||
fillColor: '#ff0000',
|
||||
color: '#ff0000',
|
||||
opacity: 0.9,
|
||||
fillOpacity: 0.9,
|
||||
});
|
||||
},
|
||||
onEachFeature: (feature, layer) => {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
layer.bindTooltip(feature.properties.popupContent);
|
||||
}
|
||||
},
|
||||
})
|
||||
.addTo(map);
|
||||
map.fitBounds(geoJsonObj.getBounds());
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload='createMap()'>
|
||||
<div id='map'></div>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
};
|
||||
|
||||
module.exports = getMapExport;
|
||||
@@ -483,6 +483,22 @@ export abstract class GridDisplay {
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {}
|
||||
|
||||
createColumnExpression(col, source, alias?) {
|
||||
let expr = null;
|
||||
if (this.dialect.createColumnViewExpression) {
|
||||
expr = this.dialect.createColumnViewExpression(col.columnName, col.dataType, source, alias);
|
||||
if (expr) {
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
return {
|
||||
exprType: 'column',
|
||||
alias: alias || col.columnName,
|
||||
source,
|
||||
...col,
|
||||
};
|
||||
}
|
||||
|
||||
createSelectBase(name: NamedObjectInfo, columns: ColumnInfo[], options) {
|
||||
if (!columns) return null;
|
||||
const orderColumnName = columns[0].columnName;
|
||||
@@ -492,12 +508,7 @@ export abstract class GridDisplay {
|
||||
name: _.pick(name, ['schemaName', 'pureName']),
|
||||
alias: 'basetbl',
|
||||
},
|
||||
columns: columns.map(col => ({
|
||||
exprType: 'column',
|
||||
alias: col.columnName,
|
||||
source: { alias: 'basetbl' },
|
||||
...col,
|
||||
})),
|
||||
columns: columns.map(col => this.createColumnExpression(col, { alias: 'basetbl' })),
|
||||
orderBy: [
|
||||
{
|
||||
exprType: 'column',
|
||||
|
||||
@@ -267,12 +267,9 @@ export class TableGridDisplay extends GridDisplay {
|
||||
) {
|
||||
for (const column of columns) {
|
||||
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
|
||||
select.columns.push({
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
alias: column.uniqueName,
|
||||
source: { name: column, alias: parentAlias },
|
||||
});
|
||||
select.columns.push(
|
||||
this.createColumnExpression(column, { name: column, alias: parentAlias }, column.uniqueName)
|
||||
);
|
||||
displayedColumnInfo[column.uniqueName] = {
|
||||
...column,
|
||||
sourceAlias: parentAlias,
|
||||
|
||||
@@ -35,17 +35,24 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'methodCall':
|
||||
dumpSqlExpression(dmp, expr.thisObject)
|
||||
dmp.put('.%s(', expr.method);
|
||||
dmp.putCollection(',', expr.args, x => dumpSqlExpression(dmp, x));
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'transform':
|
||||
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
|
||||
break;
|
||||
|
||||
case 'rowNumber':
|
||||
dmp.put(" ^row_number() ^over (^order ^by ");
|
||||
dmp.put(' ^row_number() ^over (^order ^by ');
|
||||
dmp.putCollection(', ', expr.orderBy, x => {
|
||||
dumpSqlExpression(dmp, x);
|
||||
dmp.put(' %k', x.direction);
|
||||
});
|
||||
dmp.put(")");
|
||||
dmp.put(')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ export function evaluateExpression(expr: Expression, values) {
|
||||
case 'call':
|
||||
return null;
|
||||
|
||||
case 'methodCall':
|
||||
return null;
|
||||
|
||||
case 'transform':
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,13 @@ export interface CallExpression {
|
||||
argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT
|
||||
}
|
||||
|
||||
export interface MethodCallExpression {
|
||||
exprType: 'methodCall';
|
||||
method: string;
|
||||
args: Expression[];
|
||||
thisObject: Expression;
|
||||
}
|
||||
|
||||
export interface TranformExpression {
|
||||
exprType: 'transform';
|
||||
expr: Expression;
|
||||
@@ -172,6 +179,7 @@ export type Expression =
|
||||
| PlaceholderExpression
|
||||
| RawExpression
|
||||
| CallExpression
|
||||
| MethodCallExpression
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
@@ -84,3 +84,12 @@ export function getIconForRedisType(type) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWktGeometry(s) {
|
||||
if (!_isString(s)) return false;
|
||||
|
||||
// return !!s.match(/^POINT\s*\(|/)
|
||||
return !!s.match(
|
||||
/^POINT\s*\(|^LINESTRING\s*\(|^POLYGON\s*\(|^MULTIPOINT\s*\(|^MULTILINESTRING\s*\(|^MULTIPOLYGON\s*\(|^GEOMCOLLECTION\s*\(|^GEOMETRYCOLLECTION\s*\(/
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ColumnInfo, TableInfo, ForeignKeyInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { StringNullableChain } from 'lodash';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _compact from 'lodash/compact';
|
||||
import { DatabaseAnalyser } from './DatabaseAnalyser';
|
||||
@@ -11,6 +12,7 @@ export interface ColumnInfoYaml {
|
||||
autoIncrement?: boolean;
|
||||
references?: string;
|
||||
primaryKey?: boolean;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseModelFile {
|
||||
@@ -39,6 +41,7 @@ function columnInfoToYaml(column: ColumnInfo, table: TableInfo): ColumnInfoYaml
|
||||
const res: ColumnInfoYaml = {
|
||||
name: column.columnName,
|
||||
type: column.dataType,
|
||||
default: column.defaultValue,
|
||||
};
|
||||
if (column.autoIncrement) res.autoIncrement = true;
|
||||
if (column.notNull) res.notNull = true;
|
||||
@@ -71,6 +74,7 @@ function columnInfoFromYaml(column: ColumnInfoYaml, table: TableInfoYaml): Colum
|
||||
dataType: column.length ? `${column.type}(${column.length})` : column.type,
|
||||
autoIncrement: column.autoIncrement,
|
||||
notNull: column.notNull || (table.primaryKey && table.primaryKey.includes(column.name)),
|
||||
defaultValue: column.default,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
3
packages/types/dialect.d.ts
vendored
3
packages/types/dialect.d.ts
vendored
@@ -34,4 +34,7 @@ export interface SqlDialect {
|
||||
disableExplicitTransaction?: boolean;
|
||||
|
||||
predefinedDataTypes: string[];
|
||||
|
||||
// create sql-tree expression
|
||||
createColumnViewExpression(columnName: string, dataType: string, source: { alias: string }, alias?: string): any;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"dependencies": {
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"interval-operations": "^1.0.7"
|
||||
"interval-operations": "^1.0.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"wellknown": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
7
packages/web/src/celldata/MapCellView.svelte
Normal file
7
packages/web/src/celldata/MapCellView.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MapView from '../elements/MapView.svelte';
|
||||
|
||||
export let selection;
|
||||
</script>
|
||||
|
||||
<MapView {selection} />
|
||||
@@ -112,6 +112,14 @@
|
||||
onClick: () => getCurrentDataGrid().editJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openSelectionInMap',
|
||||
category: 'Data grid',
|
||||
name: 'Open selection in map',
|
||||
testEnabled: () => getCurrentDataGrid() != null, // ?.openSelectionInMapEnabled(),
|
||||
onClick: () => getCurrentDataGrid().openSelectionInMap(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonDocument',
|
||||
category: 'Data grid',
|
||||
@@ -306,6 +314,8 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { isCtrlOrCommandKey, isMac } from '../utility/common';
|
||||
import { selectionCouldBeShownOnMap } from '../elements/MapView.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -530,6 +540,23 @@
|
||||
openJsonDocument(json);
|
||||
}
|
||||
|
||||
export function openSelectionInMap() {
|
||||
const selection = getCellsPublished(selectedCells);
|
||||
if (!selectionCouldBeShownOnMap(selection)) {
|
||||
showModal(ErrorMessageModal, { message: 'There is nothing to be shown on map' });
|
||||
return;
|
||||
}
|
||||
openNewTab({
|
||||
title: 'Map',
|
||||
icon: 'img map',
|
||||
tabComponent: 'MapTab',
|
||||
props: {
|
||||
selection,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
function getSelectedExportableCell() {
|
||||
const electron = getElectron();
|
||||
if (electron && selectedCells.length == 1) {
|
||||
@@ -1426,6 +1453,7 @@
|
||||
{ command: 'dataGrid.generateSqlFromData' },
|
||||
{ command: 'dataGrid.openFreeTable' },
|
||||
{ command: 'dataGrid.openChartFromSelection' },
|
||||
{ command: 'dataGrid.openSelectionInMap', hideDisabled: true },
|
||||
{ placeTag: 'chart' }
|
||||
);
|
||||
|
||||
|
||||
201
packages/web/src/elements/MapView.svelte
Normal file
201
packages/web/src/elements/MapView.svelte
Normal file
@@ -0,0 +1,201 @@
|
||||
<script lang="ts" context="module">
|
||||
export function selectionCouldBeShownOnMap(selection) {
|
||||
console.log('selection', selection);
|
||||
if (selection.length > 0 && _.find(selection, x => isWktGeometry(x.value))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
selection.find(x => x.column.toLowerCase().includes('lat')) &&
|
||||
(selection.find(x => x.column.toLowerCase().includes('lon')) ||
|
||||
selection.find(x => x.column.toLowerCase().includes('lng')))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import leaflet from 'leaflet';
|
||||
import wellknown from 'wellknown';
|
||||
import { isWktGeometry, ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
|
||||
import resizeObserver from '../utility/resizeObserver';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import { saveExportedFile, saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getCurrentConfig } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
export let selection;
|
||||
|
||||
let refContainer;
|
||||
let map;
|
||||
|
||||
let selectionLayers = [];
|
||||
let geoJson;
|
||||
|
||||
function createColumnsTable(cells) {
|
||||
return `<table>${cells.map(cell => `<tr><td>${cell.column}</td><td>${cell.value}</td></tr>`).join('\n')}</table>`;
|
||||
}
|
||||
|
||||
function addSelectionToMap() {
|
||||
if (!map) return;
|
||||
if (!selection) return;
|
||||
|
||||
for (const selectionLayer of selectionLayers) {
|
||||
selectionLayer.remove();
|
||||
}
|
||||
selectionLayers = [];
|
||||
|
||||
const selectedRows = _.groupBy(selection || [], 'row');
|
||||
|
||||
const features = [];
|
||||
|
||||
for (const rowKey of _.keys(selectedRows)) {
|
||||
const cells = selectedRows[rowKey];
|
||||
const lat = cells.find(x => x.column.toLowerCase().includes('lat'));
|
||||
const lon = cells.find(x => x.column.toLowerCase().includes('lon') || x.column.toLowerCase().includes('lng'));
|
||||
|
||||
const geoValues = cells.map(x => x.value).filter(isWktGeometry);
|
||||
|
||||
if (lat && lon) {
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
popupContent: createColumnsTable(cells),
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [lon.value, lat.value],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (geoValues.length > 0) {
|
||||
// parse WKT to geoJSON array
|
||||
features.push(
|
||||
...geoValues.map(wellknown).map(geometry => ({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
popupContent: createColumnsTable(cells.filter(x => !isWktGeometry(x.value))),
|
||||
},
|
||||
geometry,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (features.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
geoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features,
|
||||
};
|
||||
|
||||
const geoJsonObj = leaflet
|
||||
.geoJSON(geoJson, {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4,
|
||||
};
|
||||
},
|
||||
pointToLayer: (feature, latlng) => {
|
||||
return leaflet.circleMarker(latlng, {
|
||||
radius: 7,
|
||||
weight: 2,
|
||||
fillColor: '#ff0000',
|
||||
color: '#ff0000',
|
||||
opacity: 0.9,
|
||||
fillOpacity: 0.9,
|
||||
});
|
||||
},
|
||||
onEachFeature: (feature, layer) => {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
layer.bindTooltip(feature.properties.popupContent);
|
||||
}
|
||||
},
|
||||
})
|
||||
.addTo(map);
|
||||
// geoJsonObj.bindPopup('This is the Transamerica Pyramid'); //.openPopup();
|
||||
map.fitBounds(geoJsonObj.getBounds());
|
||||
selectionLayers.push(geoJsonObj);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
map = leaflet.map(refContainer).setView([50, 15], 13);
|
||||
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
addSelectionToMap();
|
||||
// map.fitBounds([
|
||||
// [50, 15],
|
||||
// [50.1, 15],
|
||||
// [50, 15.1],
|
||||
// ]);
|
||||
|
||||
// const marker = leaflet.marker([50, 15]).addTo(map);
|
||||
// <div bind:this={refContainer} class="flex1 map-container" />
|
||||
});
|
||||
|
||||
$: {
|
||||
selection;
|
||||
addSelectionToMap();
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{
|
||||
text: 'Open on new tab',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Map',
|
||||
icon: 'img map',
|
||||
tabComponent: 'MapTab',
|
||||
props: {
|
||||
selection,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Export to HTML file',
|
||||
onClick: () => {
|
||||
saveFileToDisk(async filePath => {
|
||||
await apiCall('files/export-map', {
|
||||
geoJson,
|
||||
filePath,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={refContainer}
|
||||
use:contextMenu={createMenu}
|
||||
class="flex1"
|
||||
use:resizeObserver={true}
|
||||
on:resize={async e => {
|
||||
await tick();
|
||||
map.invalidateSize();
|
||||
}}
|
||||
/>
|
||||
@@ -165,6 +165,7 @@
|
||||
|
||||
'img sort-asc': 'mdi mdi-sort-alphabetical-ascending color-icon-green',
|
||||
'img sort-desc': 'mdi mdi-sort-alphabetical-descending color-icon-green',
|
||||
'img map': 'mdi mdi-map color-icon-blue',
|
||||
|
||||
'img reference': 'mdi mdi-link-box',
|
||||
'img link': 'mdi mdi-link',
|
||||
|
||||
8
packages/web/src/tabs/MapTab.svelte
Normal file
8
packages/web/src/tabs/MapTab.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import MapView from '../elements/MapView.svelte';
|
||||
|
||||
export let selection;
|
||||
|
||||
</script>
|
||||
|
||||
<MapView {selection} />
|
||||
@@ -24,6 +24,7 @@ import * as DiagramTab from './DiagramTab.svelte';
|
||||
import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
|
||||
import * as QueryDataTab from './QueryDataTab.svelte';
|
||||
import * as ConnectionTab from './ConnectionTab.svelte';
|
||||
import * as MapTab from './MapTab.svelte';
|
||||
|
||||
export default {
|
||||
TableDataTab,
|
||||
@@ -52,4 +53,5 @@ export default {
|
||||
DbKeyDetailTab,
|
||||
QueryDataTab,
|
||||
ConnectionTab,
|
||||
MapTab,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import { isWktGeometry } from 'dbgate-tools';
|
||||
|
||||
const formats = [
|
||||
{
|
||||
type: 'textWrap',
|
||||
@@ -36,12 +38,23 @@
|
||||
component: HtmlCellView,
|
||||
single: false,
|
||||
},
|
||||
{
|
||||
type: 'map',
|
||||
title: 'Map',
|
||||
component: MapCellView,
|
||||
single: false,
|
||||
},
|
||||
];
|
||||
|
||||
function autodetect(selection) {
|
||||
if (selection[0]?.engine?.databaseEngineTypes?.includes('document')) {
|
||||
return 'jsonRow';
|
||||
}
|
||||
|
||||
if (selectionCouldBeShownOnMap(selection)) {
|
||||
return 'map';
|
||||
}
|
||||
|
||||
const value = selection.length == 1 ? selection[0].value : null;
|
||||
if (_.isString(value)) {
|
||||
if (value.startsWith('[') || value.startsWith('{')) return 'json';
|
||||
@@ -62,10 +75,12 @@
|
||||
import HtmlCellView from '../celldata/HtmlCellView.svelte';
|
||||
import JsonCellView from '../celldata/JsonCellView.svelte';
|
||||
import JsonRowView from '../celldata/JsonRowView.svelte';
|
||||
import MapCellView from '../celldata/MapCellView.svelte';
|
||||
import PictureCellView from '../celldata/PictureCellView.svelte';
|
||||
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
|
||||
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
import { selectionCouldBeShownOnMap } from '../elements/MapView.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import { selectedCellsCallback } from '../stores';
|
||||
import WidgetTitle from './WidgetTitle.svelte';
|
||||
|
||||
@@ -2,6 +2,8 @@ const { driverBase } = global.DBGATE_TOOLS;
|
||||
const MsSqlDumper = require('./MsSqlDumper');
|
||||
const { mssqlSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
|
||||
const spatialTypes = ['GEOGRAPHY'];
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
limitSelect: true,
|
||||
@@ -70,6 +72,21 @@ const dialect = {
|
||||
'image',
|
||||
'xml',
|
||||
],
|
||||
|
||||
createColumnViewExpression(columnName, dataType, source, alias) {
|
||||
if (dataType && spatialTypes.includes(dataType.toUpperCase())) {
|
||||
return {
|
||||
exprType: 'methodCall',
|
||||
method: 'STAsText',
|
||||
alias: alias || columnName,
|
||||
thisObject: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').EngineDriver} */
|
||||
|
||||
@@ -2,6 +2,18 @@ const { driverBase } = global.DBGATE_TOOLS;
|
||||
const { mysqlSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
const Dumper = require('./Dumper');
|
||||
|
||||
const spatialTypes = [
|
||||
'POINT',
|
||||
'LINESTRING',
|
||||
'POLYGON',
|
||||
'GEOMETRY',
|
||||
'MULTIPOINT',
|
||||
'MULTILINESTRING',
|
||||
'MULTIPOLYGON',
|
||||
'GEOMCOLLECTION',
|
||||
'GEOMETRYCOLLECTION',
|
||||
];
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
@@ -68,6 +80,23 @@ const dialect = {
|
||||
'time',
|
||||
'year',
|
||||
],
|
||||
|
||||
createColumnViewExpression(columnName, dataType, source, alias) {
|
||||
if (dataType && spatialTypes.includes(dataType.toUpperCase())) {
|
||||
return {
|
||||
exprType: 'call',
|
||||
func: 'ST_AsText',
|
||||
alias: alias || columnName,
|
||||
args: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const mysqlDriverBase = {
|
||||
|
||||
@@ -11,21 +11,36 @@ function normalizeTypeName(dataType) {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
function getColumnInfo({
|
||||
is_nullable,
|
||||
column_name,
|
||||
data_type,
|
||||
char_max_length,
|
||||
numeric_precision,
|
||||
numeric_ccale,
|
||||
default_value,
|
||||
}) {
|
||||
function getColumnInfo(
|
||||
{ is_nullable, column_name, data_type, char_max_length, numeric_precision, numeric_ccale, default_value },
|
||||
table = undefined,
|
||||
geometryColumns = undefined,
|
||||
geographyColumns = undefined
|
||||
) {
|
||||
const normDataType = normalizeTypeName(data_type);
|
||||
let fullDataType = normDataType;
|
||||
if (char_max_length && isTypeString(normDataType)) fullDataType = `${normDataType}(${char_max_length})`;
|
||||
if (numeric_precision && numeric_ccale && isTypeNumeric(normDataType))
|
||||
fullDataType = `${normDataType}(${numeric_precision},${numeric_ccale})`;
|
||||
const autoIncrement = !!(default_value && default_value.startsWith('nextval('));
|
||||
if (
|
||||
table &&
|
||||
geometryColumns &&
|
||||
geometryColumns.rows.find(
|
||||
x => x.schema_name == table.schemaName && x.pure_name == table.pureName && x.column_name == column_name
|
||||
)
|
||||
) {
|
||||
fullDataType = 'geometry';
|
||||
}
|
||||
if (
|
||||
table &&
|
||||
geographyColumns &&
|
||||
geographyColumns.rows.find(
|
||||
x => x.schema_name == table.schemaName && x.pure_name == table.pureName && x.column_name == column_name
|
||||
)
|
||||
) {
|
||||
fullDataType = 'geography';
|
||||
}
|
||||
return {
|
||||
columnName: column_name,
|
||||
dataType: fullDataType,
|
||||
@@ -145,6 +160,18 @@ class Analyser extends DatabaseAnalyser {
|
||||
: await this.driver.query(this.pool, this.createQuery('indexcols', ['tables']));
|
||||
this.feedback({ analysingMessage: 'Loading unique names' });
|
||||
const uniqueNames = await this.driver.query(this.pool, this.createQuery('uniqueNames', ['tables']));
|
||||
|
||||
let geometryColumns = { rows: [] };
|
||||
if (views.rows.find(x => x.pure_name == 'geometry_columns' && x.schema_name == 'public')) {
|
||||
this.feedback({ analysingMessage: 'Loading geometry columns' });
|
||||
geometryColumns = await this.safeQuery(this.createQuery('geometryColumns', ['tables']));
|
||||
}
|
||||
let geographyColumns = { rows: [] };
|
||||
if (views.rows.find(x => x.pure_name == 'geography_columns' && x.schema_name == 'public')) {
|
||||
this.feedback({ analysingMessage: 'Loading geography columns' });
|
||||
geographyColumns = await this.safeQuery(this.createQuery('geographyColumns', ['tables']));
|
||||
}
|
||||
|
||||
this.feedback({ analysingMessage: 'Finalizing DB structure' });
|
||||
|
||||
const columnColumnsMapped = fkColumns.rows.map(x => ({
|
||||
@@ -179,7 +206,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
...newTable,
|
||||
columns: columns.rows
|
||||
.filter(col => col.pure_name == table.pure_name && col.schema_name == table.schema_name)
|
||||
.map(getColumnInfo),
|
||||
.map(col => getColumnInfo(col, newTable, geometryColumns, geographyColumns)),
|
||||
primaryKey: DatabaseAnalyser.extractPrimaryKeys(newTable, pkColumnsMapped),
|
||||
foreignKeys: DatabaseAnalyser.extractForeignKeys(newTable, columnColumnsMapped),
|
||||
indexes: indexes.rows
|
||||
@@ -231,7 +258,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
createSql: `CREATE VIEW "${view.schema_name}"."${view.pure_name}"\nAS\n${view.create_sql}`,
|
||||
columns: columns.rows
|
||||
.filter(col => col.pure_name == view.pure_name && col.schema_name == view.schema_name)
|
||||
.map(getColumnInfo),
|
||||
.map(col => getColumnInfo(col)),
|
||||
})),
|
||||
matviews: matviews
|
||||
? matviews.rows.map(matview => ({
|
||||
@@ -242,7 +269,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
createSql: `CREATE MATERIALIZED VIEW "${matview.schema_name}"."${matview.pure_name}"\nAS\n${matview.definition}`,
|
||||
columns: matviewColumns.rows
|
||||
.filter(col => col.pure_name == matview.pure_name && col.schema_name == matview.schema_name)
|
||||
.map(getColumnInfo),
|
||||
.map(col => getColumnInfo(col)),
|
||||
}))
|
||||
: undefined,
|
||||
procedures: routines.rows
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = `
|
||||
select
|
||||
f_table_schema as "schema_name",
|
||||
f_table_name as "pure_name",
|
||||
f_geography_column as "column_name"
|
||||
from public.geography_columns
|
||||
where ('tables:' || f_table_schema || '.' || f_table_name) =OBJECT_ID_CONDITION
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = `
|
||||
select
|
||||
f_table_schema as "schema_name",
|
||||
f_table_name as "pure_name",
|
||||
f_geometry_column as "column_name"
|
||||
from public.geometry_columns
|
||||
where ('tables:' || f_table_schema || '.' || f_table_name) =OBJECT_ID_CONDITION
|
||||
`;
|
||||
@@ -13,6 +13,8 @@ const matviewColumns = require('./matviewColumns');
|
||||
const indexes = require('./indexes');
|
||||
const indexcols = require('./indexcols');
|
||||
const uniqueNames = require('./uniqueNames');
|
||||
const geometryColumns = require('./geometryColumns');
|
||||
const geographyColumns = require('./geographyColumns');
|
||||
|
||||
const fk_keyColumnUsage = require('./fk_key_column_usage');
|
||||
const fk_referentialConstraints = require('./fk_referential_constraints');
|
||||
@@ -37,4 +39,6 @@ module.exports = {
|
||||
indexes,
|
||||
indexcols,
|
||||
uniqueNames,
|
||||
geometryColumns,
|
||||
geographyColumns,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ const { driverBase } = global.DBGATE_TOOLS;
|
||||
const Dumper = require('./Dumper');
|
||||
const { postgreSplitterOptions } = require('dbgate-query-splitter/lib/options');
|
||||
|
||||
const spatialTypes = ['GEOGRAPHY'];
|
||||
|
||||
/** @type {import('dbgate-types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
@@ -78,6 +80,23 @@ const dialect = {
|
||||
'uuid',
|
||||
'xml',
|
||||
],
|
||||
|
||||
createColumnViewExpression(columnName, dataType, source, alias) {
|
||||
if (dataType && spatialTypes.includes(dataType.toUpperCase())) {
|
||||
return {
|
||||
exprType: 'call',
|
||||
func: 'ST_AsText',
|
||||
alias: alias || columnName,
|
||||
args: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const postgresDriverBase = {
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@@ -2956,6 +2956,15 @@ concat-stream@^1.5.0:
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
concat-stream@~1.5.0:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
|
||||
integrity sha512-H6xsIBfQ94aESBG8jGHXQ7i5AEpy5ZeVaLDOisDICiTCKpqEfr34/KmTrspKQNoLKNu9gTkovlpQcUi630AKiQ==
|
||||
dependencies:
|
||||
inherits "~2.0.1"
|
||||
readable-stream "~2.0.0"
|
||||
typedarray "~0.0.5"
|
||||
|
||||
concurrently@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.1.0.tgz#05523986ba7aaf4b58a49ddd658fab88fa783132"
|
||||
@@ -6993,6 +7002,11 @@ lcid@^2.0.0:
|
||||
dependencies:
|
||||
invert-kv "^2.0.0"
|
||||
|
||||
leaflet@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e"
|
||||
integrity sha512-gwhMjFCQiYs3x/Sf+d49f10ERXaEFCPr+nVTryhAW8DWbMGqJqt9G4XuIaHmFW08zYvhgdzqXGr8AlW8v8dQkA==
|
||||
|
||||
left-pad@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
|
||||
@@ -7520,6 +7534,11 @@ minimist@^1.2.3, minimist@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minimist@~1.2.0:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
minipass-collect@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
|
||||
@@ -8841,6 +8860,11 @@ printj@~1.1.0, printj@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
|
||||
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
|
||||
|
||||
process-nextick-args@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
|
||||
integrity sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
@@ -9156,6 +9180,18 @@ readable-stream@~1.1.9:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@~2.0.0:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
|
||||
integrity sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~1.0.6"
|
||||
string_decoder "~0.10.x"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readdirp@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
|
||||
@@ -10960,7 +10996,7 @@ typedarray-to-buffer@^3.1.5:
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typedarray@^0.0.6:
|
||||
typedarray@^0.0.6, typedarray@~0.0.5:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
@@ -11332,6 +11368,14 @@ webpack@^4.42.0:
|
||||
watchpack "^1.6.0"
|
||||
webpack-sources "^1.4.1"
|
||||
|
||||
wellknown@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101"
|
||||
integrity sha1-Ca6YcfqCbPCm7BU37wDDedeNcQE=
|
||||
dependencies:
|
||||
concat-stream "~1.5.0"
|
||||
minimist "~1.2.0"
|
||||
|
||||
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
|
||||
|
||||
Reference in New Issue
Block a user