Merge branch 'develop'

This commit is contained in:
Jan Prochazka 2022-08-08 19:46:39 +02:00
commit 289752c023
77 changed files with 9903 additions and 4424 deletions

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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"
]
}

View File

@ -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
View 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');

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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) {

View File

@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
};

View File

@ -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"
}
}

View File

@ -24,7 +24,7 @@ export interface DisplayColumn {
headerText: string;
uniqueName: string;
uniquePath: string[];
notNull: boolean;
notNull?: boolean;
autoIncrement?: boolean;
isPrimaryKey?: boolean;
foreignKey?: ForeignKeyInfo;

View 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 = {};
}
}

View 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;
};
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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;
}

View 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]];
}

View File

@ -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';

View 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],
})
);
});

View 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'],
},
];

View 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'],
},
];

View 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'],
},
];

File diff suppressed because it is too large Load Diff

View File

@ -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": {

View File

@ -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';

View File

@ -1,4 +1,4 @@
import { parseFilter } from './parseFilter';
const { parseFilter } = require('./parseFilter');
test('parse string', () => {
const ast = parseFilter('"123"', 'string');

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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"

View File

@ -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}`;
}

View File

@ -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;
}

View File

@ -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"

View File

@ -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();
}
};

View File

@ -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,

View File

@ -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">

View 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>

View File

@ -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';

View File

@ -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 {

View File

@ -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;

View File

@ -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 {};
}

View File

@ -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();

View File

@ -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>

View File

@ -36,6 +36,7 @@
input {
flex: 1;
min-width: 10px;
min-height: 22px;
width: 10px;
border: none;
}

View 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>

View File

@ -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',

View File

@ -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),

View File

@ -23,4 +23,5 @@
label={labelOverride || `${nodeType} `}
bracketOpen={'{'}
bracketClose={'}'}
elementValue={value}
/>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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}

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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}

View 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>

View 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,
}),
},
];
}

View File

@ -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>

View File

@ -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);
}

View 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>

View File

@ -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() {

View File

@ -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,
};

View 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;
}
);
}

View File

@ -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>

View File

@ -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 } : {}),
},
},

View File

@ -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) {

7019
yarn.lock

File diff suppressed because it is too large Load Diff