mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
Merge branch 'develop'
This commit is contained in:
commit
289752c023
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
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn adjustPackageJson
|
||||
run: |
|
||||
yarn adjustPackageJson
|
||||
- name: yarn install
|
||||
run: |
|
||||
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
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn adjustPackageJson
|
||||
run: |
|
||||
yarn adjustPackageJson
|
||||
- name: yarn install
|
||||
run: |
|
||||
# yarn --version
|
||||
|
11
.github/workflows/run-tests.yaml
vendored
11
.github/workflows/run-tests.yaml
vendored
@ -31,6 +31,11 @@ jobs:
|
||||
run: |
|
||||
cd packages/filterparser
|
||||
yarn test:ci
|
||||
- name: Datalib (perspective) tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/datalib
|
||||
yarn test:ci
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
@ -43,6 +48,12 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/filterparser/result.json
|
||||
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:
|
||||
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
|
||||
- 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
|
||||
- FIXED: Fixed problem with SSE events on web version
|
||||
- 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": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "13.6.3",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron-builder-notarize": "^1.4.0"
|
||||
"electron": "17.4.10",
|
||||
"electron-builder": "23.1.0",
|
||||
"electron-builder-notarize": "^1.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"better-sqlite3": "7.5.0",
|
||||
"msnodesqlv8": "^2.4.4"
|
||||
"better-sqlite3": "7.6.2",
|
||||
"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,
|
||||
"version": "5.0.9",
|
||||
"version": "5.1.0-beta.3",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@ -36,6 +36,7 @@
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"adjustPackageJson": "node adjustPackageJson",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
|
@ -74,7 +74,7 @@
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"better-sqlite3": "7.5.0",
|
||||
"msnodesqlv8": "^2.4.4"
|
||||
"better-sqlite3": "7.6.2",
|
||||
"msnodesqlv8": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -173,6 +173,7 @@ async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
||||
// console.log(sql);
|
||||
const res = await driver.query(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid, ...res });
|
||||
} 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",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
"files": [
|
||||
@ -12,11 +14,14 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"dbgate-filterparser": "^5.0.0-alpha.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^5.0.0-alpha.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ export interface DisplayColumn {
|
||||
headerText: string;
|
||||
uniqueName: string;
|
||||
uniquePath: string[];
|
||||
notNull: boolean;
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
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 './GridConfig';
|
||||
export * from './PerspectiveConfig';
|
||||
export * from './PerspectiveTreeNode';
|
||||
export * from './TableGridDisplay';
|
||||
export * from './ViewGridDisplay';
|
||||
export * from './JslGridDisplay';
|
||||
@ -12,3 +14,7 @@ export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
export * from './CollectionGridDisplay';
|
||||
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",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -3,7 +3,7 @@ import moment from 'moment';
|
||||
|
||||
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 (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (value === true) return 'TRUE';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { parseFilter } from './parseFilter';
|
||||
const { parseFilter } = require('./parseFilter');
|
||||
|
||||
test('parse string', () => {
|
||||
const ast = parseFilter('"123"', 'string');
|
||||
|
@ -68,5 +68,9 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dmp.put(' ^and ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
case 'in':
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(' ^in (%,v)', condition.values);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,12 @@ export interface BetweenCondition {
|
||||
right: Expression;
|
||||
}
|
||||
|
||||
export interface InCondition {
|
||||
conditionType: 'in';
|
||||
expr: Expression;
|
||||
values: any[];
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| BinaryCondition
|
||||
| NotCondition
|
||||
@ -107,7 +113,8 @@ export type Condition =
|
||||
| LikeCondition
|
||||
| ExistsCondition
|
||||
| NotExistsCondition
|
||||
| BetweenCondition;
|
||||
| BetweenCondition
|
||||
| InCondition;
|
||||
|
||||
export interface Source {
|
||||
name?: NamedObjectInfo;
|
||||
|
@ -33,6 +33,7 @@
|
||||
"dependencies": {
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"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';
|
||||
|
||||
export function fullNameFromString(name) {
|
||||
@ -54,7 +55,10 @@ export function findObjectLike(
|
||||
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));
|
||||
}
|
||||
|
||||
@ -76,7 +80,7 @@ function columnsConstraintName(prefix: string, table: TableInfo, columns: Column
|
||||
|
||||
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
if (!table) return table;
|
||||
const res = _.cloneDeep(table);
|
||||
const res = _cloneDeep(table);
|
||||
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
|
||||
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 {
|
||||
pureName: string;
|
||||
schemaName?: string;
|
||||
contentHash?: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface ColumnReference {
|
||||
@ -31,7 +33,8 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
|
||||
export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
isUnique: boolean;
|
||||
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
// indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
indexType: string;
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
@ -43,8 +46,8 @@ export interface CheckInfo extends ConstraintInfo {
|
||||
export interface ColumnInfo extends NamedObjectInfo {
|
||||
pairingId?: string;
|
||||
columnName: string;
|
||||
notNull: boolean;
|
||||
autoIncrement: boolean;
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
dataType: string;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
@ -119,7 +122,7 @@ export interface DatabaseInfoObjects {
|
||||
}
|
||||
|
||||
export interface DatabaseInfo extends DatabaseInfoObjects {
|
||||
schemas: SchemaInfo[];
|
||||
schemas?: SchemaInfo[];
|
||||
engine?: string;
|
||||
defaultSchema?: string;
|
||||
}
|
||||
|
@ -57,6 +57,7 @@
|
||||
"dependencies": {
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"debug": "^4.3.4",
|
||||
"interval-operations": "^1.0.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"wellknown": "^0.5.0"
|
||||
|
@ -147,11 +147,11 @@ import { tick } from 'svelte';
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCurrentSettings()['defaultAction.connectionClick'] == 'connect') {
|
||||
if (getCurrentSettings()['defaultAction.connectionClick'] == 'openDetails') {
|
||||
handleOpenConnectionTab();
|
||||
} else {
|
||||
await tick();
|
||||
handleConnect();
|
||||
} else {
|
||||
handleOpenConnectionTab();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -44,6 +44,15 @@
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img table-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Drop table',
|
||||
isDrop: true,
|
||||
@ -133,6 +142,12 @@
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img view-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
{
|
||||
label: 'Drop view',
|
||||
isDrop: true,
|
||||
|
@ -65,6 +65,14 @@
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const perspectives: FileTypeHandler = {
|
||||
icon: 'img perspective',
|
||||
format: 'json',
|
||||
tabComponent: 'PerspectiveTab',
|
||||
folder: 'pesrpectives',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@ -73,10 +81,14 @@
|
||||
query,
|
||||
sqlite,
|
||||
diagrams,
|
||||
perspectives,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
export const createMatcher = ({ file }) => filter => filterName(filter, file);
|
||||
export const createMatcher =
|
||||
({ file }) =>
|
||||
filter =>
|
||||
filterName(filter, file);
|
||||
</script>
|
||||
|
||||
<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 lang="ts">
|
||||
import { changeSetToSql, createChangeSet } from 'dbgate-datalib';
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import { scriptToSql } from 'dbgate-sqltree';
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { extensions } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
|
@ -23,13 +23,16 @@
|
||||
export let filter;
|
||||
export let setFilter;
|
||||
export let showResizeSplitter = false;
|
||||
export let onFocusGrid;
|
||||
export let onGetReference;
|
||||
export let onFocusGrid = null;
|
||||
export let onGetReference = null;
|
||||
export let foreignKey = null;
|
||||
export let conid = null;
|
||||
export let database = null;
|
||||
export let driver = null;
|
||||
export let jslid = null;
|
||||
export let customCommandIcon = null;
|
||||
export let onCustomCommand = null;
|
||||
export let customCommandTooltip = null;
|
||||
|
||||
export let pureName = null;
|
||||
export let schemaName = null;
|
||||
@ -295,6 +298,11 @@
|
||||
class:isOk
|
||||
placeholder="Filter"
|
||||
/>
|
||||
{#if customCommandIcon && onCustomCommand}
|
||||
<InlineButton on:click={onCustomCommand} title={customCommandTooltip} narrow square>
|
||||
<FontIcon icon={customCommandIcon} />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
{#if conid && database && driver}
|
||||
{#if driver?.databaseEngineTypes?.includes('sql') && foreignKey}
|
||||
<InlineButton on:click={handleShowDictionary} narrow square>
|
||||
@ -320,6 +328,7 @@
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
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">
|
||||
import _, { isPlainObject, join } from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import { arrayToHexString, isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
||||
import { isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import CellValue from './CellValue.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
@ -101,49 +64,7 @@
|
||||
class:isFocusedColumn
|
||||
{style}
|
||||
>
|
||||
{#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}
|
||||
<CellValue {rowData} {value} {jsonParsedValue} />
|
||||
|
||||
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
|
||||
<span class="hint"
|
||||
@ -256,13 +177,6 @@
|
||||
color: var(--theme-font-3);
|
||||
margin-left: 5px;
|
||||
}
|
||||
.null {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
.value {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
|
||||
.autoFillMarker {
|
||||
width: 8px;
|
||||
|
@ -1194,7 +1194,7 @@
|
||||
// console.log('event', event.nativeEvent);
|
||||
}
|
||||
|
||||
if (event.keyCode == keycodes.f2) {
|
||||
if (event.keyCode == keycodes.f2 || event.keyCode == keycodes.enter) {
|
||||
// @ts-ignore
|
||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||
}
|
||||
@ -1257,8 +1257,10 @@
|
||||
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
|
||||
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
|
||||
case keycodes.downArrow:
|
||||
case keycodes.enter:
|
||||
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:
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
case keycodes.rightArrow:
|
||||
@ -1272,25 +1274,31 @@
|
||||
case keycodes.pageDown:
|
||||
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
|
||||
case keycodes.tab: {
|
||||
if (event.shiftKey) {
|
||||
if (currentCell[1] > 0) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
|
||||
}
|
||||
} else {
|
||||
if (currentCell[1] < columnSizes.realCount - 1) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] + 1, 0, event);
|
||||
}
|
||||
}
|
||||
return moveCurrentCellWithTabKey(event.shiftKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function moveCurrentCellWithTabKey(isShift) {
|
||||
if (!isRegularCell(currentCell)) return null;
|
||||
|
||||
if (isShift) {
|
||||
if (currentCell[1] > 0) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
|
||||
}
|
||||
} else {
|
||||
if (currentCell[1] < columnSizes.realCount - 1) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] + 1, 0, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setCellValue(cell, value) {
|
||||
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
|
||||
}
|
||||
@ -1431,10 +1439,24 @@
|
||||
selectAll: action.selectAll,
|
||||
};
|
||||
case 'close': {
|
||||
const [row, col] = currentCell || [];
|
||||
if (domFocusField) domFocusField.focus();
|
||||
// @ts-ignore
|
||||
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
|
||||
if (action.mode == 'enter' || action.mode == 'tab' || action.mode == 'shiftTab') {
|
||||
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);
|
||||
return {};
|
||||
}
|
||||
|
@ -36,18 +36,26 @@
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
onSetValue(parseCellValue(domEditor.value));
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
domEditor.blur();
|
||||
event.preventDefault();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
||||
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:
|
||||
if (isCtrlOrCommandKey(event)) {
|
||||
if (isChangedRef.get()) {
|
||||
onSetValue(parseCellValue(domEditor.value));
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let width;
|
||||
export let isFlex = false;
|
||||
</script>
|
||||
|
||||
<div style={`max-width: ${width}px`}>
|
||||
<div style={`max-width: ${width}px`} class:isFlex>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -12,4 +13,8 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
div.isFlex {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
@ -36,6 +36,7 @@
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
min-height: 22px;
|
||||
width: 10px;
|
||||
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 json': 'mdi mdi-code-json',
|
||||
'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 chevron-down': 'mdi mdi-chevron-down',
|
||||
@ -120,6 +123,7 @@
|
||||
'img warn': 'mdi mdi-alert color-icon-gold',
|
||||
'img info': 'mdi mdi-information color-icon-blue',
|
||||
// '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-folder': 'mdi mdi-database-outline color-icon-green',
|
||||
@ -171,6 +175,8 @@
|
||||
'img link': 'mdi mdi-link',
|
||||
'img filter': 'mdi mdi-filter',
|
||||
'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 type-string': 'mdi mdi-alphabetical color-icon-blue',
|
||||
|
@ -12,7 +12,7 @@
|
||||
...(allowChooseModel ? [{ label: '(DB Model)', value: '__model' }] : []),
|
||||
..._.sortBy(
|
||||
($connections || [])
|
||||
.filter(conn => (direction == 'target' ? !conn.isReadOnly : true))
|
||||
.filter(conn => !conn.unsaved && (direction == 'target' ? !conn.isReadOnly : true))
|
||||
.map(conn => ({
|
||||
value: conn._id,
|
||||
label: getConnectionLabel(conn),
|
||||
|
@ -23,4 +23,5 @@
|
||||
label={labelOverride || `${nodeType} `}
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
elementValue={value}
|
||||
/>
|
||||
|
@ -4,6 +4,7 @@
|
||||
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import _ from 'lodash';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
|
||||
setContext('json-tree-context-key', {});
|
||||
|
||||
@ -34,8 +35,17 @@
|
||||
if (!closest) return;
|
||||
const value = elementData.get(closest);
|
||||
|
||||
const res = [];
|
||||
|
||||
res.push({
|
||||
text: 'Copy JSON',
|
||||
onClick: () => {
|
||||
copyTextToClipboard(JSON.stringify(value, null, 2));
|
||||
},
|
||||
});
|
||||
|
||||
if (value && _.isArray(value)) {
|
||||
return {
|
||||
res.push({
|
||||
text: 'Open as data sheet',
|
||||
onClick: () => {
|
||||
openNewTab(
|
||||
@ -53,8 +63,9 @@
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -2,15 +2,19 @@
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import FormProvider from '../forms/FormProvider.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 newQuery from '../query/newQuery';
|
||||
import newQuery from '../query/newQuery';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
|
||||
export let script;
|
||||
export let onConfirm;
|
||||
export let skipConfirmSettingKey = null;
|
||||
|
||||
let dontAskAgain;
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
@ -21,6 +25,20 @@ import newQuery from '../query/newQuery';
|
||||
<AceEditor mode="javascript" readOnly value={script} />
|
||||
</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">
|
||||
<FormSubmit
|
||||
value="OK"
|
||||
|
@ -17,12 +17,13 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import _, { startsWith } from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import TemplatedCheckboxField from '../forms/TemplatedCheckboxField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import newQuery from '../query/newQuery';
|
||||
import SqlEditor from '../query/SqlEditor.svelte';
|
||||
@ -38,6 +39,9 @@
|
||||
export let engine;
|
||||
export let recreates;
|
||||
export let deleteCascadesScripts;
|
||||
export let skipConfirmSettingKey = null;
|
||||
|
||||
let dontAskAgain;
|
||||
|
||||
$: isRecreated = _.sum(_.values(recreates || {})) > 0;
|
||||
const values = writable({});
|
||||
@ -122,6 +126,20 @@
|
||||
</div>
|
||||
{/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">
|
||||
<FormSubmit
|
||||
value="OK"
|
||||
|
@ -59,6 +59,19 @@
|
||||
let submenuOffset;
|
||||
|
||||
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) {
|
||||
if (item.disabled) return;
|
||||
@ -70,7 +83,7 @@
|
||||
submenuOffset = hoverOffset;
|
||||
return;
|
||||
}
|
||||
dispatch('close');
|
||||
dispatchClose();
|
||||
if (onCloseParent) onCloseParent();
|
||||
if (item.onClick) item.onClick();
|
||||
}
|
||||
@ -84,13 +97,13 @@
|
||||
submenuOffset = hoverOffset;
|
||||
}, 500);
|
||||
|
||||
$: preparedItems = prepareMenuItems(items, { targetElement }, $commandsCustomized);
|
||||
$: preparedItems = prepareMenuItems(items, { targetElement, registerCloseHandler }, $commandsCustomized);
|
||||
|
||||
const handleClickOutside = event => {
|
||||
// if (element && !element.contains(event.target) && !event.defaultPrevented) {
|
||||
if (event.target.closest('ul.dropDownMenuMarker')) return;
|
||||
|
||||
dispatch('close');
|
||||
dispatchClose();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
@ -134,7 +147,7 @@
|
||||
{...submenuOffset}
|
||||
onCloseParent={() => {
|
||||
if (onCloseParent) onCloseParent();
|
||||
dispatch('close');
|
||||
dispatchClose();
|
||||
}}
|
||||
/>
|
||||
{/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={[
|
||||
{ label: 'General', slot: 1 },
|
||||
{ label: 'Themes', slot: 2 },
|
||||
{ label: 'Actions', slot: 3 },
|
||||
{ label: 'Default Actions', slot: 3 },
|
||||
{ label: 'Confirmations', slot: 4 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="1">
|
||||
@ -166,7 +167,7 @@ ORDER BY
|
||||
label="Connection click"
|
||||
name="defaultAction.connectionClick"
|
||||
isNative
|
||||
defaultValue="openDetails"
|
||||
defaultValue="connect"
|
||||
options={[
|
||||
{ value: 'openDetails', label: 'Edit / open details' },
|
||||
{ value: 'connect', label: 'Connect' },
|
||||
@ -224,6 +225,15 @@ ORDER BY
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
</FormValues>
|
||||
|
||||
|
@ -25,8 +25,6 @@
|
||||
import {
|
||||
createChangeSet,
|
||||
createGridCache,
|
||||
createGridConfig,
|
||||
TableFormViewDisplay,
|
||||
CollectionGridDisplay,
|
||||
changeSetContainsChanges,
|
||||
runMacroOnChangeSet,
|
||||
@ -45,8 +43,6 @@
|
||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import EditJsonModal from '../modals/EditJsonModal.svelte';
|
||||
import ChangeSetGrider from '../datagrid/ChangeSetGrider';
|
||||
import { setContext } from 'svelte';
|
||||
import _ from 'lodash';
|
||||
import { apiCall } from '../utility/api';
|
||||
@ -54,6 +50,7 @@
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@ -119,11 +116,16 @@
|
||||
const driver = findEngineDriver($connection, $extensions);
|
||||
const script = driver.getCollectionUpdateScript ? driver.getCollectionUpdateScript(json) : null;
|
||||
if (script) {
|
||||
showModal(ConfirmNoSqlModal, {
|
||||
script,
|
||||
onConfirm: () => handleConfirmChange(json),
|
||||
engine: display.engine,
|
||||
});
|
||||
if (getBoolSettingsValue('skipConfirm.collectionDataSave', false)) {
|
||||
handleConfirmChange(json);
|
||||
} else {
|
||||
showModal(ConfirmNoSqlModal, {
|
||||
script,
|
||||
onConfirm: () => handleConfirmChange(json),
|
||||
engine: display.engine,
|
||||
skipConfirmSettingKey: 'skipConfirm.collectionDataSave',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
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 ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import { getBoolSettingsValue, getIntSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@ -142,12 +142,17 @@
|
||||
script: scriptToSql(driver, commands),
|
||||
}));
|
||||
// console.log('deleteCascadesScripts', deleteCascadesScripts);
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql,
|
||||
onConfirm: sqlOverride => handleConfirmSql(sqlOverride || sql),
|
||||
engine: driver.engine,
|
||||
deleteCascadesScripts,
|
||||
});
|
||||
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
|
||||
handleConfirmSql(sql);
|
||||
} else {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql,
|
||||
onConfirm: sqlOverride => handleConfirmSql(sqlOverride || sql),
|
||||
engine: driver.engine,
|
||||
deleteCascadesScripts,
|
||||
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function canSave() {
|
||||
|
@ -25,6 +25,7 @@ import * as DbKeyDetailTab from './DbKeyDetailTab.svelte';
|
||||
import * as QueryDataTab from './QueryDataTab.svelte';
|
||||
import * as ConnectionTab from './ConnectionTab.svelte';
|
||||
import * as MapTab from './MapTab.svelte';
|
||||
import * as PerspectiveTab from './PerspectiveTab.svelte';
|
||||
|
||||
export default {
|
||||
TableDataTab,
|
||||
@ -54,4 +55,5 @@ export default {
|
||||
QueryDataTab,
|
||||
ConnectionTab,
|
||||
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 sqliteFiles = useFiles({ folder: 'sqlite' });
|
||||
const diagramFiles = useFiles({ folder: 'diagrams' });
|
||||
const perspectiveFiles = useFiles({ folder: 'perspectives' });
|
||||
|
||||
$: files = [
|
||||
...($sqlFiles || []),
|
||||
@ -29,10 +30,13 @@
|
||||
...($queryFiles || []),
|
||||
...($sqliteFiles || []),
|
||||
...($diagramFiles || []),
|
||||
...($perspectiveFiles || []),
|
||||
];
|
||||
|
||||
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>
|
||||
|
||||
|
@ -22,7 +22,7 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) {
|
||||
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) => {
|
||||
const connectionOptions = {
|
||||
encrypt: !!ssl,
|
||||
@ -43,11 +43,11 @@ async function tediousConnect({ server, port, user, password, database, ssl, tru
|
||||
server,
|
||||
|
||||
authentication: {
|
||||
type: windowsDomnain ? 'ntlm' : 'default',
|
||||
type: windowsDomain ? 'ntlm' : 'default',
|
||||
options: {
|
||||
userName: user,
|
||||
password: password,
|
||||
...(windowsDomnain ? { domain: windowsDomnain } : {}),
|
||||
...(windowsDomain ? { domain: windowsDomain } : {}),
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -38,6 +38,8 @@ class Dumper extends SqlDumper {
|
||||
this.endCommand();
|
||||
}
|
||||
|
||||
autoIncrement() {}
|
||||
|
||||
specialColumnOptions(column) {
|
||||
if (column.isUnsigned) {
|
||||
this.put('^unsigned ');
|
||||
@ -45,6 +47,9 @@ class Dumper extends SqlDumper {
|
||||
if (column.isZerofill) {
|
||||
this.put('^zerofill ');
|
||||
}
|
||||
if (column.autoIncrement) {
|
||||
this.put('^auto_increment ');
|
||||
}
|
||||
}
|
||||
|
||||
columnDefinition(col, options) {
|
||||
|
Loading…
Reference in New Issue
Block a user