mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-05-01 15:33:57 +00:00
Merge branch 'develop'
This commit is contained in:
3
.github/workflows/build-app-beta.yaml
vendored
3
.github/workflows/build-app-beta.yaml
vendored
@@ -27,6 +27,9 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
- name: yarn adjustPackageJson
|
||||||
|
run: |
|
||||||
|
yarn adjustPackageJson
|
||||||
- name: yarn install
|
- name: yarn install
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
yarn install
|
||||||
|
|||||||
3
.github/workflows/build-app.yaml
vendored
3
.github/workflows/build-app.yaml
vendored
@@ -31,6 +31,9 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
- name: yarn adjustPackageJson
|
||||||
|
run: |
|
||||||
|
yarn adjustPackageJson
|
||||||
- name: yarn install
|
- name: yarn install
|
||||||
run: |
|
run: |
|
||||||
# yarn --version
|
# yarn --version
|
||||||
|
|||||||
11
.github/workflows/run-tests.yaml
vendored
11
.github/workflows/run-tests.yaml
vendored
@@ -31,6 +31,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd packages/filterparser
|
cd packages/filterparser
|
||||||
yarn test:ci
|
yarn test:ci
|
||||||
|
- name: Datalib (perspective) tests
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
cd packages/datalib
|
||||||
|
yarn test:ci
|
||||||
- uses: tanmen/jest-reporter@v1
|
- uses: tanmen/jest-reporter@v1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
@@ -43,6 +48,12 @@ jobs:
|
|||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
result-file: packages/filterparser/result.json
|
result-file: packages/filterparser/result.json
|
||||||
action-name: Filter parser test results
|
action-name: Filter parser test results
|
||||||
|
- uses: tanmen/jest-reporter@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
result-file: packages/datalib/result.json
|
||||||
|
action-name: Datalib (perspectives) test results
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest"
|
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest",
|
||||||
|
"cSpell.words": [
|
||||||
|
"dbgate"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -8,6 +8,17 @@ Builds:
|
|||||||
- linux - application for linux
|
- linux - application for linux
|
||||||
- win - application for Windows
|
- win - application for Windows
|
||||||
|
|
||||||
|
### 5.1.0
|
||||||
|
- ADDED: Perspectives
|
||||||
|
- CHANGED: Upgraded SQLite engine version (driver better-sqlite3: 7.6.2)
|
||||||
|
- CHANGED: Upgraded ElectronJS version (from version 13 to version 17)
|
||||||
|
- CHANGED: Upgraded all dependencies with current available minor version updates
|
||||||
|
- CHANGED: By deffault, connect on click #332˝
|
||||||
|
- CHANGED: Improved keyboard navigation, when editing table data #331
|
||||||
|
- ADDED: Option to skip Save changes dialog #329
|
||||||
|
- FIXED: Unsigned column doesn't work correctly. #324
|
||||||
|
- FIXED: Connect to MS SQL with doamin user now works also under Linux and Mac #305
|
||||||
|
|
||||||
### 5.0.9
|
### 5.0.9
|
||||||
- FIXED: Fixed problem with SSE events on web version
|
- FIXED: Fixed problem with SSE events on web version
|
||||||
- ADDED: Added menu command "New query designer"
|
- ADDED: Added menu command "New query designer"
|
||||||
|
|||||||
12
adjustPackageJson.js
Normal file
12
adjustPackageJson.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
function adjustFile(file) {
|
||||||
|
const json = JSON.parse(fs.readFileSync(file, { encoding: 'utf-8' }));
|
||||||
|
if (process.platform != 'win32') {
|
||||||
|
delete json.optionalDependencies.msnodesqlv8;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(file, JSON.stringify(json, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustFile('packages/api/package.json');
|
||||||
|
adjustFile('app/package.json');
|
||||||
@@ -107,12 +107,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"copyfiles": "^2.2.0",
|
"copyfiles": "^2.2.0",
|
||||||
"cross-env": "^6.0.3",
|
"cross-env": "^6.0.3",
|
||||||
"electron": "13.6.3",
|
"electron": "17.4.10",
|
||||||
"electron-builder": "22.14.5",
|
"electron-builder": "23.1.0",
|
||||||
"electron-builder-notarize": "^1.4.0"
|
"electron-builder-notarize": "^1.5.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"better-sqlite3": "7.5.0",
|
"better-sqlite3": "7.6.2",
|
||||||
"msnodesqlv8": "^2.4.4"
|
"msnodesqlv8": "^2.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
989
app/yarn.lock
989
app/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "5.0.9",
|
"version": "5.1.0-beta.3",
|
||||||
"name": "dbgate-all",
|
"name": "dbgate-all",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"start:app:local": "cd app && yarn start:local",
|
"start:app:local": "cd app && yarn start:local",
|
||||||
"setCurrentVersion": "node setCurrentVersion",
|
"setCurrentVersion": "node setCurrentVersion",
|
||||||
"generatePadFile": "node generatePadFile",
|
"generatePadFile": "node generatePadFile",
|
||||||
|
"adjustPackageJson": "node adjustPackageJson",
|
||||||
"fillNativeModules": "node fillNativeModules",
|
"fillNativeModules": "node fillNativeModules",
|
||||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
"webpack-cli": "^3.3.11"
|
"webpack-cli": "^3.3.11"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"better-sqlite3": "7.5.0",
|
"better-sqlite3": "7.6.2",
|
||||||
"msnodesqlv8": "^2.4.4"
|
"msnodesqlv8": "^2.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
|
|||||||
const driver = requireEngineDriver(storedConnection);
|
const driver = requireEngineDriver(storedConnection);
|
||||||
try {
|
try {
|
||||||
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
||||||
|
// console.log(sql);
|
||||||
const res = await driver.query(systemConnection, sql);
|
const res = await driver.query(systemConnection, sql);
|
||||||
process.send({ msgtype: 'response', msgid, ...res });
|
process.send({ msgtype: 'response', msgid, ...res });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
5
packages/datalib/jest.config.js
Normal file
5
packages/datalib/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
moduleFileExtensions: ['js'],
|
||||||
|
};
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
"typings": "lib/index.d.ts",
|
"typings": "lib/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
"test": "jest",
|
||||||
|
"test:ci": "jest --json --outputFile=result.json --testLocationInResults",
|
||||||
"start": "tsc --watch"
|
"start": "tsc --watch"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -12,11 +14,14 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||||
|
"dbgate-tools": "^5.0.0-alpha.1",
|
||||||
"dbgate-filterparser": "^5.0.0-alpha.1"
|
"dbgate-filterparser": "^5.0.0-alpha.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dbgate-types": "^5.0.0-alpha.1",
|
"dbgate-types": "^5.0.0-alpha.1",
|
||||||
"@types/node": "^13.7.0",
|
"@types/node": "^13.7.0",
|
||||||
|
"jest": "^28.1.3",
|
||||||
|
"ts-jest": "^28.0.7",
|
||||||
"typescript": "^4.4.3"
|
"typescript": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export interface DisplayColumn {
|
|||||||
headerText: string;
|
headerText: string;
|
||||||
uniqueName: string;
|
uniqueName: string;
|
||||||
uniquePath: string[];
|
uniquePath: string[];
|
||||||
notNull: boolean;
|
notNull?: boolean;
|
||||||
autoIncrement?: boolean;
|
autoIncrement?: boolean;
|
||||||
isPrimaryKey?: boolean;
|
isPrimaryKey?: boolean;
|
||||||
foreignKey?: ForeignKeyInfo;
|
foreignKey?: ForeignKeyInfo;
|
||||||
|
|||||||
116
packages/datalib/src/PerspectiveCache.ts
Normal file
116
packages/datalib/src/PerspectiveCache.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { RangeDefinition } from 'dbgate-types';
|
||||||
|
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||||
|
import _pick from 'lodash/pick';
|
||||||
|
import _zip from 'lodash/zip';
|
||||||
|
import _difference from 'lodash/difference';
|
||||||
|
import debug from 'debug';
|
||||||
|
import stableStringify from 'json-stable-stringify';
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectiveCache');
|
||||||
|
|
||||||
|
export class PerspectiveBindingGroup {
|
||||||
|
constructor(public table: PerspectiveCacheTable) {}
|
||||||
|
|
||||||
|
groupSize?: number;
|
||||||
|
loadedAll: boolean;
|
||||||
|
loadedRows: any[] = [];
|
||||||
|
bindingValues: any[];
|
||||||
|
|
||||||
|
matchRow(row) {
|
||||||
|
return this.table.bindingColumns.every((column, index) => row[column] == this.bindingValues[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveCacheTable {
|
||||||
|
constructor(props: PerspectiveDataLoadProps, public cache: PerspectiveCache) {
|
||||||
|
this.schemaName = props.schemaName;
|
||||||
|
this.pureName = props.pureName;
|
||||||
|
this.bindingColumns = props.bindingColumns;
|
||||||
|
this.dataColumns = props.dataColumns;
|
||||||
|
this.loadedAll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaName: string;
|
||||||
|
pureName: string;
|
||||||
|
bindingColumns?: string[];
|
||||||
|
dataColumns: string[];
|
||||||
|
loadedAll: boolean;
|
||||||
|
loadedRows: any[] = [];
|
||||||
|
bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {};
|
||||||
|
|
||||||
|
get loadedCount() {
|
||||||
|
return this.loadedRows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowsResult(props: PerspectiveDataLoadProps): { rows: any[]; incomplete: boolean } {
|
||||||
|
return {
|
||||||
|
rows: this.loadedRows.slice(0, props.topCount),
|
||||||
|
incomplete: props.topCount < this.loadedCount || !this.loadedAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getBindingGroup(groupValues: any[]) {
|
||||||
|
const key = stableStringify(groupValues);
|
||||||
|
return this.bindingGroups[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
getUncachedBindingGroups(props: PerspectiveDataLoadProps): any[][] {
|
||||||
|
const uncached = [];
|
||||||
|
for (const group of props.bindingValues) {
|
||||||
|
const key = stableStringify(group);
|
||||||
|
const item = this.bindingGroups[key];
|
||||||
|
if (!item) {
|
||||||
|
uncached.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uncached;
|
||||||
|
}
|
||||||
|
|
||||||
|
storeGroupSize(props: PerspectiveDataLoadProps, bindingValues: any[], count: number) {
|
||||||
|
const originalBindingValue = props.bindingValues.find(v => _zip(v, bindingValues).every(([x, y]) => x == y));
|
||||||
|
if (originalBindingValue) {
|
||||||
|
const key = stableStringify(originalBindingValue);
|
||||||
|
// console.log('SET SIZE', originalBindingValue, bindingValues, key, count);
|
||||||
|
const group = new PerspectiveBindingGroup(this);
|
||||||
|
group.bindingValues = bindingValues;
|
||||||
|
group.groupSize = count;
|
||||||
|
this.bindingGroups[key] = group;
|
||||||
|
} else {
|
||||||
|
dbg('Group not found', bindingValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveCache {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
tables: { [tableKey: string]: PerspectiveCacheTable } = {};
|
||||||
|
|
||||||
|
getTableCache(props: PerspectiveDataLoadProps) {
|
||||||
|
const tableKey = stableStringify(
|
||||||
|
_pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition'])
|
||||||
|
);
|
||||||
|
let res = this.tables[tableKey];
|
||||||
|
|
||||||
|
if (res && _difference(props.dataColumns, res.dataColumns).length > 0) {
|
||||||
|
dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns);
|
||||||
|
|
||||||
|
// we have incomplete cache
|
||||||
|
delete this.tables[tableKey];
|
||||||
|
res = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
res = new PerspectiveCacheTable(props, this);
|
||||||
|
this.tables[tableKey] = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache could be used
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.tables = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
87
packages/datalib/src/PerspectiveConfig.ts
Normal file
87
packages/datalib/src/PerspectiveConfig.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { DatabaseInfo, ForeignKeyInfo, NamedObjectInfo } from 'dbgate-types';
|
||||||
|
|
||||||
|
export interface PerspectiveConfigColumns {
|
||||||
|
expandedColumns: string[];
|
||||||
|
checkedColumns: string[];
|
||||||
|
uncheckedColumns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerspectiveCustomJoinConfig {
|
||||||
|
joinid: string;
|
||||||
|
joinName: string;
|
||||||
|
baseUniqueName: string;
|
||||||
|
conid?: string;
|
||||||
|
database?: string;
|
||||||
|
refSchemaName?: string;
|
||||||
|
refTableName: string;
|
||||||
|
columns: {
|
||||||
|
baseColumnName: string;
|
||||||
|
refColumnName: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerspectiveFilterColumnInfo {
|
||||||
|
columnName: string;
|
||||||
|
filterType: string;
|
||||||
|
pureName: string;
|
||||||
|
schemaName: string;
|
||||||
|
foreignKey: ForeignKeyInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerspectiveParentFilterConfig {
|
||||||
|
uniqueName: string;
|
||||||
|
}
|
||||||
|
export interface PerspectiveConfig extends PerspectiveConfigColumns {
|
||||||
|
rootObject: { schemaName?: string; pureName: string };
|
||||||
|
filters: { [uniqueName: string]: string };
|
||||||
|
sort: {
|
||||||
|
[parentUniqueName: string]: {
|
||||||
|
uniqueName: string;
|
||||||
|
order: 'ASC' | 'DESC';
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
customJoins: PerspectiveCustomJoinConfig[];
|
||||||
|
parentFilters: PerspectiveParentFilterConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPerspectiveConfig(rootObject: { schemaName?: string; pureName: string }): PerspectiveConfig {
|
||||||
|
return {
|
||||||
|
expandedColumns: [],
|
||||||
|
checkedColumns: [],
|
||||||
|
uncheckedColumns: [],
|
||||||
|
customJoins: [],
|
||||||
|
filters: {},
|
||||||
|
sort: {},
|
||||||
|
rootObject,
|
||||||
|
parentFilters: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChangePerspectiveConfigFunc = (
|
||||||
|
changeFunc: (config: PerspectiveConfig) => PerspectiveConfig,
|
||||||
|
reload?: boolean
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export function extractPerspectiveDatabases(
|
||||||
|
{ conid, database },
|
||||||
|
cfg: PerspectiveConfig
|
||||||
|
): { conid: string; database: string }[] {
|
||||||
|
const res: { conid: string; database: string }[] = [];
|
||||||
|
res.push({ conid, database });
|
||||||
|
|
||||||
|
function add(conid, database) {
|
||||||
|
if (res.find(x => x.conid == conid && x.database == database)) return;
|
||||||
|
res.push({ conid, database });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const custom of cfg.customJoins) {
|
||||||
|
add(custom.conid || conid, custom.database || database);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultipleDatabaseInfo {
|
||||||
|
[conid: string]: {
|
||||||
|
[database: string]: DatabaseInfo;
|
||||||
|
};
|
||||||
|
}
|
||||||
141
packages/datalib/src/PerspectiveDataLoader.ts
Normal file
141
packages/datalib/src/PerspectiveDataLoader.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||||
|
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectiveDataLoader');
|
||||||
|
|
||||||
|
export class PerspectiveDataLoader {
|
||||||
|
constructor(public apiCall) {}
|
||||||
|
|
||||||
|
buildCondition(props: PerspectiveDataLoadProps): Condition {
|
||||||
|
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||||
|
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
if (condition) {
|
||||||
|
conditions.push(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingColumns?.length == 1) {
|
||||||
|
conditions.push({
|
||||||
|
conditionType: 'in',
|
||||||
|
expr: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: bindingColumns[0],
|
||||||
|
source: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: bindingValues.map(x => x[0]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions.length > 0
|
||||||
|
? {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGrouping(props: PerspectiveDataLoadProps) {
|
||||||
|
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props;
|
||||||
|
|
||||||
|
const bindingColumnExpressions = bindingColumns.map(
|
||||||
|
columnName =>
|
||||||
|
({
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
source: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
} as Expression)
|
||||||
|
);
|
||||||
|
|
||||||
|
const select: Select = {
|
||||||
|
commandType: 'select',
|
||||||
|
from: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
exprType: 'call',
|
||||||
|
func: 'COUNT',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
exprType: 'raw',
|
||||||
|
sql: '*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
alias: '_perspective_group_size_',
|
||||||
|
},
|
||||||
|
...bindingColumnExpressions,
|
||||||
|
],
|
||||||
|
where: this.buildCondition(props),
|
||||||
|
};
|
||||||
|
|
||||||
|
select.groupBy = bindingColumnExpressions;
|
||||||
|
|
||||||
|
if (dbg?.enabled) {
|
||||||
|
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.apiCall('database-connections/sql-select', {
|
||||||
|
conid: props.databaseConfig.conid,
|
||||||
|
database: props.databaseConfig.database,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.errorMessage) return response;
|
||||||
|
return response.rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
_perspective_group_size_: parseInt(row._perspective_group_size_),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData(props: PerspectiveDataLoadProps) {
|
||||||
|
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||||
|
|
||||||
|
const select: Select = {
|
||||||
|
commandType: 'select',
|
||||||
|
from: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
columns: dataColumns?.map(columnName => ({
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
source: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
selectAll: !dataColumns,
|
||||||
|
orderBy: orderBy?.map(({ columnName, order }) => ({
|
||||||
|
exprType: 'column',
|
||||||
|
columnName,
|
||||||
|
direction: order,
|
||||||
|
source: {
|
||||||
|
name: { schemaName, pureName },
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
range: props.range,
|
||||||
|
where: this.buildCondition(props),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dbg?.enabled) {
|
||||||
|
dbg(
|
||||||
|
`LOAD DATA, table=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${props.range?.offset},${
|
||||||
|
props.range?.limit
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.apiCall('database-connections/sql-select', {
|
||||||
|
conid: props.databaseConfig.conid,
|
||||||
|
database: props.databaseConfig.database,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.errorMessage) return response;
|
||||||
|
return response.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
205
packages/datalib/src/PerspectiveDataProvider.ts
Normal file
205
packages/datalib/src/PerspectiveDataProvider.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { Condition } from 'dbgate-sqltree';
|
||||||
|
import { RangeDefinition } from 'dbgate-types';
|
||||||
|
import { format } from 'path';
|
||||||
|
import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache';
|
||||||
|
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
|
||||||
|
|
||||||
|
export const PERSPECTIVE_PAGE_SIZE = 100;
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectiveDataProvider');
|
||||||
|
|
||||||
|
export interface PerspectiveDatabaseConfig {
|
||||||
|
conid: string;
|
||||||
|
database: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerspectiveDataLoadProps {
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig;
|
||||||
|
schemaName: string;
|
||||||
|
pureName: string;
|
||||||
|
dataColumns: string[];
|
||||||
|
orderBy: {
|
||||||
|
columnName: string;
|
||||||
|
order: 'ASC' | 'DESC';
|
||||||
|
}[];
|
||||||
|
bindingColumns?: string[];
|
||||||
|
bindingValues?: any[][];
|
||||||
|
range?: RangeDefinition;
|
||||||
|
topCount?: number;
|
||||||
|
condition?: Condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveDataProvider {
|
||||||
|
constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {}
|
||||||
|
async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||||
|
dbg('load data', props);
|
||||||
|
// console.log('LOAD DATA', props);
|
||||||
|
if (props.bindingColumns) {
|
||||||
|
return this.loadDataNested(props);
|
||||||
|
} else {
|
||||||
|
return this.loadDataFlat(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDataNested(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||||
|
const tableCache = this.cache.getTableCache(props);
|
||||||
|
|
||||||
|
const uncached = tableCache.getUncachedBindingGroups(props);
|
||||||
|
if (uncached.length > 0) {
|
||||||
|
const counts = await this.loader.loadGrouping({
|
||||||
|
...props,
|
||||||
|
bindingValues: uncached,
|
||||||
|
});
|
||||||
|
// console.log('COUNTS', counts);
|
||||||
|
for (const resetItem of uncached) {
|
||||||
|
tableCache.storeGroupSize(props, resetItem, 0);
|
||||||
|
}
|
||||||
|
for (const countItem of counts) {
|
||||||
|
const { _perspective_group_size_, ...fields } = countItem;
|
||||||
|
tableCache.storeGroupSize(
|
||||||
|
props,
|
||||||
|
props.bindingColumns.map(col => fields[col]),
|
||||||
|
_perspective_group_size_
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
// console.log('CACHE', tableCache.bindingGroups);
|
||||||
|
|
||||||
|
let groupIndex = 0;
|
||||||
|
let loadCalled = false;
|
||||||
|
let shouldReturn = false;
|
||||||
|
for (; groupIndex < props.bindingValues.length; groupIndex++) {
|
||||||
|
const groupValues = props.bindingValues[groupIndex];
|
||||||
|
const group = tableCache.getBindingGroup(groupValues);
|
||||||
|
|
||||||
|
if (!group.loadedAll) {
|
||||||
|
if (loadCalled) {
|
||||||
|
shouldReturn = true;
|
||||||
|
} else {
|
||||||
|
// we need to load next data
|
||||||
|
await this.loadNextGroup(props, groupIndex);
|
||||||
|
loadCalled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('GRP', groupValues, group);
|
||||||
|
rows.push(...group.loadedRows);
|
||||||
|
if (rows.length >= props.topCount || shouldReturn) {
|
||||||
|
return {
|
||||||
|
rows: rows.slice(0, props.topCount),
|
||||||
|
incomplete: props.topCount < rows.length || !group.loadedAll || groupIndex < props.bindingValues.length - 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupIndex >= props.bindingValues.length) {
|
||||||
|
// all groups are fully loaded
|
||||||
|
return { rows, incomplete: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNextGroup(props: PerspectiveDataLoadProps, groupIndex: number) {
|
||||||
|
const tableCache = this.cache.getTableCache(props);
|
||||||
|
|
||||||
|
const planLoadingGroupIndexes: number[] = [];
|
||||||
|
const planLoadingGroups: PerspectiveBindingGroup[] = [];
|
||||||
|
let planLoadRowCount = 0;
|
||||||
|
|
||||||
|
const loadPlanned = async () => {
|
||||||
|
// console.log(
|
||||||
|
// 'LOAD PLANNED',
|
||||||
|
// planLoadingGroupIndexes,
|
||||||
|
// planLoadingGroupIndexes.map(idx => props.bindingValues[idx])
|
||||||
|
// );
|
||||||
|
const rows = await this.loader.loadData({
|
||||||
|
...props,
|
||||||
|
bindingValues: planLoadingGroupIndexes.map(idx => props.bindingValues[idx]),
|
||||||
|
});
|
||||||
|
// console.log('LOADED PLANNED', rows);
|
||||||
|
// distribute rows into groups
|
||||||
|
for (const row of rows) {
|
||||||
|
const group = planLoadingGroups.find(x => x.matchRow(row));
|
||||||
|
if (group) {
|
||||||
|
group.loadedRows.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const group of planLoadingGroups) {
|
||||||
|
group.loadedAll = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (; groupIndex < props.bindingValues.length; groupIndex++) {
|
||||||
|
const groupValues = props.bindingValues[groupIndex];
|
||||||
|
const group = tableCache.getBindingGroup(groupValues);
|
||||||
|
if (group.loadedAll) continue;
|
||||||
|
if (group.groupSize == 0) {
|
||||||
|
group.loadedAll = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (group.groupSize >= PERSPECTIVE_PAGE_SIZE) {
|
||||||
|
if (planLoadingGroupIndexes.length > 0) {
|
||||||
|
await loadPlanned();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextRows = await this.loader.loadData({
|
||||||
|
...props,
|
||||||
|
topCount: null,
|
||||||
|
range: {
|
||||||
|
offset: group.loadedRows.length,
|
||||||
|
limit: PERSPECTIVE_PAGE_SIZE,
|
||||||
|
},
|
||||||
|
bindingValues: [group.bindingValues],
|
||||||
|
});
|
||||||
|
group.loadedRows = [...group.loadedRows, ...nextRows];
|
||||||
|
group.loadedAll = nextRows.length < PERSPECTIVE_PAGE_SIZE;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (planLoadRowCount + group.groupSize > PERSPECTIVE_PAGE_SIZE) {
|
||||||
|
await loadPlanned();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
planLoadingGroupIndexes.push(groupIndex);
|
||||||
|
planLoadingGroups.push(group);
|
||||||
|
planLoadRowCount += group.groupSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planLoadingGroupIndexes.length > 0) {
|
||||||
|
await loadPlanned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDataFlat(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||||
|
const tableCache = this.cache.getTableCache(props);
|
||||||
|
|
||||||
|
if (props.topCount <= tableCache.loadedCount) {
|
||||||
|
return tableCache.getRowsResult(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load missing rows
|
||||||
|
tableCache.dataColumns = props.dataColumns;
|
||||||
|
|
||||||
|
const nextRows = await this.loader.loadData({
|
||||||
|
...props,
|
||||||
|
topCount: null,
|
||||||
|
range: {
|
||||||
|
offset: tableCache.loadedCount,
|
||||||
|
limit: props.topCount - tableCache.loadedCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextRows.errorMessage) {
|
||||||
|
throw new Error(nextRows.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
tableCache.loadedRows = [...tableCache.loadedRows, ...nextRows];
|
||||||
|
tableCache.loadedAll = nextRows.length < props.topCount - tableCache.loadedCount;
|
||||||
|
|
||||||
|
// const rows=tableCache.getRows(props);
|
||||||
|
|
||||||
|
return tableCache.getRowsResult(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
264
packages/datalib/src/PerspectiveDisplay.ts
Normal file
264
packages/datalib/src/PerspectiveDisplay.ts
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import { getTableChildPerspectiveNodes, PerspectiveTableNode, PerspectiveTreeNode } from './PerspectiveTreeNode';
|
||||||
|
import _max from 'lodash/max';
|
||||||
|
import _range from 'lodash/max';
|
||||||
|
import _fill from 'lodash/fill';
|
||||||
|
import _findIndex from 'lodash/findIndex';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectiveDisplay');
|
||||||
|
|
||||||
|
let lastJoinId = 0;
|
||||||
|
function getJoinId(): number {
|
||||||
|
lastJoinId += 1;
|
||||||
|
return lastJoinId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveDisplayColumn {
|
||||||
|
title: string;
|
||||||
|
dataField: string;
|
||||||
|
parentNodes: PerspectiveTreeNode[] = [];
|
||||||
|
colSpanAtLevel = {};
|
||||||
|
columnIndex = 0;
|
||||||
|
dataNode: PerspectiveTreeNode = null;
|
||||||
|
|
||||||
|
constructor(public display: PerspectiveDisplay) {}
|
||||||
|
|
||||||
|
get rowSpan() {
|
||||||
|
return this.display.columnLevelCount - this.parentNodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
showParent(level: number) {
|
||||||
|
return !!this.colSpanAtLevel[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
getColSpan(level: number) {
|
||||||
|
return this.colSpanAtLevel[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible(level: number) {
|
||||||
|
return level == this.columnLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnLevel() {
|
||||||
|
return this.parentNodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentName(level) {
|
||||||
|
return this.parentNodes[level]?.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentNode(level) {
|
||||||
|
return this.parentNodes[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentTableUniqueName(level) {
|
||||||
|
return this.parentNodes[level]?.headerTableAttributes ? this.parentNodes[level]?.uniqueName : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasParentNode(node: PerspectiveTreeNode) {
|
||||||
|
// return this.parentNodes.includes(node);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PerspectiveSubRowCollection {
|
||||||
|
rows: CollectedPerspectiveDisplayRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollectedPerspectiveDisplayRow {
|
||||||
|
columnIndexes: number[];
|
||||||
|
rowData: any[];
|
||||||
|
subRowCollections: PerspectiveSubRowCollection[];
|
||||||
|
incompleteRowsIndicator?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveDisplayRow {
|
||||||
|
constructor(public display: PerspectiveDisplay) {
|
||||||
|
this.rowData = _fill(Array(display.columns.length), undefined);
|
||||||
|
this.rowSpans = _fill(Array(display.columns.length), 1);
|
||||||
|
this.rowJoinIds = _fill(Array(display.columns.length), 0);
|
||||||
|
this.rowCellSkips = _fill(Array(display.columns.length), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData: any[] = [];
|
||||||
|
rowSpans: number[] = null;
|
||||||
|
rowCellSkips: boolean[] = null;
|
||||||
|
|
||||||
|
rowJoinIds: number[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveDisplay {
|
||||||
|
columns: PerspectiveDisplayColumn[] = [];
|
||||||
|
rows: PerspectiveDisplayRow[] = [];
|
||||||
|
readonly columnLevelCount: number;
|
||||||
|
loadIndicatorsCounts: { [uniqueName: string]: number } = {};
|
||||||
|
|
||||||
|
constructor(public root: PerspectiveTreeNode, rows: any[]) {
|
||||||
|
// dbg('source rows', rows);
|
||||||
|
this.fillColumns(root.childNodes, [root]);
|
||||||
|
if (this.columns.length > 0) {
|
||||||
|
this.columns[0].colSpanAtLevel[0] = this.columns.length;
|
||||||
|
}
|
||||||
|
this.columnLevelCount = _max(this.columns.map(x => x.parentNodes.length)) + 1;
|
||||||
|
const collectedRows = this.collectRows(rows, root.childNodes);
|
||||||
|
dbg('collected rows', collectedRows);
|
||||||
|
// console.log('COLLECTED', JSON.stringify(collectedRows, null, 2));
|
||||||
|
// this.mergeRows(collectedRows);
|
||||||
|
this.mergeRows(collectedRows);
|
||||||
|
// dbg('merged rows', this.rows);
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// 'MERGED',
|
||||||
|
// this.rows.map(r =>
|
||||||
|
// r.incompleteRowsIndicator
|
||||||
|
// ? `************************************ ${r.incompleteRowsIndicator.join('|')}`
|
||||||
|
// : r.rowData.join('|')
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRowAt(rowIndex) {
|
||||||
|
while (this.rows.length <= rowIndex) {
|
||||||
|
this.rows.push(new PerspectiveDisplayRow(this));
|
||||||
|
}
|
||||||
|
return this.rows[rowIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) {
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.isChecked) {
|
||||||
|
this.processColumn(child, parentNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) {
|
||||||
|
if (node.isExpandable) {
|
||||||
|
const countBefore = this.columns.length;
|
||||||
|
this.fillColumns(node.childNodes, [...parentNodes, node]);
|
||||||
|
|
||||||
|
if (this.columns.length > countBefore) {
|
||||||
|
this.columns[countBefore].colSpanAtLevel[parentNodes.length] = this.columns.length - countBefore;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const column = new PerspectiveDisplayColumn(this);
|
||||||
|
column.title = node.columnTitle;
|
||||||
|
column.dataField = node.dataField;
|
||||||
|
column.parentNodes = parentNodes;
|
||||||
|
column.display = this;
|
||||||
|
column.columnIndex = this.columns.length;
|
||||||
|
column.dataNode = node;
|
||||||
|
this.columns.push(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findColumnIndexFromNode(node: PerspectiveTreeNode) {
|
||||||
|
return _findIndex(this.columns, x => x.dataNode.uniqueName == node.uniqueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] {
|
||||||
|
const columnNodes = nodes.filter(x => x.isChecked && !x.isExpandable);
|
||||||
|
const treeNodes = nodes.filter(x => x.isChecked && x.isExpandable);
|
||||||
|
|
||||||
|
const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node));
|
||||||
|
|
||||||
|
const res: CollectedPerspectiveDisplayRow[] = [];
|
||||||
|
for (const sourceRow of sourceRows) {
|
||||||
|
// console.log('PROCESS SOURCE', sourceRow);
|
||||||
|
// row.startIndex = startIndex;
|
||||||
|
const rowData = columnNodes.map(node => sourceRow[node.codeName]);
|
||||||
|
const subRowCollections = [];
|
||||||
|
|
||||||
|
for (const node of treeNodes) {
|
||||||
|
if (sourceRow[node.fieldName]) {
|
||||||
|
const subrows = {
|
||||||
|
rows: this.collectRows(sourceRow[node.fieldName], node.childNodes),
|
||||||
|
};
|
||||||
|
subRowCollections.push(subrows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
rowData,
|
||||||
|
columnIndexes,
|
||||||
|
subRowCollections,
|
||||||
|
incompleteRowsIndicator: sourceRow.incompleteRowsIndicator,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillRowSpans() {
|
||||||
|
for (let col = 0; col < this.columns.length; col++) {
|
||||||
|
// let lastFilledJoinId = null;
|
||||||
|
let lastFilledRow = 0;
|
||||||
|
let rowIndex = 0;
|
||||||
|
|
||||||
|
for (const row of this.rows) {
|
||||||
|
if (
|
||||||
|
row.rowData[col] === undefined &&
|
||||||
|
row.rowJoinIds[col] == this.rows[lastFilledRow].rowJoinIds[col] &&
|
||||||
|
row.rowJoinIds[col]
|
||||||
|
) {
|
||||||
|
row.rowCellSkips[col] = true;
|
||||||
|
this.rows[lastFilledRow].rowSpans[col] = rowIndex - lastFilledRow + 1;
|
||||||
|
} else {
|
||||||
|
lastFilledRow = rowIndex;
|
||||||
|
}
|
||||||
|
rowIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeRows(collectedRows: CollectedPerspectiveDisplayRow[]) {
|
||||||
|
let rowIndex = 0;
|
||||||
|
for (const collectedRow of collectedRows) {
|
||||||
|
const count = this.mergeRow(collectedRow, rowIndex);
|
||||||
|
rowIndex += count;
|
||||||
|
}
|
||||||
|
this.fillRowSpans();
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeRow(collectedRow: CollectedPerspectiveDisplayRow, rowIndex: number): number {
|
||||||
|
if (collectedRow.incompleteRowsIndicator?.length > 0) {
|
||||||
|
for (const indicator of collectedRow.incompleteRowsIndicator) {
|
||||||
|
if (!this.loadIndicatorsCounts[indicator]) {
|
||||||
|
this.loadIndicatorsCounts[indicator] = rowIndex;
|
||||||
|
}
|
||||||
|
if (rowIndex < this.loadIndicatorsCounts[indicator]) {
|
||||||
|
this.loadIndicatorsCounts[indicator] = rowIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainRow = this.getRowAt(rowIndex);
|
||||||
|
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
|
||||||
|
mainRow.rowData[collectedRow.columnIndexes[i]] = collectedRow.rowData[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowCount = 1;
|
||||||
|
for (const subrows of collectedRow.subRowCollections) {
|
||||||
|
let additionalRowCount = 0;
|
||||||
|
let currentRowIndex = rowIndex;
|
||||||
|
for (const subrow of subrows.rows) {
|
||||||
|
const count = this.mergeRow(subrow, currentRowIndex);
|
||||||
|
additionalRowCount += count;
|
||||||
|
currentRowIndex += count;
|
||||||
|
}
|
||||||
|
if (additionalRowCount > rowCount) {
|
||||||
|
rowCount = additionalRowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinId = getJoinId();
|
||||||
|
for (let radd = 0; radd < rowCount; radd++) {
|
||||||
|
const row = this.getRowAt(rowIndex + radd);
|
||||||
|
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
|
||||||
|
row.rowJoinIds[collectedRow.columnIndexes[i]] = joinId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
916
packages/datalib/src/PerspectiveTreeNode.ts
Normal file
916
packages/datalib/src/PerspectiveTreeNode.ts
Normal file
@@ -0,0 +1,916 @@
|
|||||||
|
import {
|
||||||
|
ColumnInfo,
|
||||||
|
DatabaseInfo,
|
||||||
|
ForeignKeyInfo,
|
||||||
|
NamedObjectInfo,
|
||||||
|
RangeDefinition,
|
||||||
|
TableInfo,
|
||||||
|
ViewInfo,
|
||||||
|
} from 'dbgate-types';
|
||||||
|
import {
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
MultipleDatabaseInfo,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveConfigColumns,
|
||||||
|
PerspectiveCustomJoinConfig,
|
||||||
|
PerspectiveFilterColumnInfo,
|
||||||
|
} from './PerspectiveConfig';
|
||||||
|
import _isEqual from 'lodash/isEqual';
|
||||||
|
import _cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import _compact from 'lodash/compact';
|
||||||
|
import _uniq from 'lodash/uniq';
|
||||||
|
import _flatten from 'lodash/flatten';
|
||||||
|
import _uniqBy from 'lodash/uniqBy';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import _cloneDeepWith from 'lodash/cloneDeepWith';
|
||||||
|
import {
|
||||||
|
PerspectiveDatabaseConfig,
|
||||||
|
PerspectiveDataLoadProps,
|
||||||
|
PerspectiveDataProvider,
|
||||||
|
} from './PerspectiveDataProvider';
|
||||||
|
import stableStringify from 'json-stable-stringify';
|
||||||
|
import { getFilterType, parseFilter } from 'dbgate-filterparser';
|
||||||
|
import { FilterType } from 'dbgate-filterparser/lib/types';
|
||||||
|
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||||
|
import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns';
|
||||||
|
|
||||||
|
export interface PerspectiveDataLoadPropsWithNode {
|
||||||
|
props: PerspectiveDataLoadProps;
|
||||||
|
node: PerspectiveTreeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function groupPerspectiveLoadProps(
|
||||||
|
// ...list: PerspectiveDataLoadPropsWithNode[]
|
||||||
|
// ): PerspectiveDataLoadPropsWithNode[] {
|
||||||
|
// const res: PerspectiveDataLoadPropsWithNode[] = [];
|
||||||
|
// for (const item of list) {
|
||||||
|
// const existing = res.find(
|
||||||
|
// x =>
|
||||||
|
// x.node == item.node &&
|
||||||
|
// x.props.schemaName == item.props.schemaName &&
|
||||||
|
// x.props.pureName == item.props.pureName &&
|
||||||
|
// _isEqual(x.props.bindingColumns, item.props.bindingColumns)
|
||||||
|
// );
|
||||||
|
// if (existing) {
|
||||||
|
// existing.props.bindingValues.push(...item.props.bindingValues);
|
||||||
|
// } else {
|
||||||
|
// res.push(_cloneDeep(item));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return res;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export abstract class PerspectiveTreeNode {
|
||||||
|
constructor(
|
||||||
|
public dbs: MultipleDatabaseInfo,
|
||||||
|
public config: PerspectiveConfig,
|
||||||
|
public setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
public parentNode: PerspectiveTreeNode,
|
||||||
|
public dataProvider: PerspectiveDataProvider,
|
||||||
|
public databaseConfig: PerspectiveDatabaseConfig
|
||||||
|
) {}
|
||||||
|
defaultChecked: boolean;
|
||||||
|
abstract get title();
|
||||||
|
abstract get codeName();
|
||||||
|
abstract get isExpandable();
|
||||||
|
abstract get childNodes(): PerspectiveTreeNode[];
|
||||||
|
abstract get icon(): string;
|
||||||
|
get fieldName() {
|
||||||
|
return this.codeName;
|
||||||
|
}
|
||||||
|
get headerTableAttributes() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get dataField() {
|
||||||
|
return this.codeName;
|
||||||
|
}
|
||||||
|
get tableCode() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get namedObject(): NamedObjectInfo {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps;
|
||||||
|
get isRoot() {
|
||||||
|
return this.parentNode == null;
|
||||||
|
}
|
||||||
|
get rootNode(): PerspectiveTreeNode {
|
||||||
|
if (this.isRoot) return this;
|
||||||
|
return this.parentNode?.rootNode;
|
||||||
|
}
|
||||||
|
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTableCode(code: string) {
|
||||||
|
return code == this.tableCode || this.parentNode?.hasTableCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
get uniqueName() {
|
||||||
|
if (this.parentNode) return `${this.parentNode.uniqueName}::${this.codeName}`;
|
||||||
|
return this.codeName;
|
||||||
|
}
|
||||||
|
get level() {
|
||||||
|
if (this.parentNode) return this.parentNode.level + 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
get isExpanded() {
|
||||||
|
return this.config.expandedColumns.includes(this.uniqueName);
|
||||||
|
}
|
||||||
|
get isChecked() {
|
||||||
|
if (this.config.checkedColumns.includes(this.uniqueName)) return true;
|
||||||
|
if (this.config.uncheckedColumns.includes(this.uniqueName)) return false;
|
||||||
|
return this.defaultChecked;
|
||||||
|
}
|
||||||
|
get columnTitle() {
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
get filterType(): FilterType {
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
get columnName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get customJoinConfig(): PerspectiveCustomJoinConfig {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get db(): DatabaseInfo {
|
||||||
|
return this.dbs?.[this.databaseConfig.conid]?.[this.databaseConfig.database];
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildMatchColumns() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentMatchColumns() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFilterCondition(source = null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get childDataColumn() {
|
||||||
|
if (!this.isExpandable && this.isChecked) {
|
||||||
|
return this.codeName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleExpanded(value?: boolean) {
|
||||||
|
this.includeInColumnSet('expandedColumns', this.uniqueName, value == null ? !this.isExpanded : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleChecked(value?: boolean) {
|
||||||
|
if (this.defaultChecked) {
|
||||||
|
this.includeInColumnSet('uncheckedColumns', this.uniqueName, value == null ? this.isChecked : value);
|
||||||
|
} else {
|
||||||
|
this.includeInColumnSet('checkedColumns', this.uniqueName, value == null ? !this.isChecked : value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
includeInColumnSet(field: keyof PerspectiveConfigColumns, uniqueName: string, isIncluded: boolean) {
|
||||||
|
if (isIncluded) {
|
||||||
|
this.setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
[field]: [...(cfg[field] || []), uniqueName],
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
[field]: (cfg[field] || []).filter(x => x != uniqueName),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilter() {
|
||||||
|
return this.config.filters[this.uniqueName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataLoadColumns() {
|
||||||
|
return _compact(
|
||||||
|
_uniq([
|
||||||
|
...this.childNodes.map(x => x.childDataColumn),
|
||||||
|
..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())),
|
||||||
|
...this.getParentMatchColumns(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildrenCondition(source = null): Condition {
|
||||||
|
const conditions = _compact([
|
||||||
|
...this.childNodes.map(x => x.parseFilterCondition(source)),
|
||||||
|
...this.buildParentFilterConditions(),
|
||||||
|
]);
|
||||||
|
if (conditions.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (conditions.length == 1) {
|
||||||
|
return conditions[0];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] {
|
||||||
|
const res = _compact(
|
||||||
|
this.childNodes.map(node => {
|
||||||
|
const sort = this.config?.sort?.[node?.parentNode?.uniqueName]?.find(x => x.uniqueName == node.uniqueName);
|
||||||
|
if (sort) {
|
||||||
|
return {
|
||||||
|
columnName: node.columnName,
|
||||||
|
order: sort.order,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return res.length > 0
|
||||||
|
? res
|
||||||
|
: (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [
|
||||||
|
{ columnName: table?.columns[0].columnName, order: 'ASC' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseTables() {
|
||||||
|
const res = [];
|
||||||
|
const table = this.getBaseTableFromThis();
|
||||||
|
if (table) res.push({ table, node: this });
|
||||||
|
for (const child of this.childNodes) {
|
||||||
|
if (!child.isChecked) continue;
|
||||||
|
res.push(...child.getBaseTables());
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
getBaseTableFromThis() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterInfo(): PerspectiveFilterColumnInfo {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
findChildNodeByUniquePath(uniquePath: string[]) {
|
||||||
|
if (uniquePath.length == 0) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const child = this.childNodes.find(x => x.codeName == uniquePath[0]);
|
||||||
|
return child?.findChildNodeByUniquePath(uniquePath.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
findNodeByUniqueName(uniqueName: string): PerspectiveTreeNode {
|
||||||
|
if (!uniqueName) return null;
|
||||||
|
const uniquePath = uniqueName.split('::');
|
||||||
|
if (uniquePath[0] != this.codeName) return null;
|
||||||
|
return this.findChildNodeByUniquePath(uniquePath.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
get supportsParentFilter() {
|
||||||
|
return (
|
||||||
|
(this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) &&
|
||||||
|
this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid &&
|
||||||
|
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isParentFilter() {
|
||||||
|
return !!(this.config.parentFilters || []).find(x => x.uniqueName == this.uniqueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParentFilterConditions(): Condition[] {
|
||||||
|
const leafNodes = _compact(
|
||||||
|
(this.config?.parentFilters || []).map(x => this.rootNode.findNodeByUniqueName(x.uniqueName))
|
||||||
|
);
|
||||||
|
const conditions: Condition[] = _compact(
|
||||||
|
leafNodes.map(leafNode => {
|
||||||
|
if (leafNode == this) return null;
|
||||||
|
const select: Select = {
|
||||||
|
commandType: 'select',
|
||||||
|
from: {
|
||||||
|
name: leafNode.namedObject,
|
||||||
|
alias: 'pert_0',
|
||||||
|
relations: [],
|
||||||
|
},
|
||||||
|
selectAll: true,
|
||||||
|
};
|
||||||
|
let lastNode = leafNode;
|
||||||
|
let node = leafNode;
|
||||||
|
let index = 1;
|
||||||
|
let lastAlias = 'pert_0';
|
||||||
|
while (node?.parentNode && node?.parentNode?.uniqueName != this?.uniqueName) {
|
||||||
|
node = node.parentNode;
|
||||||
|
let alias = `pert_${index}`;
|
||||||
|
select.from.relations.push({
|
||||||
|
joinType: 'INNER JOIN',
|
||||||
|
alias,
|
||||||
|
name: node.namedObject,
|
||||||
|
conditions: lastNode.getParentJoinCondition(lastAlias, alias),
|
||||||
|
});
|
||||||
|
lastAlias = alias;
|
||||||
|
lastNode = node;
|
||||||
|
}
|
||||||
|
if (node?.parentNode?.uniqueName != this?.uniqueName) return null;
|
||||||
|
select.where = {
|
||||||
|
conditionType: 'and',
|
||||||
|
conditions: _compact([
|
||||||
|
...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName),
|
||||||
|
leafNode.getChildrenCondition({ alias: 'pert_0' }),
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
conditionType: 'exists',
|
||||||
|
subQuery: select,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||||
|
foreignKey: ForeignKeyInfo;
|
||||||
|
refTable: TableInfo;
|
||||||
|
isView: boolean;
|
||||||
|
isTable: boolean;
|
||||||
|
constructor(
|
||||||
|
public column: ColumnInfo,
|
||||||
|
public table: TableInfo | ViewInfo,
|
||||||
|
dbs: MultipleDatabaseInfo,
|
||||||
|
config: PerspectiveConfig,
|
||||||
|
setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
dataProvider: PerspectiveDataProvider,
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
parentNode: PerspectiveTreeNode
|
||||||
|
) {
|
||||||
|
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||||
|
|
||||||
|
this.isTable = !!this.db?.tables?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
|
||||||
|
this.isView = !!this.db?.views?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
|
||||||
|
|
||||||
|
this.foreignKey = (table as TableInfo)?.foreignKeys?.find(
|
||||||
|
fk => fk.columns.length == 1 && fk.columns[0].columnName == column.columnName
|
||||||
|
);
|
||||||
|
|
||||||
|
this.refTable = this.db.tables.find(
|
||||||
|
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||||
|
if (!this.foreignKey) return false;
|
||||||
|
return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildMatchColumns() {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return [this.foreignKey.columns[0].columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentMatchColumns() {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return [this.foreignKey.columns[0].refColumnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return this.foreignKey.columns.map(column => {
|
||||||
|
const res: Condition = {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.columnName,
|
||||||
|
source: { alias: parentAlias },
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.refColumnName,
|
||||||
|
source: { alias },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||||
|
if (!this.foreignKey) return null;
|
||||||
|
return {
|
||||||
|
schemaName: this.foreignKey.refSchemaName,
|
||||||
|
pureName: this.foreignKey.refTableName,
|
||||||
|
bindingColumns: [this.foreignKey.columns[0].refColumnName],
|
||||||
|
bindingValues: _uniqBy(
|
||||||
|
parentRows.map(row => [row[this.foreignKey.columns[0].columnName]]),
|
||||||
|
stableStringify
|
||||||
|
),
|
||||||
|
dataColumns: this.getDataLoadColumns(),
|
||||||
|
databaseConfig: this.databaseConfig,
|
||||||
|
orderBy: this.getOrderBy(this.refTable),
|
||||||
|
condition: this.getChildrenCondition(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
if (this.isCircular) return 'img circular';
|
||||||
|
if (this.column.autoIncrement) return 'img autoincrement';
|
||||||
|
if (this.foreignKey) return 'img foreign-key';
|
||||||
|
return 'img column';
|
||||||
|
}
|
||||||
|
|
||||||
|
get codeName() {
|
||||||
|
return this.column.columnName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnName() {
|
||||||
|
return this.column.columnName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldName() {
|
||||||
|
return this.codeName + 'Ref';
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.column.columnName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpandable() {
|
||||||
|
return !!this.foreignKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterType(): FilterType {
|
||||||
|
return getFilterType(this.column.dataType);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCircular() {
|
||||||
|
return !!this.parentNode?.parentNode?.hasTableCode(this.tableCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
get childNodes(): PerspectiveTreeNode[] {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
const tbl = this?.db?.tables?.find(
|
||||||
|
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
|
||||||
|
);
|
||||||
|
|
||||||
|
return getTableChildPerspectiveNodes(
|
||||||
|
tbl,
|
||||||
|
this.dbs,
|
||||||
|
this.config,
|
||||||
|
this.setConfig,
|
||||||
|
this.dataProvider,
|
||||||
|
this.databaseConfig,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseTableFromThis() {
|
||||||
|
return this.refTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterInfo(): PerspectiveFilterColumnInfo {
|
||||||
|
return {
|
||||||
|
columnName: this.columnName,
|
||||||
|
filterType: this.filterType,
|
||||||
|
pureName: this.column.pureName,
|
||||||
|
schemaName: this.column.schemaName,
|
||||||
|
foreignKey: this.foreignKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFilterCondition(source = null): Condition {
|
||||||
|
const filter = this.getFilter();
|
||||||
|
if (!filter) return null;
|
||||||
|
const condition = parseFilter(filter, this.filterType);
|
||||||
|
if (!condition) return null;
|
||||||
|
return _cloneDeepWith(condition, (expr: Expression) => {
|
||||||
|
if (expr.exprType == 'placeholder') {
|
||||||
|
return {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: this.column.columnName,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerTableAttributes() {
|
||||||
|
if (this.foreignKey) {
|
||||||
|
return {
|
||||||
|
schemaName: this.foreignKey.refSchemaName,
|
||||||
|
pureName: this.foreignKey.refTableName,
|
||||||
|
conid: this.databaseConfig.conid,
|
||||||
|
database: this.databaseConfig.database,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tableCode() {
|
||||||
|
if (this.foreignKey) {
|
||||||
|
return `${this.foreignKey.refSchemaName}|${this.foreignKey.refTableName}`;
|
||||||
|
}
|
||||||
|
return `${this.table.schemaName}|${this.table.pureName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get namedObject(): NamedObjectInfo {
|
||||||
|
if (this.foreignKey) {
|
||||||
|
return {
|
||||||
|
schemaName: this.foreignKey.refSchemaName,
|
||||||
|
pureName: this.foreignKey.refTableName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||||
|
constructor(
|
||||||
|
public table: TableInfo | ViewInfo,
|
||||||
|
dbs: MultipleDatabaseInfo,
|
||||||
|
config: PerspectiveConfig,
|
||||||
|
setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
public dataProvider: PerspectiveDataProvider,
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
parentNode: PerspectiveTreeNode
|
||||||
|
) {
|
||||||
|
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||||
|
return {
|
||||||
|
schemaName: this.table.schemaName,
|
||||||
|
pureName: this.table.pureName,
|
||||||
|
dataColumns: this.getDataLoadColumns(),
|
||||||
|
databaseConfig: this.databaseConfig,
|
||||||
|
orderBy: this.getOrderBy(this.table),
|
||||||
|
condition: this.getChildrenCondition(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get codeName() {
|
||||||
|
return this.table.schemaName ? `${this.table.schemaName}:${this.table.pureName}` : this.table.pureName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.table.pureName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpandable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get childNodes(): PerspectiveTreeNode[] {
|
||||||
|
return getTableChildPerspectiveNodes(
|
||||||
|
this.table,
|
||||||
|
this.dbs,
|
||||||
|
this.config,
|
||||||
|
this.setConfig,
|
||||||
|
this.dataProvider,
|
||||||
|
this.databaseConfig,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return 'img table';
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseTableFromThis() {
|
||||||
|
return this.table;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerTableAttributes() {
|
||||||
|
return {
|
||||||
|
schemaName: this.table.schemaName,
|
||||||
|
pureName: this.table.pureName,
|
||||||
|
conid: this.databaseConfig.conid,
|
||||||
|
database: this.databaseConfig.database,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get tableCode() {
|
||||||
|
return `${this.table.schemaName}|${this.table.pureName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get namedObject(): NamedObjectInfo {
|
||||||
|
return {
|
||||||
|
schemaName: this.table.schemaName,
|
||||||
|
pureName: this.table.pureName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export class PerspectiveViewNode extends PerspectiveTreeNode {
|
||||||
|
// constructor(
|
||||||
|
// public view: ViewInfo,
|
||||||
|
// dbs: MultipleDatabaseInfo,
|
||||||
|
// config: PerspectiveConfig,
|
||||||
|
// setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
// public dataProvider: PerspectiveDataProvider,
|
||||||
|
// databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
// parentNode: PerspectiveTreeNode
|
||||||
|
// ) {
|
||||||
|
// super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||||
|
// return {
|
||||||
|
// schemaName: this.view.schemaName,
|
||||||
|
// pureName: this.view.pureName,
|
||||||
|
// dataColumns: this.getDataLoadColumns(),
|
||||||
|
// databaseConfig: this.databaseConfig,
|
||||||
|
// orderBy: this.getOrderBy(this.view),
|
||||||
|
// condition: this.getChildrenCondition(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get codeName() {
|
||||||
|
// return this.view.schemaName ? `${this.view.schemaName}:${this.view.pureName}` : this.view.pureName;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get title() {
|
||||||
|
// return this.view.pureName;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get isExpandable() {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get childNodes(): PerspectiveTreeNode[] {
|
||||||
|
// return getTableChildPerspectiveNodes(
|
||||||
|
// this.view,
|
||||||
|
// this.dbs,
|
||||||
|
// this.config,
|
||||||
|
// this.setConfig,
|
||||||
|
// this.dataProvider,
|
||||||
|
// this.databaseConfig,
|
||||||
|
// this
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get icon() {
|
||||||
|
// return 'img table';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getBaseTableFromThis() {
|
||||||
|
// return this.view;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
|
||||||
|
constructor(
|
||||||
|
public foreignKey: ForeignKeyInfo,
|
||||||
|
table: TableInfo,
|
||||||
|
dbs: MultipleDatabaseInfo,
|
||||||
|
config: PerspectiveConfig,
|
||||||
|
setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
public dataProvider: PerspectiveDataProvider,
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
public isMultiple: boolean,
|
||||||
|
parentNode: PerspectiveTreeNode
|
||||||
|
) {
|
||||||
|
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||||
|
if (!this.foreignKey) return false;
|
||||||
|
return parentRow[this.foreignKey.columns[0].refColumnName] == childRow[this.foreignKey.columns[0].columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildMatchColumns() {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return [this.foreignKey.columns[0].refColumnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentMatchColumns() {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return [this.foreignKey.columns[0].columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||||
|
if (!this.foreignKey) return null;
|
||||||
|
return {
|
||||||
|
schemaName: this.table.schemaName,
|
||||||
|
pureName: this.table.pureName,
|
||||||
|
bindingColumns: [this.foreignKey.columns[0].columnName],
|
||||||
|
bindingValues: _uniqBy(
|
||||||
|
parentRows.map(row => [row[this.foreignKey.columns[0].refColumnName]]),
|
||||||
|
stableStringify
|
||||||
|
),
|
||||||
|
dataColumns: this.getDataLoadColumns(),
|
||||||
|
databaseConfig: this.databaseConfig,
|
||||||
|
orderBy: this.getOrderBy(this.table),
|
||||||
|
condition: this.getChildrenCondition(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get columnTitle() {
|
||||||
|
return this.table.pureName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
if (this.isMultiple) {
|
||||||
|
return `${super.title} (${this.foreignKey.columns.map(x => x.columnName).join(', ')})`;
|
||||||
|
}
|
||||||
|
return super.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get codeName() {
|
||||||
|
if (this.isMultiple) {
|
||||||
|
return `${super.codeName}-${this.foreignKey.columns.map(x => x.columnName).join('_')}`;
|
||||||
|
}
|
||||||
|
return super.codeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||||
|
if (!this.foreignKey) return [];
|
||||||
|
return this.foreignKey.columns.map(column => {
|
||||||
|
const res: Condition = {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.refColumnName,
|
||||||
|
source: { alias: parentAlias },
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.columnName,
|
||||||
|
source: { alias },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
|
||||||
|
constructor(
|
||||||
|
public customJoin: PerspectiveCustomJoinConfig,
|
||||||
|
table: TableInfo | ViewInfo,
|
||||||
|
dbs: MultipleDatabaseInfo,
|
||||||
|
config: PerspectiveConfig,
|
||||||
|
setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
public dataProvider: PerspectiveDataProvider,
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
parentNode: PerspectiveTreeNode
|
||||||
|
) {
|
||||||
|
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||||
|
for (const column of this.customJoin.columns) {
|
||||||
|
if (parentRow[column.baseColumnName] != childRow[column.refColumnName]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildMatchColumns() {
|
||||||
|
return this.customJoin.columns.map(x => x.baseColumnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentMatchColumns() {
|
||||||
|
return this.customJoin.columns.map(x => x.refColumnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||||
|
// console.log('CUSTOM JOIN', this.customJoin);
|
||||||
|
// console.log('this.getDataLoadColumns()', this.getDataLoadColumns());
|
||||||
|
return {
|
||||||
|
schemaName: this.table.schemaName,
|
||||||
|
pureName: this.table.pureName,
|
||||||
|
bindingColumns: this.getParentMatchColumns(),
|
||||||
|
bindingValues: _uniqBy(
|
||||||
|
parentRows.map(row => this.customJoin.columns.map(x => row[x.baseColumnName])),
|
||||||
|
stableStringify
|
||||||
|
),
|
||||||
|
dataColumns: this.getDataLoadColumns(),
|
||||||
|
databaseConfig: this.databaseConfig,
|
||||||
|
orderBy: this.getOrderBy(this.table),
|
||||||
|
condition: this.getChildrenCondition(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.customJoin.joinName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get icon() {
|
||||||
|
return 'icon custom-join';
|
||||||
|
}
|
||||||
|
|
||||||
|
get codeName() {
|
||||||
|
return this.customJoin.joinid;
|
||||||
|
}
|
||||||
|
|
||||||
|
get customJoinConfig(): PerspectiveCustomJoinConfig {
|
||||||
|
return this.customJoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||||
|
return this.customJoin.columns.map(column => {
|
||||||
|
const res: Condition = {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator: '=',
|
||||||
|
left: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.baseColumnName,
|
||||||
|
source: { alias: parentAlias },
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'column',
|
||||||
|
columnName: column.refColumnName,
|
||||||
|
source: { alias },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTableChildPerspectiveNodes(
|
||||||
|
table: TableInfo | ViewInfo,
|
||||||
|
dbs: MultipleDatabaseInfo,
|
||||||
|
config: PerspectiveConfig,
|
||||||
|
setConfig: ChangePerspectiveConfigFunc,
|
||||||
|
dataProvider: PerspectiveDataProvider,
|
||||||
|
databaseConfig: PerspectiveDatabaseConfig,
|
||||||
|
parentColumn: PerspectiveTreeNode
|
||||||
|
) {
|
||||||
|
if (!table) return [];
|
||||||
|
const db = parentColumn.db;
|
||||||
|
|
||||||
|
const columnNodes = table.columns.map(
|
||||||
|
col =>
|
||||||
|
new PerspectiveTableColumnNode(col, table, dbs, config, setConfig, dataProvider, databaseConfig, parentColumn)
|
||||||
|
);
|
||||||
|
const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName);
|
||||||
|
const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns);
|
||||||
|
for (const node of columnNodes) {
|
||||||
|
node.defaultChecked = defaultColumns.includes(node.columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = [];
|
||||||
|
res.push(...columnNodes);
|
||||||
|
const dependencies = [];
|
||||||
|
if (db && (table as TableInfo)?.dependencies) {
|
||||||
|
for (const fk of (table as TableInfo)?.dependencies) {
|
||||||
|
const tbl = db.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
|
||||||
|
if (tbl) {
|
||||||
|
const isMultiple =
|
||||||
|
(table as TableInfo)?.dependencies.filter(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName)
|
||||||
|
.length >= 2;
|
||||||
|
dependencies.push(
|
||||||
|
new PerspectiveTableReferenceNode(
|
||||||
|
fk,
|
||||||
|
tbl,
|
||||||
|
dbs,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
dataProvider,
|
||||||
|
databaseConfig,
|
||||||
|
isMultiple,
|
||||||
|
parentColumn
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.push(..._sortBy(dependencies, 'title'));
|
||||||
|
|
||||||
|
const customs = [];
|
||||||
|
for (const join of config.customJoins || []) {
|
||||||
|
if (join.baseUniqueName == parentColumn.uniqueName) {
|
||||||
|
const newConfig = { ...databaseConfig };
|
||||||
|
if (join.conid) newConfig.conid = join.conid;
|
||||||
|
if (join.database) newConfig.database = join.database;
|
||||||
|
const db = dbs?.[newConfig.conid]?.[newConfig.database];
|
||||||
|
const table = db?.tables?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||||
|
const view = db?.views?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||||
|
|
||||||
|
if (table || view) {
|
||||||
|
customs.push(
|
||||||
|
new PerspectiveCustomJoinTreeNode(
|
||||||
|
join,
|
||||||
|
table || view,
|
||||||
|
dbs,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
dataProvider,
|
||||||
|
newConfig,
|
||||||
|
parentColumn
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.push(..._sortBy(customs, 'title'));
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
33
packages/datalib/src/getPerspectiveDefaultColumns.ts
Normal file
33
packages/datalib/src/getPerspectiveDefaultColumns.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||||
|
import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
|
||||||
|
|
||||||
|
export function getPerspectiveDefaultColumns(
|
||||||
|
table: TableInfo | ViewInfo,
|
||||||
|
db: DatabaseInfo,
|
||||||
|
circularColumns: string[]
|
||||||
|
): string[] {
|
||||||
|
const columns = table.columns.map(x => x.columnName);
|
||||||
|
const predicates = [
|
||||||
|
x => x.toLowerCase() == 'name',
|
||||||
|
x => x.toLowerCase() == 'title',
|
||||||
|
x => x.toLowerCase().includes('name'),
|
||||||
|
x => x.toLowerCase().includes('title'),
|
||||||
|
x => x.toLowerCase().includes('subject'),
|
||||||
|
// x => x.toLowerCase().includes('text'),
|
||||||
|
// x => x.toLowerCase().includes('desc'),
|
||||||
|
x =>
|
||||||
|
table.columns
|
||||||
|
.find(y => y.columnName == x)
|
||||||
|
?.dataType?.toLowerCase()
|
||||||
|
?.includes('char'),
|
||||||
|
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1 && !circularColumns.includes(x),
|
||||||
|
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const predicate of predicates) {
|
||||||
|
const col = columns.find(predicate);
|
||||||
|
if (col) return [col];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [columns[0]];
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './GridDisplay';
|
export * from './GridDisplay';
|
||||||
export * from './GridConfig';
|
export * from './GridConfig';
|
||||||
|
export * from './PerspectiveConfig';
|
||||||
|
export * from './PerspectiveTreeNode';
|
||||||
export * from './TableGridDisplay';
|
export * from './TableGridDisplay';
|
||||||
export * from './ViewGridDisplay';
|
export * from './ViewGridDisplay';
|
||||||
export * from './JslGridDisplay';
|
export * from './JslGridDisplay';
|
||||||
@@ -12,3 +14,7 @@ export * from './FormViewDisplay';
|
|||||||
export * from './TableFormViewDisplay';
|
export * from './TableFormViewDisplay';
|
||||||
export * from './CollectionGridDisplay';
|
export * from './CollectionGridDisplay';
|
||||||
export * from './deleteCascade';
|
export * from './deleteCascade';
|
||||||
|
export * from './PerspectiveDisplay';
|
||||||
|
export * from './PerspectiveDataProvider';
|
||||||
|
export * from './PerspectiveCache';
|
||||||
|
export * from './PerspectiveConfig';
|
||||||
|
|||||||
122
packages/datalib/src/tests/PerspectiveDisplay.test.ts
Normal file
122
packages/datalib/src/tests/PerspectiveDisplay.test.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { TableInfo } from 'dbgate-types';
|
||||||
|
import { PerspectiveDisplay } from '../PerspectiveDisplay';
|
||||||
|
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
|
||||||
|
import { chinookDbInfo } from './chinookDbInfo';
|
||||||
|
import { createPerspectiveConfig } from '../PerspectiveConfig';
|
||||||
|
import artistDataFlat from './artistDataFlat';
|
||||||
|
import artistDataAlbum from './artistDataAlbum';
|
||||||
|
import artistDataAlbumTrack from './artistDataAlbumTrack';
|
||||||
|
|
||||||
|
test('test flat view', () => {
|
||||||
|
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||||
|
const root = new PerspectiveTableNode(
|
||||||
|
artistTable,
|
||||||
|
{ conid: { db: chinookDbInfo } },
|
||||||
|
createPerspectiveConfig({ pureName: 'Artist' }),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ conid: 'conid', database: 'db' },
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const display = new PerspectiveDisplay(root, artistDataFlat);
|
||||||
|
|
||||||
|
// console.log(display.loadIndicatorsCounts);
|
||||||
|
// console.log(display.rows);
|
||||||
|
expect(display.rows.length).toEqual(4);
|
||||||
|
expect(display.rows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: ['AC/DC'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.loadIndicatorsCounts).toEqual({
|
||||||
|
Artist: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test one level nesting', () => {
|
||||||
|
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||||
|
const root = new PerspectiveTableNode(
|
||||||
|
artistTable,
|
||||||
|
{ conid: { db: chinookDbInfo } },
|
||||||
|
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album'] },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ conid: 'conid', database: 'db' },
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const display = new PerspectiveDisplay(root, artistDataAlbum);
|
||||||
|
|
||||||
|
console.log(display.loadIndicatorsCounts);
|
||||||
|
// console.log(display.rows);
|
||||||
|
expect(display.rows.length).toEqual(6);
|
||||||
|
expect(display.rows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: ['AC/DC', 'For Those About To Rock We Salute You'],
|
||||||
|
rowSpans: [2, 1],
|
||||||
|
rowCellSkips: [false, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.rows[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: [undefined, 'Let There Be Rock'],
|
||||||
|
rowSpans: [1, 1],
|
||||||
|
rowCellSkips: [true, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.rows[2]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: ['Accept', 'Balls to the Wall'],
|
||||||
|
rowSpans: [2, 1],
|
||||||
|
rowCellSkips: [false, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.rows[5]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: ['Alanis Morissette', 'Jagged Little Pill'],
|
||||||
|
rowSpans: [1, 1],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(display.loadIndicatorsCounts).toEqual({
|
||||||
|
Artist: 6,
|
||||||
|
'Artist.Album': 6,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test two level nesting', () => {
|
||||||
|
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||||
|
const root = new PerspectiveTableNode(
|
||||||
|
artistTable,
|
||||||
|
{ conid: { db: chinookDbInfo } },
|
||||||
|
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album', 'Artist::Album::Track'] },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ conid: 'conid', database: 'db' },
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const display = new PerspectiveDisplay(root, artistDataAlbumTrack);
|
||||||
|
|
||||||
|
console.log(display.rows);
|
||||||
|
expect(display.rows.length).toEqual(8);
|
||||||
|
expect(display.rows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: ['AC/DC', 'For Those About To Rock We Salute You', 'For Those About To Rock (We Salute You)'],
|
||||||
|
rowSpans: [4, 2, 1],
|
||||||
|
rowCellSkips: [false, false, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.rows[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: [undefined, undefined, 'Put The Finger On You'],
|
||||||
|
rowSpans: [1, 1, 1],
|
||||||
|
rowCellSkips: [true, true, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(display.rows[2]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
rowData: [undefined, 'Let There Be Rock', 'Go Down'],
|
||||||
|
rowSpans: [1, 2, 1],
|
||||||
|
rowCellSkips: [true, false, false],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
56
packages/datalib/src/tests/artistDataAlbum.ts
Normal file
56
packages/datalib/src/tests/artistDataAlbum.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
ArtistId: 1,
|
||||||
|
Name: 'AC/DC',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'For Those About To Rock We Salute You',
|
||||||
|
ArtistId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: 'Let There Be Rock',
|
||||||
|
ArtistId: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 2,
|
||||||
|
Name: 'Accept',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'Balls to the Wall',
|
||||||
|
ArtistId: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: 'Restless and Wild',
|
||||||
|
ArtistId: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 3,
|
||||||
|
Name: 'Aerosmith',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'Big Ones',
|
||||||
|
ArtistId: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 4,
|
||||||
|
Name: 'Alanis Morissette',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'Jagged Little Pill',
|
||||||
|
ArtistId: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incompleteRowsIndicator: ['Artist.Album'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incompleteRowsIndicator: ['Artist'],
|
||||||
|
},
|
||||||
|
];
|
||||||
78
packages/datalib/src/tests/artistDataAlbumTrack.ts
Normal file
78
packages/datalib/src/tests/artistDataAlbumTrack.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
ArtistId: 1,
|
||||||
|
Name: 'AC/DC',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'For Those About To Rock We Salute You',
|
||||||
|
AlbumId: 1,
|
||||||
|
ArtistId: 1,
|
||||||
|
Track: [
|
||||||
|
{
|
||||||
|
Name: 'For Those About To Rock (We Salute You)',
|
||||||
|
AlbumId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'Put The Finger On You',
|
||||||
|
AlbumId: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: 'Let There Be Rock',
|
||||||
|
AlbumId: 4,
|
||||||
|
ArtistId: 1,
|
||||||
|
Track: [
|
||||||
|
{
|
||||||
|
Name: 'Go Down',
|
||||||
|
AlbumId: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'Dog Eat Dog',
|
||||||
|
AlbumId: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 2,
|
||||||
|
Name: 'Accept',
|
||||||
|
Album: [
|
||||||
|
{
|
||||||
|
Title: 'Balls to the Wall',
|
||||||
|
AlbumId: 2,
|
||||||
|
ArtistId: 2,
|
||||||
|
Track: [
|
||||||
|
{
|
||||||
|
Name: 'Balls to the Wall',
|
||||||
|
AlbumId: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: 'Restless and Wild',
|
||||||
|
AlbumId: 3,
|
||||||
|
ArtistId: 2,
|
||||||
|
Track: [
|
||||||
|
{
|
||||||
|
Name: 'Fast As a Shark',
|
||||||
|
AlbumId: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'Restless and Wild',
|
||||||
|
AlbumId: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'Princess of the Dawn',
|
||||||
|
AlbumId: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
incompleteRowsIndicator: ['Artist'],
|
||||||
|
},
|
||||||
|
];
|
||||||
21
packages/datalib/src/tests/artistDataFlat.ts
Normal file
21
packages/datalib/src/tests/artistDataFlat.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
ArtistId: 1,
|
||||||
|
Name: 'AC/DC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 2,
|
||||||
|
Name: 'Accept',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 3,
|
||||||
|
Name: 'Aerosmith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ArtistId: 4,
|
||||||
|
Name: 'Alanis Morissette',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
incompleteRowsIndicator: ['Artist'],
|
||||||
|
},
|
||||||
|
];
|
||||||
1777
packages/datalib/src/tests/chinookDbInfo.ts
Normal file
1777
packages/datalib/src/tests/chinookDbInfo.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,8 @@
|
|||||||
"dbgate-types": "^5.0.0-alpha.1",
|
"dbgate-types": "^5.0.0-alpha.1",
|
||||||
"@types/jest": "^25.1.4",
|
"@types/jest": "^25.1.4",
|
||||||
"@types/node": "^13.7.0",
|
"@types/node": "^13.7.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^28.1.3",
|
||||||
"ts-jest": "^25.2.1",
|
"ts-jest": "^28.0.7",
|
||||||
"typescript": "^4.4.3"
|
"typescript": "^4.4.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import moment from 'moment';
|
|||||||
|
|
||||||
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
|
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
|
||||||
|
|
||||||
export function getFilterValueExpression(value, dataType) {
|
export function getFilterValueExpression(value, dataType?) {
|
||||||
if (value == null) return 'NULL';
|
if (value == null) return 'NULL';
|
||||||
if (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
if (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
if (value === true) return 'TRUE';
|
if (value === true) return 'TRUE';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseFilter } from './parseFilter';
|
const { parseFilter } = require('./parseFilter');
|
||||||
|
|
||||||
test('parse string', () => {
|
test('parse string', () => {
|
||||||
const ast = parseFilter('"123"', 'string');
|
const ast = parseFilter('"123"', 'string');
|
||||||
|
|||||||
@@ -68,5 +68,9 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
|||||||
dmp.put(' ^and ');
|
dmp.put(' ^and ');
|
||||||
dumpSqlExpression(dmp, condition.right);
|
dumpSqlExpression(dmp, condition.right);
|
||||||
break;
|
break;
|
||||||
|
case 'in':
|
||||||
|
dumpSqlExpression(dmp, condition.expr);
|
||||||
|
dmp.put(' ^in (%,v)', condition.values);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export interface BetweenCondition {
|
|||||||
right: Expression;
|
right: Expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InCondition {
|
||||||
|
conditionType: 'in';
|
||||||
|
expr: Expression;
|
||||||
|
values: any[];
|
||||||
|
}
|
||||||
|
|
||||||
export type Condition =
|
export type Condition =
|
||||||
| BinaryCondition
|
| BinaryCondition
|
||||||
| NotCondition
|
| NotCondition
|
||||||
@@ -107,7 +113,8 @@ export type Condition =
|
|||||||
| LikeCondition
|
| LikeCondition
|
||||||
| ExistsCondition
|
| ExistsCondition
|
||||||
| NotExistsCondition
|
| NotExistsCondition
|
||||||
| BetweenCondition;
|
| BetweenCondition
|
||||||
|
| InCondition;
|
||||||
|
|
||||||
export interface Source {
|
export interface Source {
|
||||||
name?: NamedObjectInfo;
|
name?: NamedObjectInfo;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dbgate-query-splitter": "^4.9.0",
|
"dbgate-query-splitter": "^4.9.0",
|
||||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"json-stable-stringify": "^1.0.1",
|
"json-stable-stringify": "^1.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"uuid": "^3.4.0"
|
"uuid": "^3.4.0"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import _isString from 'lodash/isString';
|
||||||
import { ColumnInfo, ColumnReference, DatabaseInfo, DatabaseInfoObjects, SqlDialect, TableInfo } from 'dbgate-types';
|
import { ColumnInfo, ColumnReference, DatabaseInfo, DatabaseInfoObjects, SqlDialect, TableInfo } from 'dbgate-types';
|
||||||
|
|
||||||
export function fullNameFromString(name) {
|
export function fullNameFromString(name) {
|
||||||
@@ -54,7 +55,10 @@ export function findObjectLike(
|
|||||||
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
|
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo | string) {
|
||||||
|
if (_isString(column)) {
|
||||||
|
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column));
|
||||||
|
}
|
||||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ function columnsConstraintName(prefix: string, table: TableInfo, columns: Column
|
|||||||
|
|
||||||
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||||
if (!table) return table;
|
if (!table) return table;
|
||||||
const res = _.cloneDeep(table);
|
const res = _cloneDeep(table);
|
||||||
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
|
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
|
||||||
res.primaryKey.constraintName = `PK_${res.pureName}`;
|
res.primaryKey.constraintName = `PK_${res.pureName}`;
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/types/dbinfo.d.ts
vendored
11
packages/types/dbinfo.d.ts
vendored
@@ -1,6 +1,8 @@
|
|||||||
export interface NamedObjectInfo {
|
export interface NamedObjectInfo {
|
||||||
pureName: string;
|
pureName: string;
|
||||||
schemaName?: string;
|
schemaName?: string;
|
||||||
|
contentHash?: string;
|
||||||
|
engine?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnReference {
|
export interface ColumnReference {
|
||||||
@@ -31,7 +33,8 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
|||||||
|
|
||||||
export interface IndexInfo extends ColumnsConstraintInfo {
|
export interface IndexInfo extends ColumnsConstraintInfo {
|
||||||
isUnique: boolean;
|
isUnique: boolean;
|
||||||
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
// indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||||
|
indexType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||||
@@ -43,8 +46,8 @@ export interface CheckInfo extends ConstraintInfo {
|
|||||||
export interface ColumnInfo extends NamedObjectInfo {
|
export interface ColumnInfo extends NamedObjectInfo {
|
||||||
pairingId?: string;
|
pairingId?: string;
|
||||||
columnName: string;
|
columnName: string;
|
||||||
notNull: boolean;
|
notNull?: boolean;
|
||||||
autoIncrement: boolean;
|
autoIncrement?: boolean;
|
||||||
dataType: string;
|
dataType: string;
|
||||||
precision?: number;
|
precision?: number;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
@@ -119,7 +122,7 @@ export interface DatabaseInfoObjects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseInfo extends DatabaseInfoObjects {
|
export interface DatabaseInfo extends DatabaseInfoObjects {
|
||||||
schemas: SchemaInfo[];
|
schemas?: SchemaInfo[];
|
||||||
engine?: string;
|
engine?: string;
|
||||||
defaultSchema?: string;
|
defaultSchema?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chartjs-plugin-zoom": "^1.2.0",
|
"chartjs-plugin-zoom": "^1.2.0",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"interval-operations": "^1.0.7",
|
"interval-operations": "^1.0.7",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"wellknown": "^0.5.0"
|
"wellknown": "^0.5.0"
|
||||||
|
|||||||
@@ -147,11 +147,11 @@ import { tick } from 'svelte';
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCurrentSettings()['defaultAction.connectionClick'] == 'connect') {
|
if (getCurrentSettings()['defaultAction.connectionClick'] == 'openDetails') {
|
||||||
|
handleOpenConnectionTab();
|
||||||
|
} else {
|
||||||
await tick();
|
await tick();
|
||||||
handleConnect();
|
handleConnect();
|
||||||
} else {
|
|
||||||
handleOpenConnectionTab();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,15 @@
|
|||||||
tab: 'TableStructureTab',
|
tab: 'TableStructureTab',
|
||||||
icon: 'img table-structure',
|
icon: 'img table-structure',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Open perspective',
|
||||||
|
tab: 'PerspectiveTab',
|
||||||
|
forceNewTab: true,
|
||||||
|
icon: 'img perspective',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Drop table',
|
label: 'Drop table',
|
||||||
isDrop: true,
|
isDrop: true,
|
||||||
@@ -133,6 +142,12 @@
|
|||||||
tab: 'TableStructureTab',
|
tab: 'TableStructureTab',
|
||||||
icon: 'img view-structure',
|
icon: 'img view-structure',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Open perspective',
|
||||||
|
tab: 'PerspectiveTab',
|
||||||
|
forceNewTab: true,
|
||||||
|
icon: 'img perspective',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Drop view',
|
label: 'Drop view',
|
||||||
isDrop: true,
|
isDrop: true,
|
||||||
|
|||||||
@@ -65,6 +65,14 @@
|
|||||||
currentConnection: true,
|
currentConnection: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const perspectives: FileTypeHandler = {
|
||||||
|
icon: 'img perspective',
|
||||||
|
format: 'json',
|
||||||
|
tabComponent: 'PerspectiveTab',
|
||||||
|
folder: 'pesrpectives',
|
||||||
|
currentConnection: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const SAVED_FILE_HANDLERS = {
|
export const SAVED_FILE_HANDLERS = {
|
||||||
sql,
|
sql,
|
||||||
shell,
|
shell,
|
||||||
@@ -73,10 +81,14 @@
|
|||||||
query,
|
query,
|
||||||
sqlite,
|
sqlite,
|
||||||
diagrams,
|
diagrams,
|
||||||
|
perspectives,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractKey = data => data.file;
|
export const extractKey = data => data.file;
|
||||||
export const createMatcher = ({ file }) => filter => filterName(filter, file);
|
export const createMatcher =
|
||||||
|
({ file }) =>
|
||||||
|
filter =>
|
||||||
|
filterName(filter, file);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
99
packages/web/src/datagrid/CellValue.svelte
Normal file
99
packages/web/src/datagrid/CellValue.svelte
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script context="module">
|
||||||
|
function makeBulletString(value) {
|
||||||
|
return _.pad('', value.length, '•');
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightSpecialCharacters(value) {
|
||||||
|
value = value.replace(/\n/g, '↲');
|
||||||
|
value = value.replace(/\r/g, '');
|
||||||
|
value = value.replace(/^(\s+)/, makeBulletString);
|
||||||
|
value = value.replace(/(\s+)$/, makeBulletString);
|
||||||
|
value = value.replace(/(\s\s+)/g, makeBulletString);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||||
|
const dateTimeRegex =
|
||||||
|
/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
if (value >= 10000 || value <= -10000) {
|
||||||
|
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
|
||||||
|
return value.toLocaleString();
|
||||||
|
} else {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(testedString) {
|
||||||
|
const m = testedString.match(dateTimeRegex);
|
||||||
|
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||||
|
import { arrayToHexString } from 'dbgate-tools';
|
||||||
|
|
||||||
|
export let rowData;
|
||||||
|
export let value;
|
||||||
|
export let jsonParsedValue = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if rowData == null}
|
||||||
|
<span class="null">(No row)</span>
|
||||||
|
{:else if value === null}
|
||||||
|
<span class="null">(NULL)</span>
|
||||||
|
{:else if value === undefined}
|
||||||
|
<span class="null">(No field)</span>
|
||||||
|
{:else if _.isDate(value)}
|
||||||
|
{value.toString()}
|
||||||
|
{:else if value === true}
|
||||||
|
<span class="value">true</span>
|
||||||
|
{:else if value === false}
|
||||||
|
<span class="value">false</span>
|
||||||
|
{:else if _.isNumber(value)}
|
||||||
|
<span class="value">{formatNumber(value)}</span>
|
||||||
|
{:else if _.isString(value) && !jsonParsedValue}
|
||||||
|
{#if dateTimeRegex.test(value)}
|
||||||
|
<span class="value">
|
||||||
|
{formatDateTime(value)}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
{highlightSpecialCharacters(value)}
|
||||||
|
{/if}
|
||||||
|
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
|
||||||
|
{#if value.data.length <= 16}
|
||||||
|
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="null">({value.data.length} bytes)</span>
|
||||||
|
{/if}
|
||||||
|
{:else if value.$oid}
|
||||||
|
<span class="value">ObjectId("{value.$oid}")</span>
|
||||||
|
{:else if _.isPlainObject(value)}
|
||||||
|
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
||||||
|
{:else if _.isArray(value)}
|
||||||
|
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
|
||||||
|
{:else if _.isPlainObject(jsonParsedValue)}
|
||||||
|
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
|
||||||
|
{:else if _.isArray(jsonParsedValue)}
|
||||||
|
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
|
||||||
|
>[{jsonParsedValue.length} items]</span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
{value.toString()}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.null {
|
||||||
|
color: var(--theme-font-3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
color: var(--theme-icon-green);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -105,19 +105,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { changeSetToSql, createChangeSet } from 'dbgate-datalib';
|
|
||||||
import { parseFilter } from 'dbgate-filterparser';
|
import { parseFilter } from 'dbgate-filterparser';
|
||||||
import { scriptToSql } from 'dbgate-sqltree';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||||
import registerCommand from '../commands/registerCommand';
|
import registerCommand from '../commands/registerCommand';
|
||||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
|
||||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
|
||||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
|
||||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import { extensions } from '../stores';
|
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
import { registerMenu } from '../utility/contextMenu';
|
import { registerMenu } from '../utility/contextMenu';
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
export let filter;
|
export let filter;
|
||||||
export let setFilter;
|
export let setFilter;
|
||||||
export let showResizeSplitter = false;
|
export let showResizeSplitter = false;
|
||||||
export let onFocusGrid;
|
export let onFocusGrid = null;
|
||||||
export let onGetReference;
|
export let onGetReference = null;
|
||||||
export let foreignKey = null;
|
export let foreignKey = null;
|
||||||
export let conid = null;
|
export let conid = null;
|
||||||
export let database = null;
|
export let database = null;
|
||||||
export let driver = null;
|
export let driver = null;
|
||||||
export let jslid = null;
|
export let jslid = null;
|
||||||
|
export let customCommandIcon = null;
|
||||||
|
export let onCustomCommand = null;
|
||||||
|
export let customCommandTooltip = null;
|
||||||
|
|
||||||
export let pureName = null;
|
export let pureName = null;
|
||||||
export let schemaName = null;
|
export let schemaName = null;
|
||||||
@@ -295,6 +298,11 @@
|
|||||||
class:isOk
|
class:isOk
|
||||||
placeholder="Filter"
|
placeholder="Filter"
|
||||||
/>
|
/>
|
||||||
|
{#if customCommandIcon && onCustomCommand}
|
||||||
|
<InlineButton on:click={onCustomCommand} title={customCommandTooltip} narrow square>
|
||||||
|
<FontIcon icon={customCommandIcon} />
|
||||||
|
</InlineButton>
|
||||||
|
{/if}
|
||||||
{#if conid && database && driver}
|
{#if conid && database && driver}
|
||||||
{#if driver?.databaseEngineTypes?.includes('sql') && foreignKey}
|
{#if driver?.databaseEngineTypes?.includes('sql') && foreignKey}
|
||||||
<InlineButton on:click={handleShowDictionary} narrow square>
|
<InlineButton on:click={handleShowDictionary} narrow square>
|
||||||
@@ -320,6 +328,7 @@
|
|||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.isError {
|
input.isError {
|
||||||
|
|||||||
@@ -1,47 +1,10 @@
|
|||||||
<script context="module">
|
|
||||||
function makeBulletString(value) {
|
|
||||||
return _.pad('', value.length, '•');
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlightSpecialCharacters(value) {
|
|
||||||
value = value.replace(/\n/g, '↲');
|
|
||||||
value = value.replace(/\r/g, '');
|
|
||||||
value = value.replace(/^(\s+)/, makeBulletString);
|
|
||||||
value = value.replace(/(\s+)$/, makeBulletString);
|
|
||||||
value = value.replace(/(\s\s+)/g, makeBulletString);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
|
||||||
const dateTimeRegex = /^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
|
|
||||||
|
|
||||||
function formatNumber(value) {
|
|
||||||
if (value >= 10000 || value <= -10000) {
|
|
||||||
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
|
|
||||||
return value.toLocaleString();
|
|
||||||
} else {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateTime(testedString) {
|
|
||||||
const m = testedString.match(dateTimeRegex);
|
|
||||||
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import _, { isPlainObject, join } from 'lodash';
|
import _ from 'lodash';
|
||||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
import { isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||||
import { arrayToHexString, isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
|
||||||
import { showModal } from '../modals/modalTools';
|
|
||||||
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
|
||||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||||
import openNewTab from '../utility/openNewTab';
|
import openNewTab from '../utility/openNewTab';
|
||||||
|
import CellValue from './CellValue.svelte';
|
||||||
|
|
||||||
export let rowIndex;
|
export let rowIndex;
|
||||||
export let col;
|
export let col;
|
||||||
@@ -101,49 +64,7 @@
|
|||||||
class:isFocusedColumn
|
class:isFocusedColumn
|
||||||
{style}
|
{style}
|
||||||
>
|
>
|
||||||
{#if rowData == null}
|
<CellValue {rowData} {value} {jsonParsedValue} />
|
||||||
<span class="null">(No row)</span>
|
|
||||||
{:else if value === null}
|
|
||||||
<span class="null">(NULL)</span>
|
|
||||||
{:else if value === undefined}
|
|
||||||
<span class="null">(No field)</span>
|
|
||||||
{:else if _.isDate(value)}
|
|
||||||
{value.toString()}
|
|
||||||
{:else if value === true}
|
|
||||||
<span class="value">true</span>
|
|
||||||
{:else if value === false}
|
|
||||||
<span class="value">false</span>
|
|
||||||
{:else if _.isNumber(value)}
|
|
||||||
<span class="value">{formatNumber(value)}</span>
|
|
||||||
{:else if _.isString(value) && !jsonParsedValue}
|
|
||||||
{#if dateTimeRegex.test(value)}
|
|
||||||
<span class="value">
|
|
||||||
{formatDateTime(value)}
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
{highlightSpecialCharacters(value)}
|
|
||||||
{/if}
|
|
||||||
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
|
|
||||||
{#if value.data.length <= 16}
|
|
||||||
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
|
|
||||||
{:else}
|
|
||||||
<span class="null">({value.data.length} bytes)</span>
|
|
||||||
{/if}
|
|
||||||
{:else if value.$oid}
|
|
||||||
<span class="value">ObjectId("{value.$oid}")</span>
|
|
||||||
{:else if _.isPlainObject(value)}
|
|
||||||
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
|
||||||
{:else if _.isArray(value)}
|
|
||||||
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
|
|
||||||
{:else if _.isPlainObject(jsonParsedValue)}
|
|
||||||
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
|
|
||||||
{:else if _.isArray(jsonParsedValue)}
|
|
||||||
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
|
|
||||||
>[{jsonParsedValue.length} items]</span
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
{value.toString()}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
|
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
|
||||||
<span class="hint"
|
<span class="hint"
|
||||||
@@ -256,13 +177,6 @@
|
|||||||
color: var(--theme-font-3);
|
color: var(--theme-font-3);
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.null {
|
|
||||||
color: var(--theme-font-3);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
color: var(--theme-icon-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autoFillMarker {
|
.autoFillMarker {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -1194,7 +1194,7 @@
|
|||||||
// console.log('event', event.nativeEvent);
|
// console.log('event', event.nativeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.keyCode == keycodes.f2) {
|
if (event.keyCode == keycodes.f2 || event.keyCode == keycodes.enter) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||||
}
|
}
|
||||||
@@ -1257,8 +1257,10 @@
|
|||||||
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
|
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
|
||||||
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
|
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
|
||||||
case keycodes.downArrow:
|
case keycodes.downArrow:
|
||||||
case keycodes.enter:
|
|
||||||
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||||
|
case keycodes.enter:
|
||||||
|
if (!grider.editable) return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||||
|
break;
|
||||||
case keycodes.leftArrow:
|
case keycodes.leftArrow:
|
||||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||||
case keycodes.rightArrow:
|
case keycodes.rightArrow:
|
||||||
@@ -1272,7 +1274,17 @@
|
|||||||
case keycodes.pageDown:
|
case keycodes.pageDown:
|
||||||
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
|
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
|
||||||
case keycodes.tab: {
|
case keycodes.tab: {
|
||||||
if (event.shiftKey) {
|
return moveCurrentCellWithTabKey(event.shiftKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCurrentCellWithTabKey(isShift) {
|
||||||
|
if (!isRegularCell(currentCell)) return null;
|
||||||
|
|
||||||
|
if (isShift) {
|
||||||
if (currentCell[1] > 0) {
|
if (currentCell[1] > 0) {
|
||||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||||
} else {
|
} else {
|
||||||
@@ -1286,10 +1298,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCellValue(cell, value) {
|
function setCellValue(cell, value) {
|
||||||
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
|
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
|
||||||
@@ -1431,10 +1439,24 @@
|
|||||||
selectAll: action.selectAll,
|
selectAll: action.selectAll,
|
||||||
};
|
};
|
||||||
case 'close': {
|
case 'close': {
|
||||||
const [row, col] = currentCell || [];
|
|
||||||
if (domFocusField) domFocusField.focus();
|
if (domFocusField) domFocusField.focus();
|
||||||
// @ts-ignore
|
if (action.mode == 'enter' || action.mode == 'tab' || action.mode == 'shiftTab') {
|
||||||
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
|
setTimeout(() => {
|
||||||
|
if (isRegularCell(currentCell)) {
|
||||||
|
switch (action.mode) {
|
||||||
|
case 'enter':
|
||||||
|
moveCurrentCell(currentCell[0] + 1, currentCell[1]);
|
||||||
|
break;
|
||||||
|
case 'tab':
|
||||||
|
moveCurrentCellWithTabKey(false);
|
||||||
|
break;
|
||||||
|
case 'shiftTab':
|
||||||
|
moveCurrentCellWithTabKey(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
// if (action.mode == 'save') setTimeout(handleSave, 0);
|
// if (action.mode == 'save') setTimeout(handleSave, 0);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,18 +36,26 @@
|
|||||||
break;
|
break;
|
||||||
case keycodes.enter:
|
case keycodes.enter:
|
||||||
if (isChangedRef.get()) {
|
if (isChangedRef.get()) {
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
onSetValue(parseCellValue(domEditor.value));
|
onSetValue(parseCellValue(domEditor.value));
|
||||||
isChangedRef.set(false);
|
isChangedRef.set(false);
|
||||||
}
|
}
|
||||||
domEditor.blur();
|
domEditor.blur();
|
||||||
|
event.preventDefault();
|
||||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
||||||
break;
|
break;
|
||||||
|
case keycodes.tab:
|
||||||
|
if (isChangedRef.get()) {
|
||||||
|
onSetValue(parseCellValue(domEditor.value));
|
||||||
|
isChangedRef.set(false);
|
||||||
|
}
|
||||||
|
domEditor.blur();
|
||||||
|
event.preventDefault();
|
||||||
|
dispatchInsplaceEditor({ type: 'close', mode: event.shiftKey ? 'shiftTab' : 'tab' });
|
||||||
|
break;
|
||||||
case keycodes.s:
|
case keycodes.s:
|
||||||
if (isCtrlOrCommandKey(event)) {
|
if (isCtrlOrCommandKey(event)) {
|
||||||
if (isChangedRef.get()) {
|
if (isChangedRef.get()) {
|
||||||
onSetValue(parseCellValue(domEditor.value));
|
onSetValue(parseCellValue(domEditor.value));
|
||||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
|
||||||
isChangedRef.set(false);
|
isChangedRef.set(false);
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let width;
|
export let width;
|
||||||
|
export let isFlex = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style={`max-width: ${width}px`}>
|
<div style={`max-width: ${width}px`} class:isFlex>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -12,4 +13,8 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.isFlex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 10px;
|
min-width: 10px;
|
||||||
|
min-height: 22px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
36
packages/web/src/forms/TemplatedCheckboxField.svelte
Normal file
36
packages/web/src/forms/TemplatedCheckboxField.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getFormContext } from './FormProviderCore.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let label;
|
||||||
|
export let name;
|
||||||
|
export let disabled = false;
|
||||||
|
export let templateProps = {};
|
||||||
|
export let checked: boolean;
|
||||||
|
|
||||||
|
let refInput;
|
||||||
|
|
||||||
|
const { template, setFieldValue, values } = getFormContext();
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleChange() {
|
||||||
|
dispatch('change', refInput.checked);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={template}
|
||||||
|
type="checkbox"
|
||||||
|
{label}
|
||||||
|
{disabled}
|
||||||
|
{...templateProps}
|
||||||
|
labelProps={disabled
|
||||||
|
? { disabled: true }
|
||||||
|
: {
|
||||||
|
onClick: () => {
|
||||||
|
dispatch('change', !refInput.checked);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input bind:this={refInput} {checked} type="checkbox" {...$$restProps} on:change={handleChange} />
|
||||||
|
</svelte:component>
|
||||||
@@ -94,6 +94,9 @@
|
|||||||
'icon add': 'mdi mdi-plus-circle',
|
'icon add': 'mdi mdi-plus-circle',
|
||||||
'icon json': 'mdi mdi-code-json',
|
'icon json': 'mdi mdi-code-json',
|
||||||
'icon lock': 'mdi mdi-lock',
|
'icon lock': 'mdi mdi-lock',
|
||||||
|
'icon custom-join': 'mdi mdi-arrow-left-right-bold',
|
||||||
|
'icon parent-filter': 'mdi mdi-home-alert',
|
||||||
|
'icon parent-filter-outline': 'mdi mdi-home-alert-outline',
|
||||||
|
|
||||||
'icon run': 'mdi mdi-play',
|
'icon run': 'mdi mdi-play',
|
||||||
'icon chevron-down': 'mdi mdi-chevron-down',
|
'icon chevron-down': 'mdi mdi-chevron-down',
|
||||||
@@ -120,6 +123,7 @@
|
|||||||
'img warn': 'mdi mdi-alert color-icon-gold',
|
'img warn': 'mdi mdi-alert color-icon-gold',
|
||||||
'img info': 'mdi mdi-information color-icon-blue',
|
'img info': 'mdi mdi-information color-icon-blue',
|
||||||
// 'img statusbar-ok': 'mdi mdi-check-circle color-on-statusbar-green',
|
// 'img statusbar-ok': 'mdi mdi-check-circle color-on-statusbar-green',
|
||||||
|
'img circular': 'mdi mdi-circular-saw color-icon-red',
|
||||||
|
|
||||||
'img archive': 'mdi mdi-table color-icon-gold',
|
'img archive': 'mdi mdi-table color-icon-gold',
|
||||||
'img archive-folder': 'mdi mdi-database-outline color-icon-green',
|
'img archive-folder': 'mdi mdi-database-outline color-icon-green',
|
||||||
@@ -171,6 +175,8 @@
|
|||||||
'img link': 'mdi mdi-link',
|
'img link': 'mdi mdi-link',
|
||||||
'img filter': 'mdi mdi-filter',
|
'img filter': 'mdi mdi-filter',
|
||||||
'img group': 'mdi mdi-group',
|
'img group': 'mdi mdi-group',
|
||||||
|
'img perspective': 'mdi mdi-eye color-icon-yellow',
|
||||||
|
'img parent-filter': 'mdi mdi-home-alert color-icon-yellow',
|
||||||
|
|
||||||
'img folder': 'mdi mdi-folder color-icon-yellow',
|
'img folder': 'mdi mdi-folder color-icon-yellow',
|
||||||
'img type-string': 'mdi mdi-alphabetical color-icon-blue',
|
'img type-string': 'mdi mdi-alphabetical color-icon-blue',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
...(allowChooseModel ? [{ label: '(DB Model)', value: '__model' }] : []),
|
...(allowChooseModel ? [{ label: '(DB Model)', value: '__model' }] : []),
|
||||||
..._.sortBy(
|
..._.sortBy(
|
||||||
($connections || [])
|
($connections || [])
|
||||||
.filter(conn => (direction == 'target' ? !conn.isReadOnly : true))
|
.filter(conn => !conn.unsaved && (direction == 'target' ? !conn.isReadOnly : true))
|
||||||
.map(conn => ({
|
.map(conn => ({
|
||||||
value: conn._id,
|
value: conn._id,
|
||||||
label: getConnectionLabel(conn),
|
label: getConnectionLabel(conn),
|
||||||
|
|||||||
@@ -23,4 +23,5 @@
|
|||||||
label={labelOverride || `${nodeType} `}
|
label={labelOverride || `${nodeType} `}
|
||||||
bracketOpen={'{'}
|
bracketOpen={'{'}
|
||||||
bracketClose={'}'}
|
bracketClose={'}'}
|
||||||
|
elementValue={value}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
||||||
import openNewTab from '../utility/openNewTab';
|
import openNewTab from '../utility/openNewTab';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { copyTextToClipboard } from '../utility/clipboard';
|
||||||
|
|
||||||
setContext('json-tree-context-key', {});
|
setContext('json-tree-context-key', {});
|
||||||
|
|
||||||
@@ -34,8 +35,17 @@
|
|||||||
if (!closest) return;
|
if (!closest) return;
|
||||||
const value = elementData.get(closest);
|
const value = elementData.get(closest);
|
||||||
|
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
text: 'Copy JSON',
|
||||||
|
onClick: () => {
|
||||||
|
copyTextToClipboard(JSON.stringify(value, null, 2));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (value && _.isArray(value)) {
|
if (value && _.isArray(value)) {
|
||||||
return {
|
res.push({
|
||||||
text: 'Open as data sheet',
|
text: 'Open as data sheet',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openNewTab(
|
openNewTab(
|
||||||
@@ -53,8 +63,9 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
import FormProvider from '../forms/FormProvider.svelte';
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
import JSONTree from '../jsontree/JSONTree.svelte';
|
import TemplatedCheckboxField from '../forms/TemplatedCheckboxField.svelte';
|
||||||
import AceEditor from '../query/AceEditor.svelte';
|
import AceEditor from '../query/AceEditor.svelte';
|
||||||
import newQuery from '../query/newQuery';
|
import newQuery from '../query/newQuery';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
import ModalBase from './ModalBase.svelte';
|
import ModalBase from './ModalBase.svelte';
|
||||||
import { closeCurrentModal } from './modalTools';
|
import { closeCurrentModal } from './modalTools';
|
||||||
|
|
||||||
export let script;
|
export let script;
|
||||||
export let onConfirm;
|
export let onConfirm;
|
||||||
|
export let skipConfirmSettingKey = null;
|
||||||
|
|
||||||
|
let dontAskAgain;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormProvider>
|
<FormProvider>
|
||||||
@@ -21,6 +25,20 @@ import newQuery from '../query/newQuery';
|
|||||||
<AceEditor mode="javascript" readOnly value={script} />
|
<AceEditor mode="javascript" readOnly value={script} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if skipConfirmSettingKey}
|
||||||
|
<div class="mt-2">
|
||||||
|
<TemplatedCheckboxField
|
||||||
|
label="Don't ask again"
|
||||||
|
templateProps={{ noMargin: true }}
|
||||||
|
checked={dontAskAgain}
|
||||||
|
on:change={e => {
|
||||||
|
dontAskAgain = e.detail;
|
||||||
|
apiCall('config/update-settings', { [skipConfirmSettingKey]: e.detail });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
<FormSubmit
|
<FormSubmit
|
||||||
value="OK"
|
value="OK"
|
||||||
|
|||||||
@@ -17,12 +17,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _, { startsWith } from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||||
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
||||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
|
import TemplatedCheckboxField from '../forms/TemplatedCheckboxField.svelte';
|
||||||
import FontIcon from '../icons/FontIcon.svelte';
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
import newQuery from '../query/newQuery';
|
import newQuery from '../query/newQuery';
|
||||||
import SqlEditor from '../query/SqlEditor.svelte';
|
import SqlEditor from '../query/SqlEditor.svelte';
|
||||||
@@ -38,6 +39,9 @@
|
|||||||
export let engine;
|
export let engine;
|
||||||
export let recreates;
|
export let recreates;
|
||||||
export let deleteCascadesScripts;
|
export let deleteCascadesScripts;
|
||||||
|
export let skipConfirmSettingKey = null;
|
||||||
|
|
||||||
|
let dontAskAgain;
|
||||||
|
|
||||||
$: isRecreated = _.sum(_.values(recreates || {})) > 0;
|
$: isRecreated = _.sum(_.values(recreates || {})) > 0;
|
||||||
const values = writable({});
|
const values = writable({});
|
||||||
@@ -122,6 +126,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if skipConfirmSettingKey}
|
||||||
|
<div class="mt-2">
|
||||||
|
<TemplatedCheckboxField
|
||||||
|
label="Don't ask again"
|
||||||
|
templateProps={{ noMargin: true }}
|
||||||
|
checked={dontAskAgain}
|
||||||
|
on:change={e => {
|
||||||
|
dontAskAgain = e.detail;
|
||||||
|
apiCall('config/update-settings', { [skipConfirmSettingKey]: e.detail });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
<FormSubmit
|
<FormSubmit
|
||||||
value="OK"
|
value="OK"
|
||||||
|
|||||||
@@ -59,6 +59,19 @@
|
|||||||
let submenuOffset;
|
let submenuOffset;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
let closeHandlers = [];
|
||||||
|
|
||||||
|
function dispatchClose() {
|
||||||
|
dispatch('close');
|
||||||
|
for (const handler of closeHandlers) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
closeHandlers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerCloseHandler(handler) {
|
||||||
|
closeHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
function handleClick(e, item) {
|
function handleClick(e, item) {
|
||||||
if (item.disabled) return;
|
if (item.disabled) return;
|
||||||
@@ -70,7 +83,7 @@
|
|||||||
submenuOffset = hoverOffset;
|
submenuOffset = hoverOffset;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch('close');
|
dispatchClose();
|
||||||
if (onCloseParent) onCloseParent();
|
if (onCloseParent) onCloseParent();
|
||||||
if (item.onClick) item.onClick();
|
if (item.onClick) item.onClick();
|
||||||
}
|
}
|
||||||
@@ -84,13 +97,13 @@
|
|||||||
submenuOffset = hoverOffset;
|
submenuOffset = hoverOffset;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
$: preparedItems = prepareMenuItems(items, { targetElement }, $commandsCustomized);
|
$: preparedItems = prepareMenuItems(items, { targetElement, registerCloseHandler }, $commandsCustomized);
|
||||||
|
|
||||||
const handleClickOutside = event => {
|
const handleClickOutside = event => {
|
||||||
// if (element && !element.contains(event.target) && !event.defaultPrevented) {
|
// if (element && !element.contains(event.target) && !event.defaultPrevented) {
|
||||||
if (event.target.closest('ul.dropDownMenuMarker')) return;
|
if (event.target.closest('ul.dropDownMenuMarker')) return;
|
||||||
|
|
||||||
dispatch('close');
|
dispatchClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -134,7 +147,7 @@
|
|||||||
{...submenuOffset}
|
{...submenuOffset}
|
||||||
onCloseParent={() => {
|
onCloseParent={() => {
|
||||||
if (onCloseParent) onCloseParent();
|
if (onCloseParent) onCloseParent();
|
||||||
dispatch('close');
|
dispatchClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
365
packages/web/src/perspectives/CustomJoinModal.svelte
Normal file
365
packages/web/src/perspectives/CustomJoinModal.svelte
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
|
||||||
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
|
import ModalBase from '../modals/ModalBase.svelte';
|
||||||
|
import { closeCurrentModal } from '../modals/modalTools';
|
||||||
|
import { fullNameFromString, fullNameToLabel, fullNameToString } from 'dbgate-tools';
|
||||||
|
import SelectField from '../forms/SelectField.svelte';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
useConnectionList,
|
||||||
|
useDatabaseInfo,
|
||||||
|
useDatabaseList,
|
||||||
|
useTableInfo,
|
||||||
|
useViewInfo,
|
||||||
|
} from '../utility/metadataLoaders';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import {
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveCustomJoinConfig,
|
||||||
|
PerspectiveTreeNode,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||||
|
import uuidv1 from 'uuid/v1';
|
||||||
|
import TextField from '../forms/TextField.svelte';
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let root: PerspectiveTreeNode;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let editValue: PerspectiveCustomJoinConfig = null;
|
||||||
|
|
||||||
|
let conidOverride = editValue?.conid || null;
|
||||||
|
let databaseOverride = editValue?.database || null;
|
||||||
|
let joinid = editValue?.joinid || uuidv1();
|
||||||
|
|
||||||
|
// $: fromDbInfo = useDatabaseInfo({
|
||||||
|
// conid,
|
||||||
|
// database,
|
||||||
|
// });
|
||||||
|
// $: fromTableInfo = useTableInfo({
|
||||||
|
// conid: conidOverride || conid,
|
||||||
|
// database: databaseOverride || database,
|
||||||
|
// schemaName: fromSchemaName,
|
||||||
|
// pureName: fromTableName,
|
||||||
|
// });
|
||||||
|
|
||||||
|
$: refDbInfo = useDatabaseInfo({
|
||||||
|
conid: conidOverride || conid,
|
||||||
|
database: databaseOverride || database,
|
||||||
|
});
|
||||||
|
$: refTableInfo = useTableInfo({
|
||||||
|
conid: conidOverride || conid,
|
||||||
|
database: databaseOverride || database,
|
||||||
|
schemaName: refSchemaName,
|
||||||
|
pureName: refTableName,
|
||||||
|
});
|
||||||
|
$: refViewInfo = useViewInfo({
|
||||||
|
conid: conidOverride || conid,
|
||||||
|
database: databaseOverride || database,
|
||||||
|
schemaName: refSchemaName,
|
||||||
|
pureName: refTableName,
|
||||||
|
});
|
||||||
|
|
||||||
|
let columns = editValue?.columns || [];
|
||||||
|
// let fromTableName = pureName;
|
||||||
|
// let fromSchemaName = schemaName;
|
||||||
|
let fromUniuqeName = editValue?.baseUniqueName || root.uniqueName;
|
||||||
|
let refTableName = editValue?.refTableName || null;
|
||||||
|
let refSchemaName = editValue?.refSchemaName || null;
|
||||||
|
let joinName = editValue?.joinName || '';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (editValue) return;
|
||||||
|
let index = 1;
|
||||||
|
while (config.customJoins?.find(x => x.joinName == `Custom join ${index}`)) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
joinName = `Custom join ${index}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// $: fromTableList = [
|
||||||
|
// ..._.sortBy($fromDbInfo?.tables || [], ['schemaName', 'pureName']),
|
||||||
|
// // ..._.sortBy($dbInfo?.views || [], ['schemaName', 'pureName']),
|
||||||
|
// ];
|
||||||
|
$: refTableList = [
|
||||||
|
..._.sortBy($refDbInfo?.tables || [], ['schemaName', 'pureName']),
|
||||||
|
..._.sortBy($refDbInfo?.views || [], ['schemaName', 'pureName']),
|
||||||
|
];
|
||||||
|
|
||||||
|
let refTableOptions = [];
|
||||||
|
let fromTableOptions = [];
|
||||||
|
|
||||||
|
$: connections = useConnectionList();
|
||||||
|
$: connectionOptions = [
|
||||||
|
{ value: null, label: 'The same as root' },
|
||||||
|
..._.sortBy(
|
||||||
|
($connections || [])
|
||||||
|
.filter(x => !x.unsaved)
|
||||||
|
.map(conn => ({
|
||||||
|
value: conn._id,
|
||||||
|
label: getConnectionLabel(conn),
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// $: fromTable = $fromDbInfo?.tables?.find(x => x.pureName == fromTableName && x.schemaName == fromSchemaName);
|
||||||
|
|
||||||
|
$: databases = useDatabaseList({ conid: conidOverride || conid });
|
||||||
|
|
||||||
|
$: databaseOptions = [
|
||||||
|
{ value: null, label: 'The same as root' },
|
||||||
|
..._.sortBy(
|
||||||
|
($databases || []).map(db => ({
|
||||||
|
value: db.name,
|
||||||
|
label: db.name,
|
||||||
|
})),
|
||||||
|
'label'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
$: fromTableList = root.getBaseTables();
|
||||||
|
$: fromTableInfo = fromTableList?.find(x => x.node.uniqueName == fromUniuqeName)?.table;
|
||||||
|
|
||||||
|
$: (async () => {
|
||||||
|
// without this has svelte problem, doesn't invalidate SelectField options
|
||||||
|
await tick();
|
||||||
|
// to replicate try to invoke VFK editor after page refresh, when active widget without DB, eg. application layers
|
||||||
|
// and comment line above. Tables list in vFK editor will be empty
|
||||||
|
|
||||||
|
fromTableOptions = fromTableList.map(tbl => ({
|
||||||
|
label: fullNameToLabel(tbl.table),
|
||||||
|
value: tbl.node.uniqueName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
refTableOptions = refTableList.map(tbl => ({
|
||||||
|
label: fullNameToLabel(tbl),
|
||||||
|
value: fullNameToString(tbl),
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
// $: refTableInfo = tableList.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
|
||||||
|
// $dbInfo?.views?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
|
||||||
|
|
||||||
|
// $: console.log('conid, database', conid, database);
|
||||||
|
// $: console.log('$dbInfo?.tables', $dbInfo?.tables);
|
||||||
|
// $: console.log('tableList', tableList);
|
||||||
|
// $: console.log('tableOptions', tableOptions);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormProvider>
|
||||||
|
<ModalBase {...$$restProps}>
|
||||||
|
<svelte:fragment slot="header">Define custom join</svelte:fragment>
|
||||||
|
|
||||||
|
<div class="largeFormMarker">
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Join name</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<TextField
|
||||||
|
value={joinName}
|
||||||
|
options={fromTableOptions}
|
||||||
|
on:change={e => {
|
||||||
|
joinName = e.target['value'];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Base table</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<SelectField
|
||||||
|
value={fromUniuqeName}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={fromTableOptions}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
fromUniuqeName = e.detail;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Connection</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<SelectField
|
||||||
|
value={conidOverride}
|
||||||
|
isNative
|
||||||
|
options={connectionOptions}
|
||||||
|
on:change={e => {
|
||||||
|
conidOverride = e.detail;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Database</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<SelectField
|
||||||
|
value={databaseOverride}
|
||||||
|
isNative
|
||||||
|
options={databaseOptions}
|
||||||
|
on:change={e => {
|
||||||
|
databaseOverride = e.detail;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <FormConnectionSelect name="conid" label="Server" {direction} />
|
||||||
|
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" /> -->
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Referenced table</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<SelectField
|
||||||
|
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={refTableOptions}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
const name = fullNameFromString(e.detail);
|
||||||
|
refTableName = name.pureName;
|
||||||
|
refSchemaName = name.schemaName;
|
||||||
|
const refTable = $refDbInfo?.tables?.find(
|
||||||
|
x => x.pureName == refTableName && x.schemaName == refSchemaName
|
||||||
|
);
|
||||||
|
columns =
|
||||||
|
refTable?.primaryKey?.columns?.map(col => ({
|
||||||
|
refColumnName: col.columnName,
|
||||||
|
})) || [];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 mr-1">
|
||||||
|
Base column - {fromTableInfo?.pureName}
|
||||||
|
</div>
|
||||||
|
<div class="col-5 ml-1">
|
||||||
|
Ref column - {refTableName || '(table not set)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each columns as column, index}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 mr-1">
|
||||||
|
{#key column.baseColumnName}
|
||||||
|
<SelectField
|
||||||
|
value={column.baseColumnName}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={(fromTableInfo?.columns || []).map(col => ({
|
||||||
|
label: col.columnName,
|
||||||
|
value: col.columnName,
|
||||||
|
}))}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
columns = columns.map((col, i) => (i == index ? { ...col, baseColumnName: e.detail } : col));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
<div class="col-5 ml-1">
|
||||||
|
{#key column.refColumnName}
|
||||||
|
<SelectField
|
||||||
|
value={column.refColumnName}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={($refTableInfo?.columns || $refViewInfo?.columns || []).map(col => ({
|
||||||
|
label: col.columnName,
|
||||||
|
value: col.columnName,
|
||||||
|
}))}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
columns = columns.map((col, i) => (i == index ? { ...col, refColumnName: e.detail } : col));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
<div class="col-2 button">
|
||||||
|
<FormStyledButton
|
||||||
|
value="Delete"
|
||||||
|
on:click={e => {
|
||||||
|
const x = [...columns];
|
||||||
|
x.splice(index, 1);
|
||||||
|
columns = x;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<FormStyledButton
|
||||||
|
type="button"
|
||||||
|
value="Add column"
|
||||||
|
on:click={() => {
|
||||||
|
columns = [
|
||||||
|
...columns,
|
||||||
|
{
|
||||||
|
baseColumnName: '',
|
||||||
|
refColumnName: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<FormSubmit
|
||||||
|
value={'Save'}
|
||||||
|
on:click={async () => {
|
||||||
|
const newJoin = {
|
||||||
|
joinid,
|
||||||
|
joinName,
|
||||||
|
baseUniqueName: fromUniuqeName,
|
||||||
|
refTableName,
|
||||||
|
refSchemaName,
|
||||||
|
columns,
|
||||||
|
conid: conidOverride,
|
||||||
|
database: databaseOverride,
|
||||||
|
};
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
customJoins: editValue
|
||||||
|
? cfg.customJoins.map(x => (x.joinid == editValue.joinid ? newJoin : x))
|
||||||
|
: [...(cfg.customJoins || []), newJoin],
|
||||||
|
}));
|
||||||
|
closeCurrentModal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ModalBase>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
margin: var(--dim-large-form-margin);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .label {
|
||||||
|
white-space: nowrap;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
align-self: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
packages/web/src/perspectives/PerspectiveCell.svelte
Normal file
32
packages/web/src/perspectives/PerspectiveCell.svelte
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CellValue from '../datagrid/CellValue.svelte';
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
export let rowSpan;
|
||||||
|
export let rowData;
|
||||||
|
export let columnIndex;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<td rowspan={rowSpan} data-column={columnIndex}>
|
||||||
|
{#if value !== undefined}
|
||||||
|
<CellValue {rowData} {value} />
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
td {
|
||||||
|
font-weight: normal;
|
||||||
|
/* border: 1px solid var(--theme-border); */
|
||||||
|
background-color: var(--theme-bg-0);
|
||||||
|
padding: 2px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: top;
|
||||||
|
border-bottom: 1px solid var(--theme-border);
|
||||||
|
border-right: 1px solid var(--theme-border);
|
||||||
|
}
|
||||||
|
td:global(.highlight) {
|
||||||
|
border: 3px solid var(--theme-icon-blue);
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
68
packages/web/src/perspectives/PerspectiveFilters.svelte
Normal file
68
packages/web/src/perspectives/PerspectiveFilters.svelte
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveTreeNode } from 'dbgate-datalib';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import PerspectiveFiltersColumn from './PerspectiveFiltersColumn.svelte';
|
||||||
|
|
||||||
|
export let managerSize;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
export let root: PerspectiveTreeNode;
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let driver;
|
||||||
|
|
||||||
|
$: allFilterNames = _.keys(config.filters || {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ManagerInnerContainer width={managerSize} isFlex={allFilterNames.length == 0}>
|
||||||
|
{#if allFilterNames.length == 0}
|
||||||
|
<div class="msg">
|
||||||
|
<div class="mb-3 bold">No Filters defined</div>
|
||||||
|
<div><FontIcon icon="img info" /> Use context menu, command "Add to filter" in table or in tree</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each allFilterNames as uniqueName}
|
||||||
|
{@const node = root?.findNodeByUniqueName(uniqueName)}
|
||||||
|
{@const filterInfo = node?.filterInfo}
|
||||||
|
{#if filterInfo}
|
||||||
|
<PerspectiveFiltersColumn
|
||||||
|
{filterInfo}
|
||||||
|
{uniqueName}
|
||||||
|
{conid}
|
||||||
|
{database}
|
||||||
|
{driver}
|
||||||
|
{node}
|
||||||
|
{config}
|
||||||
|
{setConfig}
|
||||||
|
filter={config.filters[uniqueName]}
|
||||||
|
onSetFilter={value =>
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
filters: {
|
||||||
|
...cfg.filters,
|
||||||
|
[uniqueName]: value,
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
onRemoveFilter={value =>
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
filters: _.omit(cfg.filters, [uniqueName]),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ManagerInnerContainer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.msg {
|
||||||
|
background: var(--theme-bg-1);
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||||
|
|
||||||
|
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||||
|
import InlineButton from '../buttons/InlineButton.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||||
|
import {
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveFilterColumnInfo,
|
||||||
|
PerspectiveTreeNode,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
||||||
|
import ValueLookupModal from '../modals/ValueLookupModal.svelte';
|
||||||
|
|
||||||
|
export let filterInfo: PerspectiveFilterColumnInfo;
|
||||||
|
|
||||||
|
export let filter;
|
||||||
|
export let onSetFilter;
|
||||||
|
export let onRemoveFilter;
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let driver;
|
||||||
|
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
|
||||||
|
export let node: PerspectiveTreeNode;
|
||||||
|
|
||||||
|
$: customCommandIcon = node?.parentNode?.supportsParentFilter
|
||||||
|
? node?.parentNode?.isParentFilter
|
||||||
|
? 'icon parent-filter'
|
||||||
|
: 'icon parent-filter-outline'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function changeParentFilter() {
|
||||||
|
const tableNode = node?.parentNode;
|
||||||
|
if (!tableNode) return;
|
||||||
|
if (tableNode.isParentFilter) {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
parentFilters: cfg.parentFilters.filter(x => x.uniqueName != tableNode.uniqueName),
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
parentFilters: [...(cfg.parentFilters || []), { uniqueName: tableNode.uniqueName }],
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="m-1">
|
||||||
|
<div class="space-between">
|
||||||
|
{filterInfo.columnName} ({filterInfo.pureName})
|
||||||
|
<InlineButton square narrow on:click={onRemoveFilter}>
|
||||||
|
<FontIcon icon="icon close" />
|
||||||
|
</InlineButton>
|
||||||
|
</div>
|
||||||
|
<DataFilterControl
|
||||||
|
filterType={filterInfo.filterType}
|
||||||
|
{filter}
|
||||||
|
setFilter={onSetFilter}
|
||||||
|
{conid}
|
||||||
|
{database}
|
||||||
|
{driver}
|
||||||
|
columnName={filterInfo.columnName}
|
||||||
|
pureName={filterInfo.pureName}
|
||||||
|
foreignKey={filterInfo.foreignKey}
|
||||||
|
{customCommandIcon}
|
||||||
|
onCustomCommand={customCommandIcon ? changeParentFilter : null}
|
||||||
|
customCommandTooltip='Filter parent rows'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
126
packages/web/src/perspectives/PerspectiveHeaderControl.svelte
Normal file
126
packages/web/src/perspectives/PerspectiveHeaderControl.svelte
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveDisplayColumn } from 'dbgate-datalib';
|
||||||
|
import _, { mapKeys } from 'lodash';
|
||||||
|
|
||||||
|
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
export let column: PerspectiveDisplayColumn;
|
||||||
|
export let columnLevel;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
|
||||||
|
$: parentUniqueName = column.dataNode?.parentNode?.uniqueName || '';
|
||||||
|
$: uniqueName = column.dataNode.uniqueName;
|
||||||
|
$: order = config.sort?.[parentUniqueName]?.find(x => x.uniqueName == uniqueName)?.order;
|
||||||
|
$: orderIndex =
|
||||||
|
config.sort?.[parentUniqueName]?.length > 1
|
||||||
|
? _.findIndex(config.sort?.[parentUniqueName], x => x.uniqueName == uniqueName)
|
||||||
|
: -1;
|
||||||
|
$: isSortDefined = config.sort?.[parentUniqueName]?.length > 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if column.isVisible(columnLevel)}
|
||||||
|
<th rowspan={column.rowSpan} class="columnHeader" data-column={column.columnIndex}>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="label">
|
||||||
|
{column.title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if order == 'ASC'}
|
||||||
|
<span class="icon">
|
||||||
|
<FontIcon icon="img sort-asc" />
|
||||||
|
{#if orderIndex >= 0}
|
||||||
|
<span class="color-icon-green order-index">{orderIndex + 1}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if order == 'DESC'}
|
||||||
|
<span class="icon">
|
||||||
|
<FontIcon icon="img sort-desc" />
|
||||||
|
{#if orderIndex >= 0}
|
||||||
|
<span class="color-icon-green order-index">{orderIndex + 1}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
{#if column.showParent(columnLevel)}
|
||||||
|
<th
|
||||||
|
colspan={column.getColSpan(columnLevel)}
|
||||||
|
class="tableHeader"
|
||||||
|
data-tableNodeUniqueName={column.getParentTableUniqueName(columnLevel)}
|
||||||
|
>
|
||||||
|
<div class="wrap">
|
||||||
|
{column.getParentName(columnLevel)}
|
||||||
|
{#if column.getParentNode(columnLevel)?.isParentFilter}
|
||||||
|
<span class="icon">
|
||||||
|
<FontIcon icon="img parent-filter" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.order-index {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-left: -3px;
|
||||||
|
margin-right: 2px;
|
||||||
|
top: -1px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 10px;
|
||||||
|
padding: 2px;
|
||||||
|
margin: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin-left: 3px;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.grouping {
|
||||||
|
color: var(--theme-font-alt);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.data-type {
|
||||||
|
color: var(--theme-font-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
/* border: 1px solid var(--theme-border); */
|
||||||
|
text-align: left;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--theme-bg-1);
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: center;
|
||||||
|
z-index: 100;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--theme-border);
|
||||||
|
border-right: 1px solid var(--theme-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
th.tableHeader {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.columnHeader {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:global(.highlight) {
|
||||||
|
border: 3px solid var(--theme-icon-blue);
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<div class="lds-spinner">
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lds-spinner {
|
||||||
|
color: official;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.lds-spinner div {
|
||||||
|
transform-origin: 40px 40px;
|
||||||
|
animation: lds-spinner 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
.lds-spinner div:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 37px;
|
||||||
|
width: 6px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 20%;
|
||||||
|
background: var(--theme-font-2);
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(1) {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
animation-delay: -1.1s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(2) {
|
||||||
|
transform: rotate(30deg);
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(3) {
|
||||||
|
transform: rotate(60deg);
|
||||||
|
animation-delay: -0.9s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(4) {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
animation-delay: -0.8s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(5) {
|
||||||
|
transform: rotate(120deg);
|
||||||
|
animation-delay: -0.7s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(6) {
|
||||||
|
transform: rotate(150deg);
|
||||||
|
animation-delay: -0.6s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(7) {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(8) {
|
||||||
|
transform: rotate(210deg);
|
||||||
|
animation-delay: -0.4s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(9) {
|
||||||
|
transform: rotate(240deg);
|
||||||
|
animation-delay: -0.3s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(10) {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
animation-delay: -0.2s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(11) {
|
||||||
|
transform: rotate(300deg);
|
||||||
|
animation-delay: -0.1s;
|
||||||
|
}
|
||||||
|
.lds-spinner div:nth-child(12) {
|
||||||
|
transform: rotate(330deg);
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
@keyframes lds-spinner {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
74
packages/web/src/perspectives/PerspectiveNodeRow.svelte
Normal file
74
packages/web/src/perspectives/PerspectiveNodeRow.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveTreeNode } from 'dbgate-datalib';
|
||||||
|
|
||||||
|
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||||
|
import { plusExpandIcon } from '../icons/expandIcons';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import contextMenu from '../utility/contextMenu';
|
||||||
|
import CustomJoinModal from './CustomJoinModal.svelte';
|
||||||
|
import { getPerspectiveNodeMenu } from './perspectiveMenu';
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let node: PerspectiveTreeNode;
|
||||||
|
export let root: PerspectiveTreeNode;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return getPerspectiveNodeMenu({
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
node,
|
||||||
|
root,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="row" use:contextMenu={createMenu}>
|
||||||
|
<span class="expandColumnIcon" style={`margin-right: ${5 + node.level * 10}px`}>
|
||||||
|
<FontIcon
|
||||||
|
icon={node.isExpandable ? plusExpandIcon(node.isExpanded) : 'icon invisible-box'}
|
||||||
|
on:click={() => {
|
||||||
|
node.toggleExpanded();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={node.isChecked}
|
||||||
|
on:click={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
on:mousedown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
on:change={() => {
|
||||||
|
node.toggleChecked();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FontIcon icon={node.icon} />
|
||||||
|
|
||||||
|
<span>{node.title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.row:hover {
|
||||||
|
background: var(--theme-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.isSelected {
|
||||||
|
background: var(--theme-bg-selected);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
548
packages/web/src/perspectives/PerspectiveTable.svelte
Normal file
548
packages/web/src/perspectives/PerspectiveTable.svelte
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const getCurrentEditor = () => getActiveComponent('PerspectiveTable');
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'perspective.openJson',
|
||||||
|
category: 'Perspective',
|
||||||
|
name: 'Open JSON',
|
||||||
|
isRelatedToTab: true,
|
||||||
|
testEnabled: () => getCurrentEditor()?.openJsonEnabled(),
|
||||||
|
onClick: () => getCurrentEditor().openJson(),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveDisplay,
|
||||||
|
PerspectiveTableColumnNode,
|
||||||
|
PerspectiveTreeNode,
|
||||||
|
PERSPECTIVE_PAGE_SIZE,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
import _, { values } from 'lodash';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import resizeObserver from '../utility/resizeObserver';
|
||||||
|
import debug from 'debug';
|
||||||
|
import contextMenu from '../utility/contextMenu';
|
||||||
|
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||||
|
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||||
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
import registerCommand from '../commands/registerCommand';
|
||||||
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
|
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||||
|
import PerspectiveCell from './PerspectiveCell.svelte';
|
||||||
|
import DataGridCell from '../datagrid/DataGridCell.svelte';
|
||||||
|
import PerspectiveLoadingIndicator from './PerspectiveLoadingIndicator.svelte';
|
||||||
|
import PerspectiveHeaderControl from './PerspectiveHeaderControl.svelte';
|
||||||
|
import createRef from '../utility/createRef';
|
||||||
|
import { getPerspectiveNodeMenu } from './perspectiveMenu';
|
||||||
|
import openNewTab from '../utility/openNewTab';
|
||||||
|
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectivaTable');
|
||||||
|
export const activator = createActivator('PerspectiveTable', true);
|
||||||
|
|
||||||
|
export let root: PerspectiveTreeNode;
|
||||||
|
export let loadedCounts;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
|
||||||
|
let dataRows;
|
||||||
|
let domWrapper;
|
||||||
|
let domTable;
|
||||||
|
let errorMessage;
|
||||||
|
let isLoading = false;
|
||||||
|
const lastVisibleRowIndexRef = createRef(0);
|
||||||
|
const disableLoadNextRef = createRef(false);
|
||||||
|
|
||||||
|
async function loadLevelData(node: PerspectiveTreeNode, parentRows: any[], counts) {
|
||||||
|
dbg('load level data', counts);
|
||||||
|
// const loadProps: PerspectiveDataLoadPropsWithNode[] = [];
|
||||||
|
const loadChildNodes = [];
|
||||||
|
const loadChildRows = [];
|
||||||
|
const loadProps = node.getNodeLoadProps(parentRows);
|
||||||
|
let { rows, incomplete } = await node.dataProvider.loadData({
|
||||||
|
...loadProps,
|
||||||
|
topCount: counts[node.uniqueName] || PERSPECTIVE_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
// console.log('ROWS', rows, node.isRoot);
|
||||||
|
|
||||||
|
if (node.isRoot) {
|
||||||
|
parentRows.push(...rows);
|
||||||
|
// console.log('PUSH PARENTROWS', parentRows);
|
||||||
|
|
||||||
|
if (incomplete) {
|
||||||
|
parentRows.push({
|
||||||
|
incompleteRowsIndicator: [node.uniqueName],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let lastRowWithChildren = null;
|
||||||
|
for (const parentRow of parentRows) {
|
||||||
|
const childRows = rows.filter(row => node.matchChildRow(parentRow, row));
|
||||||
|
parentRow[node.fieldName] = childRows;
|
||||||
|
if (childRows.length > 0) {
|
||||||
|
lastRowWithChildren = parentRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (incomplete && lastRowWithChildren) {
|
||||||
|
lastRowWithChildren[node.fieldName].push({
|
||||||
|
incompleteRowsIndicator: [node.uniqueName],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.isExpandable && child.isChecked) {
|
||||||
|
await loadLevelData(child, rows, counts);
|
||||||
|
// loadProps.push(child.getNodeLoadProps());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadProps.push({
|
||||||
|
// props: node.getNodeLoadProps(parentRows),
|
||||||
|
// node,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const grouped = groupPerspectiveLoadProps(...loadProps);
|
||||||
|
// for (const item of grouped) {
|
||||||
|
// const rows = await item.node.loader(item.props);
|
||||||
|
// if (item.node.isRoot) {
|
||||||
|
// parentRows.push(...rows);
|
||||||
|
// } else {
|
||||||
|
// const childRows = rows.filter(row => node.matchChildRow(row));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData(node: PerspectiveTreeNode, counts) {
|
||||||
|
// console.log('LOADING', node);
|
||||||
|
if (!node) return;
|
||||||
|
const rows = [];
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await loadLevelData(node, rows, counts);
|
||||||
|
dataRows = rows;
|
||||||
|
dbg('data rows', rows);
|
||||||
|
errorMessage = null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
errorMessage = err.message;
|
||||||
|
dataRows = null;
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
// console.log('DISPLAY ROWS', rows);
|
||||||
|
// const rows = await node.loadLevelData();
|
||||||
|
// for (const child of node.childNodes) {
|
||||||
|
// const loadProps = [];
|
||||||
|
// if (child.isExpandable && child.isChecked) {
|
||||||
|
// loadProps.push(child.getNodeLoadProps());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openJson() {
|
||||||
|
openJsonDocument(dataRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openJsonEnabled() {
|
||||||
|
return dataRows != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {});
|
||||||
|
|
||||||
|
$: loadData(root, $loadedCounts);
|
||||||
|
$: display = root && dataRows ? new PerspectiveDisplay(root, dataRows) : null;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
display;
|
||||||
|
disableLoadNextRef.set(false);
|
||||||
|
checkLoadAdditionalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMenu({ targetElement, registerCloseHandler }) {
|
||||||
|
const res = [];
|
||||||
|
const td = targetElement.closest('td') || targetElement.closest('th');
|
||||||
|
|
||||||
|
if (td) {
|
||||||
|
const tr = td.closest('tr');
|
||||||
|
|
||||||
|
const columnIndex = td.getAttribute('data-column');
|
||||||
|
const column = display?.columns?.[columnIndex];
|
||||||
|
if (column)
|
||||||
|
res.push(
|
||||||
|
getPerspectiveNodeMenu({
|
||||||
|
config,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
node: column.dataNode,
|
||||||
|
root,
|
||||||
|
setConfig,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
td.classList.add('highlight');
|
||||||
|
registerCloseHandler(() => {
|
||||||
|
td.classList.remove('highlight');
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableNodeUniqueName = td.getAttribute('data-tableNodeUniqueName');
|
||||||
|
const tableNode = root?.findNodeByUniqueName(tableNodeUniqueName);
|
||||||
|
|
||||||
|
if (tableNode?.headerTableAttributes) {
|
||||||
|
const { pureName, schemaName, conid, database } = tableNode?.headerTableAttributes;
|
||||||
|
res.push({
|
||||||
|
text: `Open table ${pureName}`,
|
||||||
|
onClick: () => {
|
||||||
|
openNewTab({
|
||||||
|
title: pureName,
|
||||||
|
icon: 'img table',
|
||||||
|
tabComponent: 'TableDataTab',
|
||||||
|
props: {
|
||||||
|
schemaName,
|
||||||
|
pureName,
|
||||||
|
conid: conid,
|
||||||
|
database: database,
|
||||||
|
objectTypeField: 'tables',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableNode?.supportsParentFilter) {
|
||||||
|
const isParentFilter = (config.parentFilters || []).find(x => x.uniqueName == tableNode.uniqueName);
|
||||||
|
if (isParentFilter) {
|
||||||
|
res.push({
|
||||||
|
text: 'Cancel filter parent rows',
|
||||||
|
onClick: () => {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
parentFilters: cfg.parentFilters.filter(x => x.uniqueName != tableNode.uniqueName),
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.push({
|
||||||
|
text: 'Filter parent rows',
|
||||||
|
onClick: () => {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
parentFilters: [...(cfg.parentFilters || []), { uniqueName: tableNode.uniqueName }],
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIndex = tr?.getAttribute('data-rowIndex');
|
||||||
|
if (rowIndex != null) {
|
||||||
|
const value = display.rows[rowIndex].rowData[columnIndex];
|
||||||
|
const { dataNode } = column;
|
||||||
|
|
||||||
|
if (dataNode instanceof PerspectiveTableColumnNode) {
|
||||||
|
const { table } = dataNode;
|
||||||
|
let tabComponent = null;
|
||||||
|
let icon = null;
|
||||||
|
let objectTypeField = null;
|
||||||
|
if (dataNode.isTable) {
|
||||||
|
tabComponent = 'TableDataTab';
|
||||||
|
icon = 'img table';
|
||||||
|
objectTypeField = 'tables';
|
||||||
|
}
|
||||||
|
if (dataNode.isView) {
|
||||||
|
tabComponent = 'ViewDataTab';
|
||||||
|
icon = 'img view';
|
||||||
|
objectTypeField = 'views';
|
||||||
|
}
|
||||||
|
if (tabComponent) {
|
||||||
|
res.push({
|
||||||
|
text: 'Open filtered table',
|
||||||
|
onClick: () => {
|
||||||
|
openNewTab(
|
||||||
|
{
|
||||||
|
title: table.pureName,
|
||||||
|
icon,
|
||||||
|
tabComponent,
|
||||||
|
props: {
|
||||||
|
schemaName: table.schemaName,
|
||||||
|
pureName: table.pureName,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
objectTypeField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
grid: {
|
||||||
|
filters: {
|
||||||
|
[dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType),
|
||||||
|
},
|
||||||
|
// isFormView: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
forceNewTab: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
text: 'Filter this value',
|
||||||
|
onClick: () => {
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
filters: {
|
||||||
|
...cfg.filters,
|
||||||
|
[dataNode.uniqueName]: getFilterValueExpression(value, dataNode.column.dataType),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push([
|
||||||
|
{ divider: true },
|
||||||
|
{ command: 'perspective.refresh' },
|
||||||
|
{ command: 'perspective.openJson' },
|
||||||
|
{ command: 'perspective.customJoin' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastVisibleRowIndex() {
|
||||||
|
var rows = domTable.querySelectorAll('tbody>tr');
|
||||||
|
const wrapBox = domWrapper.getBoundingClientRect();
|
||||||
|
|
||||||
|
function indexIsLastVisible(index) {
|
||||||
|
if (index < 0 || index >= rows.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = rows[index].getBoundingClientRect();
|
||||||
|
|
||||||
|
if (index == rows.length - 1) {
|
||||||
|
return wrapBox.bottom >= box.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
return box.top <= wrapBox.bottom && box.bottom >= wrapBox.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastValue = lastVisibleRowIndexRef.get();
|
||||||
|
|
||||||
|
let d = 0;
|
||||||
|
while (lastValue - d >= 0 || lastValue + d < rows.length) {
|
||||||
|
if (indexIsLastVisible(lastValue - d)) {
|
||||||
|
lastVisibleRowIndexRef.set(lastValue - d);
|
||||||
|
return lastValue - d;
|
||||||
|
}
|
||||||
|
if (indexIsLastVisible(lastValue + d)) {
|
||||||
|
lastVisibleRowIndexRef.set(lastValue + d);
|
||||||
|
return lastValue + d;
|
||||||
|
}
|
||||||
|
d += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// let rowIndex = 0;
|
||||||
|
// // let lastTr = null;
|
||||||
|
// for (const row of rows) {
|
||||||
|
// const box = row.getBoundingClientRect();
|
||||||
|
// // console.log('BOX', box);
|
||||||
|
// if (box.y > wrapBox.bottom) {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// // if (box.y > domWrapper.scrollTop + wrapBox.height) {
|
||||||
|
// // break;
|
||||||
|
// // }
|
||||||
|
// // lastTr = row;
|
||||||
|
// rowIndex += 1;
|
||||||
|
// }
|
||||||
|
// return rowIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLoadAdditionalData() {
|
||||||
|
if (!display) return;
|
||||||
|
await tick();
|
||||||
|
if (!domTable) return;
|
||||||
|
if (disableLoadNextRef.get()) return;
|
||||||
|
|
||||||
|
const rowIndex = getLastVisibleRowIndex();
|
||||||
|
|
||||||
|
// console.log('LAST VISIBLE', rowIndex);
|
||||||
|
|
||||||
|
const growIndicators = _.keys(display.loadIndicatorsCounts).filter(
|
||||||
|
indicator => rowIndex + 1 >= display.loadIndicatorsCounts[indicator]
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log('growIndicators', growIndicators);
|
||||||
|
// console.log('display.loadIndicatorsCounts IN', display.loadIndicatorsCounts);
|
||||||
|
// console.log('rowIndex', rowIndex);
|
||||||
|
|
||||||
|
if (growIndicators.length > 0) {
|
||||||
|
disableLoadNextRef.set(true);
|
||||||
|
dbg('load next', growIndicators);
|
||||||
|
loadedCounts.update(counts => {
|
||||||
|
const res = { ...counts };
|
||||||
|
for (const id of growIndicators) {
|
||||||
|
res[id] = (res[id] || PERSPECTIVE_PAGE_SIZE) + PERSPECTIVE_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('LAST VISIBLE ROW', rowIndex, wrapBox.height, lastTr, lastTr.getBoundingClientRect());
|
||||||
|
|
||||||
|
// var start = 0;
|
||||||
|
// var end = rows.length;
|
||||||
|
// var count = 0;
|
||||||
|
|
||||||
|
// while (start != end) {
|
||||||
|
// var mid = start + Math.floor((end - start) / 2);
|
||||||
|
// if ($(rows[mid]).offset().top < document.documentElement.scrollTop) start = mid + 1;
|
||||||
|
// else end = mid;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log('SCROLL', domTable.querySelector('tr:visible:last'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// $: console.log('display.loadIndicatorsCounts', display?.loadIndicatorsCounts);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wrapper"
|
||||||
|
bind:this={domWrapper}
|
||||||
|
use:resizeObserver={true}
|
||||||
|
use:contextMenu={buildMenu}
|
||||||
|
on:scroll={checkLoadAdditionalData}
|
||||||
|
>
|
||||||
|
{#if display}
|
||||||
|
<table bind:this={domTable}>
|
||||||
|
<thead>
|
||||||
|
{#each _.range(display.columnLevelCount) as columnLevel}
|
||||||
|
<tr>
|
||||||
|
{#each display.columns as column}
|
||||||
|
<PerspectiveHeaderControl {column} {columnLevel} {setConfig} {config} />
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
<!-- <tr>
|
||||||
|
{#each display.columns as column}
|
||||||
|
<th class="filter">
|
||||||
|
<DataFilterControl
|
||||||
|
filter={column.dataNode.getFilter()}
|
||||||
|
setFilter={value => column.dataNode.setFilter(value)}
|
||||||
|
columnName={column.dataNode.uniqueName}
|
||||||
|
filterType={column.dataNode.filterType}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
|
</tr> -->
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each display.rows as row, rowIndex}
|
||||||
|
<tr data-rowIndex={rowIndex}>
|
||||||
|
{#each display.columns as column}
|
||||||
|
{#if !row.rowCellSkips[column.columnIndex]}
|
||||||
|
<PerspectiveCell
|
||||||
|
columnIndex={column.columnIndex}
|
||||||
|
value={row.rowData[column.columnIndex]}
|
||||||
|
rowSpan={row.rowSpans[column.columnIndex]}
|
||||||
|
rowData={row.rowData}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<ErrorInfo message={errorMessage} />
|
||||||
|
|
||||||
|
<FormStyledButton
|
||||||
|
value="Reset filter"
|
||||||
|
on:click={() =>
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
filters: {},
|
||||||
|
parentFilters: [],
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="loader">
|
||||||
|
<PerspectiveLoadingIndicator />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
overflow: scroll;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
/* position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0; */
|
||||||
|
overflow: scroll;
|
||||||
|
/* border-collapse: collapse; */
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
border-collapse: separate; /* Don't collapse */
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.filter {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead :global(tr:first-child) :global(th) {
|
||||||
|
border-top: 1px solid var(--theme-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
table {
|
||||||
|
border: 1px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border: 1px solid;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
packages/web/src/perspectives/PerspectiveTree.svelte
Normal file
42
packages/web/src/perspectives/PerspectiveTree.svelte
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ChangeConfigFunc,
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
GridConfig,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveTreeNode,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
import { filterName } from 'dbgate-tools';
|
||||||
|
|
||||||
|
import PerspectiveNodeRow from './PerspectiveNodeRow.svelte';
|
||||||
|
|
||||||
|
export let root;
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let filter;
|
||||||
|
|
||||||
|
function getFlatColumns(node: PerspectiveTreeNode, filter: string) {
|
||||||
|
const res = [];
|
||||||
|
for (const col of node?.childNodes) {
|
||||||
|
if (filterName(filter, col.title)) {
|
||||||
|
res.push(col);
|
||||||
|
if (col.isExpanded) {
|
||||||
|
res.push(...getFlatColumns(col, filter));
|
||||||
|
}
|
||||||
|
} else if (col.isExpanded) {
|
||||||
|
const children = getFlatColumns(col, filter);
|
||||||
|
if (children.length > 0) {
|
||||||
|
res.push(col);
|
||||||
|
res.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each getFlatColumns(root, filter) as node}
|
||||||
|
<PerspectiveNodeRow {node} {config} {setConfig} {root} {conid} {database} />
|
||||||
|
{/each}
|
||||||
150
packages/web/src/perspectives/PerspectiveView.svelte
Normal file
150
packages/web/src/perspectives/PerspectiveView.svelte
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const getCurrentEditor = () => getActiveComponent('PerspectiveView');
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'perspective.customJoin',
|
||||||
|
category: 'Perspective',
|
||||||
|
name: 'Custom join',
|
||||||
|
keyText: 'CtrlOrCommand+J',
|
||||||
|
isRelatedToTab: true,
|
||||||
|
icon: 'icon custom-join',
|
||||||
|
testEnabled: () => getCurrentEditor() != null,
|
||||||
|
onClick: () => getCurrentEditor().defineCustomJoin(),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ChangeConfigFunc,
|
||||||
|
ChangePerspectiveConfigFunc,
|
||||||
|
extractPerspectiveDatabases,
|
||||||
|
getTableChildPerspectiveNodes,
|
||||||
|
GridConfig,
|
||||||
|
PerspectiveConfig,
|
||||||
|
PerspectiveDataLoadProps,
|
||||||
|
PerspectiveDataProvider,
|
||||||
|
PerspectiveTableColumnNode,
|
||||||
|
PerspectiveTableNode,
|
||||||
|
} from 'dbgate-datalib';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||||
|
import { useDatabaseInfo, useTableInfo, useViewInfo } from '../utility/metadataLoaders';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||||
|
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||||
|
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||||
|
import PerspectiveTree from './PerspectiveTree.svelte';
|
||||||
|
import PerspectiveTable from './PerspectiveTable.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { Select } from 'dbgate-sqltree';
|
||||||
|
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
|
||||||
|
import { PerspectiveDataLoader } from 'dbgate-datalib/lib/PerspectiveDataLoader';
|
||||||
|
import stableStringify from 'json-stable-stringify';
|
||||||
|
import createRef from '../utility/createRef';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
|
import registerCommand from '../commands/registerCommand';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import CustomJoinModal from './CustomJoinModal.svelte';
|
||||||
|
import JsonViewFilters from '../jsonview/JsonViewFilters.svelte';
|
||||||
|
import PerspectiveFilters from './PerspectiveFilters.svelte';
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||||
|
import { useMultipleDatabaseInfo } from '../utility/useMultipleDatabaseInfo';
|
||||||
|
|
||||||
|
const dbg = debug('dbgate:PerspectiveView');
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let driver;
|
||||||
|
|
||||||
|
export let config: PerspectiveConfig;
|
||||||
|
export let setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
export let loadedCounts;
|
||||||
|
|
||||||
|
export let cache;
|
||||||
|
|
||||||
|
let managerSize;
|
||||||
|
let filter;
|
||||||
|
|
||||||
|
export const activator = createActivator('PerspectiveView', true);
|
||||||
|
|
||||||
|
$: if (managerSize) setLocalStorage('perspectiveManagerWidth', managerSize);
|
||||||
|
|
||||||
|
function getInitialManagerSize() {
|
||||||
|
const width = getLocalStorage('perspectiveManagerWidth');
|
||||||
|
if (_.isNumber(width) && width > 30 && width < 500) {
|
||||||
|
return `${width}px`;
|
||||||
|
}
|
||||||
|
return '300px';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineCustomJoin() {
|
||||||
|
if (!root) return;
|
||||||
|
showModal(CustomJoinModal, {
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: dbInfos = useMultipleDatabaseInfo(extractPerspectiveDatabases({ conid, database }, config));
|
||||||
|
$: tableInfo = useTableInfo({ conid, database, ...config?.rootObject });
|
||||||
|
$: viewInfo = useViewInfo({ conid, database, ...config?.rootObject });
|
||||||
|
|
||||||
|
$: dataProvider = new PerspectiveDataProvider(cache, loader);
|
||||||
|
$: loader = new PerspectiveDataLoader(apiCall);
|
||||||
|
$: root =
|
||||||
|
$tableInfo || $viewInfo
|
||||||
|
? new PerspectiveTableNode(
|
||||||
|
$tableInfo || $viewInfo,
|
||||||
|
$dbInfos,
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
dataProvider,
|
||||||
|
{ conid, database },
|
||||||
|
null
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize}>
|
||||||
|
<div class="left" slot="1">
|
||||||
|
<WidgetColumnBar>
|
||||||
|
<WidgetColumnBarItem title="Choose data" name="perspectiveTree" height={'70%'}>
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search column or table" bind:value={filter} />
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
|
||||||
|
<ManagerInnerContainer width={managerSize}>
|
||||||
|
{#if root}
|
||||||
|
<PerspectiveTree {root} {config} {setConfig} {conid} {database} {filter} />
|
||||||
|
{/if}
|
||||||
|
</ManagerInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem title="Filters" name="tableFilters">
|
||||||
|
<PerspectiveFilters {managerSize} {config} {setConfig} {conid} {database} {driver} {root} />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
</WidgetColumnBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svelte:fragment slot="2">
|
||||||
|
<PerspectiveTable {root} {loadedCounts} {config} {setConfig} {conid} {database} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</HorizontalSplitter>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--theme-bg-0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
108
packages/web/src/perspectives/perspectiveMenu.ts
Normal file
108
packages/web/src/perspectives/perspectiveMenu.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveTreeNode } from 'dbgate-datalib';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import CustomJoinModal from './CustomJoinModal.svelte';
|
||||||
|
|
||||||
|
interface PerspectiveNodeMenuProps {
|
||||||
|
node: PerspectiveTreeNode;
|
||||||
|
conid: string;
|
||||||
|
database: string;
|
||||||
|
root: PerspectiveTreeNode;
|
||||||
|
config: PerspectiveConfig;
|
||||||
|
setConfig: ChangePerspectiveConfigFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPerspectiveNodeMenu(props: PerspectiveNodeMenuProps) {
|
||||||
|
const { node, conid, database, root, config, setConfig } = props;
|
||||||
|
const customJoin = node.customJoinConfig;
|
||||||
|
const filterInfo = node.filterInfo;
|
||||||
|
|
||||||
|
const parentUniqueName = node?.parentNode?.uniqueName || '';
|
||||||
|
const uniqueName = node.uniqueName;
|
||||||
|
const order = config.sort?.[parentUniqueName]?.find(x => x.uniqueName == uniqueName)?.order;
|
||||||
|
const orderIndex =
|
||||||
|
config.sort?.[parentUniqueName]?.length > 1
|
||||||
|
? _.findIndex(config.sort?.[parentUniqueName], x => x.uniqueName == uniqueName)
|
||||||
|
: -1;
|
||||||
|
const isSortDefined = config.sort?.[parentUniqueName]?.length > 0;
|
||||||
|
|
||||||
|
const setSort = order => {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
sort: {
|
||||||
|
...cfg.sort,
|
||||||
|
[parentUniqueName]: [{ uniqueName, order }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToSort = order => {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
sort: {
|
||||||
|
...cfg.sort,
|
||||||
|
[parentUniqueName]: [...(cfg.sort?.[parentUniqueName] || []), { uniqueName, order }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSort = () => {
|
||||||
|
setConfig(
|
||||||
|
cfg => ({
|
||||||
|
...cfg,
|
||||||
|
sort: {
|
||||||
|
...cfg.sort,
|
||||||
|
[parentUniqueName]: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||||
|
{ onClick: () => setSort('DESC'), text: 'Sort descending' },
|
||||||
|
isSortDefined && !order && { onClick: () => addToSort('ASC'), text: 'Add to sort - ascending' },
|
||||||
|
isSortDefined && !order && { onClick: () => addToSort('DESC'), text: 'Add to sort - descending' },
|
||||||
|
order && { onClick: () => clearSort(), text: 'Clear sort criteria' },
|
||||||
|
{ divider: true },
|
||||||
|
|
||||||
|
filterInfo && {
|
||||||
|
text: 'Add to filter',
|
||||||
|
onClick: () =>
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
filters: {
|
||||||
|
...cfg.filters,
|
||||||
|
[node.uniqueName]: '',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
customJoin && {
|
||||||
|
text: 'Remove custom join',
|
||||||
|
onClick: () =>
|
||||||
|
setConfig(cfg => ({
|
||||||
|
...cfg,
|
||||||
|
customJoins: (cfg.customJoins || []).filter(x => x.joinid != customJoin.joinid),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
customJoin && {
|
||||||
|
text: 'Edit custom join',
|
||||||
|
onClick: () =>
|
||||||
|
showModal(CustomJoinModal, {
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
root,
|
||||||
|
editValue: customJoin,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -60,7 +60,8 @@ ORDER BY
|
|||||||
tabs={[
|
tabs={[
|
||||||
{ label: 'General', slot: 1 },
|
{ label: 'General', slot: 1 },
|
||||||
{ label: 'Themes', slot: 2 },
|
{ label: 'Themes', slot: 2 },
|
||||||
{ label: 'Actions', slot: 3 },
|
{ label: 'Default Actions', slot: 3 },
|
||||||
|
{ label: 'Confirmations', slot: 4 },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="1">
|
<svelte:fragment slot="1">
|
||||||
@@ -166,7 +167,7 @@ ORDER BY
|
|||||||
label="Connection click"
|
label="Connection click"
|
||||||
name="defaultAction.connectionClick"
|
name="defaultAction.connectionClick"
|
||||||
isNative
|
isNative
|
||||||
defaultValue="openDetails"
|
defaultValue="connect"
|
||||||
options={[
|
options={[
|
||||||
{ value: 'openDetails', label: 'Edit / open details' },
|
{ value: 'openDetails', label: 'Edit / open details' },
|
||||||
{ value: 'connect', label: 'Connect' },
|
{ value: 'connect', label: 'Connect' },
|
||||||
@@ -224,6 +225,15 @@ ORDER BY
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="4">
|
||||||
|
<div class="heading">Confirmations</div>
|
||||||
|
|
||||||
|
<FormCheckboxField name="skipConfirm.tableDataSave" label="Skip confirmation when saving table data (SQL)" />
|
||||||
|
<FormCheckboxField
|
||||||
|
name="skipConfirm.collectionDataSave"
|
||||||
|
label="Skip confirmation when saving collection data (NoSQL)"
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</FormValues>
|
</FormValues>
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
import {
|
import {
|
||||||
createChangeSet,
|
createChangeSet,
|
||||||
createGridCache,
|
createGridCache,
|
||||||
createGridConfig,
|
|
||||||
TableFormViewDisplay,
|
|
||||||
CollectionGridDisplay,
|
CollectionGridDisplay,
|
||||||
changeSetContainsChanges,
|
changeSetContainsChanges,
|
||||||
runMacroOnChangeSet,
|
runMacroOnChangeSet,
|
||||||
@@ -45,8 +43,6 @@
|
|||||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
||||||
import registerCommand from '../commands/registerCommand';
|
import registerCommand from '../commands/registerCommand';
|
||||||
import { registerMenu } from '../utility/contextMenu';
|
import { registerMenu } from '../utility/contextMenu';
|
||||||
import EditJsonModal from '../modals/EditJsonModal.svelte';
|
|
||||||
import ChangeSetGrider from '../datagrid/ChangeSetGrider';
|
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
@@ -54,6 +50,7 @@
|
|||||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||||
|
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||||
|
|
||||||
export let tabid;
|
export let tabid;
|
||||||
export let conid;
|
export let conid;
|
||||||
@@ -119,11 +116,16 @@
|
|||||||
const driver = findEngineDriver($connection, $extensions);
|
const driver = findEngineDriver($connection, $extensions);
|
||||||
const script = driver.getCollectionUpdateScript ? driver.getCollectionUpdateScript(json) : null;
|
const script = driver.getCollectionUpdateScript ? driver.getCollectionUpdateScript(json) : null;
|
||||||
if (script) {
|
if (script) {
|
||||||
|
if (getBoolSettingsValue('skipConfirm.collectionDataSave', false)) {
|
||||||
|
handleConfirmChange(json);
|
||||||
|
} else {
|
||||||
showModal(ConfirmNoSqlModal, {
|
showModal(ConfirmNoSqlModal, {
|
||||||
script,
|
script,
|
||||||
onConfirm: () => handleConfirmChange(json),
|
onConfirm: () => handleConfirmChange(json),
|
||||||
engine: display.engine,
|
engine: display.engine,
|
||||||
|
skipConfirmSettingKey: 'skipConfirm.collectionDataSave',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handleConfirmChange(json);
|
handleConfirmChange(json);
|
||||||
}
|
}
|
||||||
|
|||||||
143
packages/web/src/tabs/PerspectiveTab.svelte
Normal file
143
packages/web/src/tabs/PerspectiveTab.svelte
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const getCurrentEditor = () => getActiveComponent('PerspectiveTab');
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'perspective.refresh',
|
||||||
|
category: 'Perspective',
|
||||||
|
name: 'Refresh',
|
||||||
|
keyText: 'F5 | CtrlOrCommand+R',
|
||||||
|
toolbar: true,
|
||||||
|
isRelatedToTab: true,
|
||||||
|
icon: 'icon reload',
|
||||||
|
testEnabled: () => getCurrentEditor() != null,
|
||||||
|
onClick: () => getCurrentEditor().refresh(),
|
||||||
|
});
|
||||||
|
|
||||||
|
registerFileCommands({
|
||||||
|
idPrefix: 'perspective',
|
||||||
|
category: 'Perspective',
|
||||||
|
getCurrentEditor,
|
||||||
|
folder: 'perspectives',
|
||||||
|
format: 'json',
|
||||||
|
fileExtension: 'perspective',
|
||||||
|
|
||||||
|
undoRedo: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const allowAddToFavorites = props => true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { createPerspectiveConfig, PerspectiveCache } from 'dbgate-datalib';
|
||||||
|
|
||||||
|
import PerspectiveView from '../perspectives/PerspectiveView.svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import registerCommand from '../commands/registerCommand';
|
||||||
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
|
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||||
|
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||||
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
|
import { useConnectionInfo } from '../utility/metadataLoaders';
|
||||||
|
import { extensions } from '../stores';
|
||||||
|
import invalidateCommands from '../commands/invalidateCommands';
|
||||||
|
import useEditorData from '../query/useEditorData';
|
||||||
|
import createUndoReducer from '../utility/createUndoReducer';
|
||||||
|
import { registerFileCommands } from '../commands/stdCommands';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import ToolStripSaveButton from '../buttons/ToolStripSaveButton.svelte';
|
||||||
|
|
||||||
|
export let tabid;
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let schemaName;
|
||||||
|
export let pureName;
|
||||||
|
|
||||||
|
export const activator = createActivator('PerspectiveTab', true);
|
||||||
|
|
||||||
|
$: connection = useConnectionInfo({ conid });
|
||||||
|
$: driver = findEngineDriver($connection, $extensions);
|
||||||
|
|
||||||
|
$: setEditorData($modelState.value);
|
||||||
|
|
||||||
|
export function getTabId() {
|
||||||
|
return tabid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getData() {
|
||||||
|
return $editorState.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canUndo() {
|
||||||
|
return $modelState.canUndo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function undo() {
|
||||||
|
dispatchModel({ type: 'undo' });
|
||||||
|
invalidateCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canRedo() {
|
||||||
|
return $modelState.canRedo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redo() {
|
||||||
|
dispatchModel({ type: 'redo' });
|
||||||
|
invalidateCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { editorState, editorValue, setEditorData } = useEditorData({
|
||||||
|
tabid,
|
||||||
|
onInitialData: value => {
|
||||||
|
dispatchModel({ type: 'reset', value });
|
||||||
|
invalidateCommands();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [modelState, dispatchModel] = createUndoReducer(
|
||||||
|
createPerspectiveConfig({
|
||||||
|
schemaName,
|
||||||
|
pureName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const cache = new PerspectiveCache();
|
||||||
|
const loadedCounts = writable({});
|
||||||
|
|
||||||
|
export function refresh() {
|
||||||
|
cache.clear();
|
||||||
|
loadedCounts.set({});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ToolStripContainer>
|
||||||
|
<PerspectiveView
|
||||||
|
{conid}
|
||||||
|
{database}
|
||||||
|
{driver}
|
||||||
|
config={$modelState.value}
|
||||||
|
setConfig={(value, reload) => {
|
||||||
|
if (reload) {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
dispatchModel({
|
||||||
|
type: 'compute',
|
||||||
|
// useMerge: skipUndoChain,
|
||||||
|
compute: v => (_.isFunction(value) ? value(v) : value),
|
||||||
|
});
|
||||||
|
invalidateCommands();
|
||||||
|
|
||||||
|
// config.update(value);
|
||||||
|
// loadedCounts.set({});
|
||||||
|
}}
|
||||||
|
{cache}
|
||||||
|
{loadedCounts}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<svelte:fragment slot="toolstrip">
|
||||||
|
<ToolStripCommandButton command="perspective.refresh" />
|
||||||
|
<ToolStripCommandButton command="perspective.customJoin" />
|
||||||
|
<ToolStripSaveButton idPrefix="perspective" />
|
||||||
|
<ToolStripCommandButton command="perspective.undo" />
|
||||||
|
<ToolStripCommandButton command="perspective.redo" />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ToolStripContainer>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||||
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
||||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
import { getBoolSettingsValue, getIntSettingsValue } from '../settings/settingsTools';
|
||||||
|
|
||||||
export let tabid;
|
export let tabid;
|
||||||
export let conid;
|
export let conid;
|
||||||
@@ -142,13 +142,18 @@
|
|||||||
script: scriptToSql(driver, commands),
|
script: scriptToSql(driver, commands),
|
||||||
}));
|
}));
|
||||||
// console.log('deleteCascadesScripts', deleteCascadesScripts);
|
// console.log('deleteCascadesScripts', deleteCascadesScripts);
|
||||||
|
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
|
||||||
|
handleConfirmSql(sql);
|
||||||
|
} else {
|
||||||
showModal(ConfirmSqlModal, {
|
showModal(ConfirmSqlModal, {
|
||||||
sql,
|
sql,
|
||||||
onConfirm: sqlOverride => handleConfirmSql(sqlOverride || sql),
|
onConfirm: sqlOverride => handleConfirmSql(sqlOverride || sql),
|
||||||
engine: driver.engine,
|
engine: driver.engine,
|
||||||
deleteCascadesScripts,
|
deleteCascadesScripts,
|
||||||
|
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function canSave() {
|
export function canSave() {
|
||||||
return changeSetContainsChanges($changeSetStore?.value);
|
return changeSetContainsChanges($changeSetStore?.value);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
|
|||||||
import * as QueryDataTab from './QueryDataTab.svelte';
|
import * as QueryDataTab from './QueryDataTab.svelte';
|
||||||
import * as ConnectionTab from './ConnectionTab.svelte';
|
import * as ConnectionTab from './ConnectionTab.svelte';
|
||||||
import * as MapTab from './MapTab.svelte';
|
import * as MapTab from './MapTab.svelte';
|
||||||
|
import * as PerspectiveTab from './PerspectiveTab.svelte';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
TableDataTab,
|
TableDataTab,
|
||||||
@@ -54,4 +55,5 @@ export default {
|
|||||||
QueryDataTab,
|
QueryDataTab,
|
||||||
ConnectionTab,
|
ConnectionTab,
|
||||||
MapTab,
|
MapTab,
|
||||||
|
PerspectiveTab,
|
||||||
};
|
};
|
||||||
|
|||||||
24
packages/web/src/utility/useMultipleDatabaseInfo.ts
Normal file
24
packages/web/src/utility/useMultipleDatabaseInfo.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { derived, Readable } from 'svelte/store';
|
||||||
|
import { useDatabaseInfo } from './metadataLoaders';
|
||||||
|
import { MultipleDatabaseInfo } from 'dbgate-datalib';
|
||||||
|
|
||||||
|
export function useMultipleDatabaseInfo(dbs: { conid: string; database: string }[]): Readable<MultipleDatabaseInfo> {
|
||||||
|
return derived(
|
||||||
|
dbs.map(db => useDatabaseInfo(db)),
|
||||||
|
values => {
|
||||||
|
let res = {};
|
||||||
|
for (let i = 0; i < dbs.length; i++) {
|
||||||
|
const { conid, database } = dbs[i];
|
||||||
|
const dbInfo = values[i];
|
||||||
|
res = {
|
||||||
|
...res,
|
||||||
|
[conid]: {
|
||||||
|
...res[conid],
|
||||||
|
[database]: dbInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
const queryFiles = useFiles({ folder: 'query' });
|
const queryFiles = useFiles({ folder: 'query' });
|
||||||
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
||||||
const diagramFiles = useFiles({ folder: 'diagrams' });
|
const diagramFiles = useFiles({ folder: 'diagrams' });
|
||||||
|
const perspectiveFiles = useFiles({ folder: 'perspectives' });
|
||||||
|
|
||||||
$: files = [
|
$: files = [
|
||||||
...($sqlFiles || []),
|
...($sqlFiles || []),
|
||||||
@@ -29,10 +30,13 @@
|
|||||||
...($queryFiles || []),
|
...($queryFiles || []),
|
||||||
...($sqliteFiles || []),
|
...($sqliteFiles || []),
|
||||||
...($diagramFiles || []),
|
...($diagramFiles || []),
|
||||||
|
...($perspectiveFiles || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleRefreshFiles() {
|
function handleRefreshFiles() {
|
||||||
apiCall('files/refresh', { folders: ['sql', 'shell', 'markdown', 'charts', 'query', 'sqlite', 'diagrams'] });
|
apiCall('files/refresh', {
|
||||||
|
folders: ['sql', 'shell', 'markdown', 'charts', 'query', 'sqlite', 'diagrams', 'perspectives'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate, windowsDomnain }) {
|
async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate, windowsDomain }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const connectionOptions = {
|
const connectionOptions = {
|
||||||
encrypt: !!ssl,
|
encrypt: !!ssl,
|
||||||
@@ -43,11 +43,11 @@ async function tediousConnect({ server, port, user, password, database, ssl, tru
|
|||||||
server,
|
server,
|
||||||
|
|
||||||
authentication: {
|
authentication: {
|
||||||
type: windowsDomnain ? 'ntlm' : 'default',
|
type: windowsDomain ? 'ntlm' : 'default',
|
||||||
options: {
|
options: {
|
||||||
userName: user,
|
userName: user,
|
||||||
password: password,
|
password: password,
|
||||||
...(windowsDomnain ? { domain: windowsDomnain } : {}),
|
...(windowsDomain ? { domain: windowsDomain } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class Dumper extends SqlDumper {
|
|||||||
this.endCommand();
|
this.endCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoIncrement() {}
|
||||||
|
|
||||||
specialColumnOptions(column) {
|
specialColumnOptions(column) {
|
||||||
if (column.isUnsigned) {
|
if (column.isUnsigned) {
|
||||||
this.put('^unsigned ');
|
this.put('^unsigned ');
|
||||||
@@ -45,6 +47,9 @@ class Dumper extends SqlDumper {
|
|||||||
if (column.isZerofill) {
|
if (column.isZerofill) {
|
||||||
this.put('^zerofill ');
|
this.put('^zerofill ');
|
||||||
}
|
}
|
||||||
|
if (column.autoIncrement) {
|
||||||
|
this.put('^auto_increment ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
columnDefinition(col, options) {
|
columnDefinition(col, options) {
|
||||||
|
|||||||
Reference in New Issue
Block a user