SYNC: Merge pull request #80 from dbgate/feature/loading-fix

This commit is contained in:
Jan Prochazka
2026-03-10 11:16:21 +01:00
committed by Diflow
parent 066f2baa03
commit a11b93b4cc
21 changed files with 1602 additions and 1381 deletions
@@ -95,10 +95,12 @@ module.exports = {
}
},
handle_response(conid, database, { msgid, ...response }) {
const [resolve, reject, additionalData] = this.requests[msgid];
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
const [resolve, reject, additionalData] = this.requests[msgid] || [];
if (resolve) {
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
}
}
delete this.requests[msgid];
},
@@ -239,7 +241,7 @@ module.exports = {
sendRequest(conn, message, additionalData = {}) {
const msgid = crypto.randomUUID();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject, additionalData];
this.requests[msgid] = [resolve, reject, additionalData, conn.conid, conn.database];
try {
const serializedMessage = serializeJsTypesForJsonStringify({ msgid, ...message });
conn.subprocess.send(serializedMessage);
@@ -264,12 +266,12 @@ module.exports = {
},
sqlSelect_meta: true,
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
async sqlSelect({ conid, database, select, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(
opened,
{ msgtype: 'sqlSelect', select },
{ msgtype: 'sqlSelect', select, commandTimeout },
{
auditLogger:
auditLogSessionGroup && select?.from?.name?.pureName
@@ -344,9 +346,12 @@ module.exports = {
},
collectionData_meta: true,
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
async collectionData({ conid, database, options, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
if (commandTimeout && options) {
options.commandTimeout = commandTimeout;
}
const res = await this.sendRequest(
opened,
{ msgtype: 'collectionData', options },
@@ -580,6 +585,24 @@ module.exports = {
};
},
pingDatabases_meta: true,
async pingDatabases({ databases }, req) {
if (!databases || !Array.isArray(databases)) return { status: 'ok' };
for (const { conid, database } of databases) {
if (!conid || !database) continue;
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
try {
existing.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00000 Error pinging DB connection');
this.close(conid, database);
}
}
}
return { status: 'ok' };
},
refresh_meta: true,
async refresh({ conid, database, keepOpen }, req) {
await testConnectionPermission(conid, req);
@@ -622,6 +645,15 @@ module.exports = {
structure: existing.structure,
};
socket.emitChanged(`database-status-changed`, { conid, database });
// Reject all pending requests for this connection
for (const [msgid, entry] of Object.entries(this.requests)) {
const [resolve, reject, additionalData, reqConid, reqDatabase] = entry;
if (reqConid === conid && reqDatabase === database) {
reject('DBGM-00000 Database connection closed');
delete this.requests[msgid];
}
}
}
},
@@ -234,12 +234,12 @@ async function handleRunOperation({ msgid, operation, useTransaction }, skipRead
}
}
async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) {
async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadonlyCheck = false) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
const res = await driver.query(dbhan, sql, { range });
const res = await driver.query(dbhan, sql, { range, commandTimeout });
process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
} catch (err) {
process.send({
@@ -250,11 +250,11 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false)
}
}
async function handleSqlSelect({ msgid, select }) {
async function handleSqlSelect({ msgid, select, commandTimeout }) {
const driver = requireEngineDriver(storedConnection);
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
return handleQueryData({ msgid, sql: dmp.s, range: select.range }, true);
return handleQueryData({ msgid, sql: dmp.s, range: select.range, commandTimeout }, true);
}
async function handleDriverDataCore(msgid, callMethod, { logName }) {
+1
View File
@@ -59,6 +59,7 @@ export interface QueryOptions {
importSqlDump?: boolean;
range?: { offset: number; limit: number };
readonly?: boolean;
commandTimeout?: number;
}
export interface WriteTableOptions {
@@ -1,286 +1,307 @@
<script context="module" lang="ts">
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
id: 'collectionDataGrid.openQuery',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'collectionDataGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
keyText: 'CtrlOrCommand+E',
icon: 'icon export',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
function buildConditionForGrid(props) {
const filters = props?.display?.config?.filters;
const filterBehaviour =
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
const conditions = [];
for (const uniqueName in filters || {}) {
if (!filters[uniqueName]) continue;
try {
const ast = parseFilter(filters[uniqueName], filterBehaviour);
// console.log('AST', ast);
const cond = _.cloneDeepWith(ast, expr => {
if (expr.exprType == 'placeholder') {
return {
exprType: 'column',
columnName: uniqueName,
};
}
// if (expr.__placeholder__) {
// return {
// [uniqueName]: expr.__placeholder__,
// };
// }
});
conditions.push(cond);
} catch (err) {
// error in filter
}
}
return conditions.length > 0
? {
conditionType: 'and',
conditions,
}
: undefined;
}
function buildSortForGrid(props) {
const sort = props?.display?.config?.sort;
if (sort?.length > 0) {
return sort.map(col => ({
columnName: col.uniqueName,
direction: col.order,
}));
}
return null;
}
export async function loadCollectionDataPage(props, offset, limit) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
limit,
skip: offset,
condition: buildConditionForGrid(props),
sort: buildSortForGrid(props),
},
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
return true;
// const { display } = props;
// const sql = display.getPageQuery(0, 1);
// return !!sql;
}
async function loadRowCount(props) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
});
return response.count;
}
</script>
<script lang="ts">
import { parseFilter } from 'dbgate-filterparser';
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
import { openImportExportTab } from '../utility/importExportTools';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let setLoadedRows = null;
export let onPublishedCellsChanged;
// export let onChangeGrider = undefined;
let loadedRows = [];
let publishedCells = [];
export const activator = createActivator('CollectionDataGridCore', false);
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
function getExportQuery() {
return display?.driver?.getCollectionExportQueryScript?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return `db.collection('${pureName}')
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
}
function getExportQueryJson() {
return display?.driver?.getCollectionExportQueryJson?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return {
// collection: pureName,
// condition: buildConditionForGrid($$props) || {},
// sort: buildMongoSort($$props) || {},
// };
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(getExportQueryJson(), undefined, 2)
: getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = [pureName];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
conid,
database,
},
},
{
editor: getExportQuery(),
}
);
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
createQuickExportMenu(
quickExportHandler,
{
command: 'collectionDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
if (setLoadedRows) setLoadedRows(rows);
}
</script>
<LoadingDataGridCore
{...$$props}
loadDataPage={loadCollectionDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
onOpenQuery={openQuery}
{grider}
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
id: 'collectionDataGrid.openQuery',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'collectionDataGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
keyText: 'CtrlOrCommand+E',
icon: 'icon export',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
function buildConditionForGrid(props) {
const filters = props?.display?.config?.filters;
const filterBehaviour =
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
const conditions = [];
for (const uniqueName in filters || {}) {
if (!filters[uniqueName]) continue;
try {
const ast = parseFilter(filters[uniqueName], filterBehaviour);
// console.log('AST', ast);
const cond = _.cloneDeepWith(ast, expr => {
if (expr.exprType == 'placeholder') {
return {
exprType: 'column',
columnName: uniqueName,
};
}
// if (expr.__placeholder__) {
// return {
// [uniqueName]: expr.__placeholder__,
// };
// }
});
conditions.push(cond);
} catch (err) {
// error in filter
}
}
return conditions.length > 0
? {
conditionType: 'and',
conditions,
}
: undefined;
}
function buildSortForGrid(props) {
const sort = props?.display?.config?.sort;
if (sort?.length > 0) {
return sort.map(col => ({
columnName: col.uniqueName,
direction: col.order,
}));
}
return null;
}
export async function loadCollectionDataPage(props, offset, limit) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
limit,
skip: offset,
condition: buildConditionForGrid(props),
sort: buildSortForGrid(props),
},
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
return true;
// const { display } = props;
// const sql = display.getPageQuery(0, 1);
// return !!sql;
}
async function loadRowCount(props) {
const { conid, database } = props;
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const response = await Promise.race([
apiCall('database-connections/collection-data', {
conid,
database,
commandTimeout: 3000,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
}),
timeoutPromise,
]);
if (response && typeof response === 'object' && (response as any).errorMessage) {
return { errorMessage: (response as any).errorMessage };
}
if (response && typeof response === 'object' && typeof (response as any).count === 'number') {
return (response as any).count;
}
return { errorMessage: 'Error loading row count' };
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<script lang="ts">
import { parseFilter } from 'dbgate-filterparser';
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
import { openImportExportTab } from '../utility/importExportTools';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let setLoadedRows = null;
export let onPublishedCellsChanged;
// export let onChangeGrider = undefined;
let loadedRows = [];
let publishedCells = [];
export const activator = createActivator('CollectionDataGridCore', false);
// $: console.log('loadedRows BIND', loadedRows);
$: grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
function getExportQuery() {
return display?.driver?.getCollectionExportQueryScript?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return `db.collection('${pureName}')
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
}
function getExportQueryJson() {
return display?.driver?.getCollectionExportQueryJson?.(
pureName,
buildConditionForGrid($$props),
buildSortForGrid($$props)
);
// return {
// collection: pureName,
// condition: buildConditionForGrid($$props) || {},
// sort: buildMongoSort($$props) || {},
// };
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(getExportQueryJson(), undefined, 2)
: getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = [pureName];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery() {
openNewTab(
{
title: 'Query #',
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
conid,
database,
},
},
{
editor: getExportQuery(),
}
);
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
createQuickExportMenu(
quickExportHandler,
{
command: 'collectionDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
if (setLoadedRows) setLoadedRows(rows);
}
</script>
<LoadingDataGridCore
{...$$props}
loadDataPage={loadCollectionDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
onOpenQuery={openQuery}
{grider}
/>
@@ -461,6 +461,8 @@
export let frameSelection = undefined;
export let isLoading = false;
export let allRowCount = undefined;
export let allRowCountError = undefined;
export let onReloadRowCount = undefined;
export let onReferenceSourceChanged = undefined;
export let onPublishedCellsChanged = undefined;
export let onReferenceClick = undefined;
@@ -2400,6 +2402,15 @@
<div class="row-count-label">
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {allRowCount.toLocaleString()}
</div>
{:else if allRowCountError && multipleGridsOnTab}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="row-count-label row-count-error"
title={allRowCountError}
on:click={onReloadRowCount}
>
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}
</div>
{/if}
{#if isLoading}
@@ -2408,6 +2419,13 @@
{#if !tabControlHiddenTab && !multipleGridsOnTab && allRowCount != null}
<StatusBarTabItem text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${allRowCount.toLocaleString()}`} />
{:else if !tabControlHiddenTab && !multipleGridsOnTab && allRowCountError}
<StatusBarTabItem
text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}`}
title={allRowCountError}
clickable
onClick={onReloadRowCount}
/>
{/if}
</div>
{/if}
@@ -2472,6 +2490,15 @@
opacity: 1;
}
.row-count-error {
cursor: pointer;
color: var(--theme-font-3);
}
.row-count-error:hover {
text-decoration: underline;
}
.selection-menu {
position: absolute;
background-color: var(--theme-datagrid-corner-label-background);
@@ -25,6 +25,7 @@
let isLoadedAll = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
let domGrid;
@@ -37,8 +38,14 @@
}
const handleLoadRowCount = async () => {
const rowCount = await loadRowCount($$props);
allRowCount = rowCount;
const result = await loadRowCount($$props);
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
};
async function loadNextData() {
@@ -103,6 +110,7 @@
function reload() {
allRowCount = null;
allRowCountError = null;
isLoading = false;
loadedRows = [];
isLoadedAll = false;
@@ -132,6 +140,8 @@
{errorMessage}
{isLoading}
allRowCount={rowCountLoaded || allRowCount}
allRowCountError={allRowCountError}
onReloadRowCount={handleLoadRowCount}
{isLoadedAll}
{loadedTime}
{grider}
+250 -235
View File
@@ -2,238 +2,253 @@
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import hasPermission from '../utility/hasPermission';
import { __t, _t } from '../translations'
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openQuery',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'sqlDataGrid.export',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('common.export', { defaultMessage : 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
onClick: () => getCurrentEditor().exportGrid(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte'; import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte'; import { openImportExportTab } from '../utility/importExportTools';
import { getIntSettingsValue } from '../settings/settingsTools';
import OverlayDiffGrider from './OverlayDiffGrider';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let overlayDefinition = null;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
let publishedCells = [];
// export let onChangeGrider = undefined;
export const activator = createActivator('SqlDataGridCore', false);
let loadedRows = [];
let grider;
// $: console.log('loadedRows BIND', loadedRows);
$: {
if (!overlayDefinition && macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
}
}
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
$: {
if (!overlayDefinition && !macroPreview) {
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
}
}
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
$: {
if (overlayDefinition) {
grider = new OverlayDiffGrider(
loadedRows,
display,
overlayDefinition.matchColumns,
overlayDefinition.overlayData,
overlayDefinition.matchedDbKeys
);
}
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
: display.getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery(sql?) {
openNewTab(
{
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
schemaName: display.baseTableOrSimilar?.schemaName,
pureName: display.baseTableOrSimilar?.pureName,
conid,
database,
},
},
{
editor: sql ?? display.getExportQuery(),
}
);
}
function openQueryOnError() {
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu(
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'sqlDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
}
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const select = display.getPageQuery(offset, limit);
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const select = display.getPageQuery(0, 1);
return !!select;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const select = display.getCountQuery();
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
});
return parseInt(response.rows[0].count);
}
</script>
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
{grider}
{display}
onOpenQuery={openQuery}
onOpenQueryOnError={openQueryOnError}
/>
import { __t, _t } from '../translations'
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openQuery',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
onClick: () => getCurrentEditor().openQuery(),
});
registerCommand({
id: 'sqlDataGrid.export',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('common.export', { defaultMessage : 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
onClick: () => getCurrentEditor().exportGrid(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
import { getIntSettingsValue } from '../settings/settingsTools';
import OverlayDiffGrider from './OverlayDiffGrider';
export let conid;
export let display;
export let database;
export let schemaName;
export let pureName;
export let config;
export let changeSetState;
export let dispatchChangeSet;
export let overlayDefinition = null;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
let publishedCells = [];
// export let onChangeGrider = undefined;
export const activator = createActivator('SqlDataGridCore', false);
let loadedRows = [];
let grider;
// $: console.log('loadedRows BIND', loadedRows);
$: {
if (!overlayDefinition && macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells
);
}
}
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
$: {
if (!overlayDefinition && !macroPreview) {
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
}
}
// $: console.log('GRIDER', grider);
// $: if (onChangeGrider) onChangeGrider(grider);
$: {
if (overlayDefinition) {
grider = new OverlayDiffGrider(
loadedRows,
display,
overlayDefinition.matchColumns,
overlayDefinition.overlayData,
overlayDefinition.matchedDbKeys
);
}
}
export async function exportGrid() {
const coninfo = await getConnectionInfo({ conid });
const initialValues: any = {};
initialValues.sourceStorageType = 'query';
initialValues.sourceConnectionId = conid;
initialValues.sourceDatabaseName = database;
initialValues.sourceQuery = coninfo.isReadOnly
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
: display.getExportQuery();
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
export function openQuery(sql?) {
openNewTab(
{
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
icon: 'img sql-file',
tabComponent: 'QueryTab',
focused: true,
props: {
schemaName: display.baseTableOrSimilar?.schemaName,
pureName: display.baseTableOrSimilar?.pureName,
conid,
database,
},
},
{
editor: sql ?? display.getExportQuery(),
}
);
}
function openQueryOnError() {
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
}
const quickExportHandler = fmt => async () => {
const coninfo = await getConnectionInfo({ conid });
exportQuickExportFile(
pureName || 'Data',
{
functionName: 'queryReader',
props: {
...extractShellConnectionHostable(coninfo, database),
queryType: coninfo.isReadOnly ? 'json' : 'native',
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
},
hostConnection: extractShellHostConnection(coninfo, database),
},
fmt,
display.getExportColumnMap()
);
};
registerQuickExportHandler(quickExportHandler);
registerMenu(
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'sqlDataGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
}
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const select = display.getPageQuery(offset, limit);
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-grid',
});
if (response.errorMessage) return response;
return response.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const select = display.getPageQuery(0, 1);
return !!select;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const select = display.getCountQuery();
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const response = await Promise.race([
apiCall('database-connections/sql-select', {
conid,
database,
select,
commandTimeout: 3000,
}),
timeoutPromise,
]);
if (response.errorMessage) return { errorMessage: response.errorMessage };
return parseInt(response.rows[0].count);
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
frameSelection={!!macroPreview}
{grider}
{display}
onOpenQuery={openQuery}
onOpenQueryOnError={openQueryOnError}
/>
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,7 @@
let isLoadedCount = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
const handleLoadCurrentRow = async () => {
@@ -38,7 +39,14 @@
const handleLoadRowCount = async () => {
isLoadingCount = true;
allRowCount = await loadRowCountFunc();
const result = await loadRowCountFunc();
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
isLoadedCount = true;
isLoadingCount = false;
};
@@ -55,6 +63,7 @@
rowData = null;
loadedTime = new Date().getTime();
allRowCount = null;
allRowCountError = null;
errorMessage = null;
}
@@ -82,4 +91,4 @@
$: if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
</script>
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} onNavigate={handleNavigate} />
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} {allRowCountError} onReloadRowCount={handleLoadRowCount} onNavigate={handleNavigate} />
+45 -33
View File
@@ -1,35 +1,47 @@
<script lang="ts" context="module">
import { apiCall } from '../utility/api';
async function loadRow(props, select) {
const { conid, database } = props;
if (!select) return null;
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-form',
});
if (response.errorMessage) return response;
return response.rows[0];
}
</script>
<script lang="ts"> import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
}
async function handleLoadRowCount() {
const countRow = await loadRow($$props, display.getCountQuery());
return countRow ? parseInt(countRow.count) : null;
}
</script>
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
async function loadRow(props, select, options = {}) {
const { conid, database } = props;
if (!select) return null;
const response = await apiCall('database-connections/sql-select', {
conid,
database,
select,
auditLogSessionGroup: 'data-form',
...options,
});
if (response.errorMessage) return response;
return response.rows[0];
}
</script>
<script lang="ts">
import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
}
async function handleLoadRowCount() {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const countRow = await Promise.race([
loadRow($$props, display.getCountQuery(), { commandTimeout: 3000 }),
timeoutPromise,
]);
return countRow ? parseInt(countRow.count) : null;
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
+32 -1
View File
@@ -1,5 +1,11 @@
import _ from 'lodash';
import { currentDatabase, openedConnectionsWithTemporary, getCurrentConfig, getOpenedConnections } from '../stores';
import {
currentDatabase,
openedConnectionsWithTemporary,
getCurrentConfig,
getOpenedConnections,
getOpenedTabs,
} from '../stores';
import { apiCall, getVolatileConnections, strmid } from './api';
import hasPermission from '../utility/hasPermission';
import { getConfig } from './metadataLoaders';
@@ -37,9 +43,29 @@ const doDatabasePing = value => {
}
};
function pingAllOpenedDatabases() {
const tabs = getOpenedTabs() || [];
const allDbs = tabs
.filter(tab => !tab.closedTime && tab.props?.conid && tab.props?.database)
.map(tab => ({ conid: tab.props.conid as string, database: tab.props.database as string }));
const seen = new Set<string>();
const databases: { conid: string; database: string }[] = [];
for (const db of allDbs) {
const key = `${db.conid}/${db.database}`;
if (!seen.has(key)) {
seen.add(key);
databases.push(db);
}
}
if (databases.length > 0) {
apiCall('database-connections/ping-databases', { databases });
}
}
let openedConnectionsHandle = null;
let currentDatabaseHandle = null;
let allDatabasesHandle = null;
export function subscribeConnectionPingers() {
openedConnectionsWithTemporary.subscribe(value => {
@@ -53,6 +79,11 @@ export function subscribeConnectionPingers() {
if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle);
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
});
// Ping all databases that have open (non-closed) tabs, not just the current one
pingAllOpenedDatabases();
if (allDatabasesHandle) window.clearInterval(allDatabasesHandle);
allDatabasesHandle = window.setInterval(() => pingAllOpenedDatabases(), 20 * 1000);
}
export function callServerPing() {
+1 -1
View File
@@ -181,7 +181,7 @@
</div>
<div class="container">
{#each contextItems || [] as item}
<div class="item" class:clickable={item.clickable} on:click={item.onClick}>
<div class="item" class:clickable={item.clickable} on:click={item.onClick} title={item.title || null}>
{#if item.icon}
<FontIcon icon={item.icon} padRight />
{/if}
@@ -8,14 +8,15 @@
export let clickable = false;
export let icon = null;
export let onClick = null;
export let title = null;
const key = uuidv1();
const tabid = getContext('tabid');
onMount(() => {
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
});
onDestroy(() => updateStatuBarInfoItem(tabid, key, null));
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
</script>
@@ -71,14 +71,19 @@ const driver = {
// called for retrieve data (eg. browse in data grid) and for update database
async query(dbhan, query, options) {
const offset = options?.range?.offset;
const commandTimeout = options?.commandTimeout;
const executeOptions = {};
if (commandTimeout) {
executeOptions.readTimeout = parseInt(commandTimeout);
}
if (options?.discardResult) {
await dbhan.client.execute(query);
await dbhan.client.execute(query, [], executeOptions);
return {
rows: [],
columns: [],
};
}
const result = await dbhan.client.execute(query);
const result = await dbhan.client.execute(query, [], executeOptions);
if (!result.rows?.[0]) {
return {
rows: [],
@@ -25,6 +25,7 @@ const driver = {
},
// called for retrieve data (eg. browse in data grid) and for update database
async query(dbhan, query, options) {
const commandTimeout = options?.commandTimeout;
if (options?.discardResult) {
await dbhan.client.command({
query,
@@ -34,10 +35,14 @@ const driver = {
columns: [],
};
} else {
const resultSet = await dbhan.client.query({
const queryOptions = {
query,
format: 'JSONCompactEachRowWithNamesAndTypes',
});
};
if (commandTimeout) {
queryOptions.settings = { max_execution_time: Math.ceil(parseInt(commandTimeout) / 1000) };
}
const resultSet = await dbhan.client.query(queryOptions);
const dataSet = await resultSet.json();
if (!dataSet?.[0]) {
@@ -487,7 +487,11 @@ const drivers = driverBases.map((driverBase) => ({
const collection = dbhan.getDatabase().collection(options.pureName);
if (options.countDocuments) {
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {});
const countOptions = {};
if (options.commandTimeout) {
countOptions.maxTimeMS = parseInt(options.commandTimeout);
}
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {}, countOptions);
return { count };
} else if (options.aggregate) {
let cursor = await collection.aggregate(deserializeMongoData(convertToMongoAggregate(options.aggregate)));
@@ -119,7 +119,7 @@ async function tediousQueryCore(dbhan, sql, options) {
columns: [],
});
}
const { addDriverNativeColumn, discardResult } = options || {};
const { addDriverNativeColumn, discardResult, commandTimeout } = options || {};
return new Promise((resolve, reject) => {
const result = {
rows: [],
@@ -129,6 +129,9 @@ async function tediousQueryCore(dbhan, sql, options) {
if (err) reject(err);
else resolve(result);
});
if (commandTimeout) {
request.setTimeout(parseInt(commandTimeout));
}
request.on('columnMetadata', function (columns) {
result.columns = extractTediousColumns(columns, addDriverNativeColumn);
});
@@ -105,9 +105,18 @@ const drivers = driverBases.map(driverBase => ({
};
}
const commandTimeout = options?.commandTimeout;
const queryOptions = {};
if (commandTimeout) {
queryOptions.timeout = parseInt(commandTimeout);
}
return new Promise((resolve, reject) => {
dbhan.client.query(sql, function (error, results, fields) {
if (error) reject(error);
dbhan.client.query({ sql, ...queryOptions }, function (error, results, fields) {
if (error) {
reject(error);
return;
}
const columns = extractColumns(fields);
resolve({ rows: results && columns && results.map && results.map(row => modifyRow(zipDataRow(row, columns), columns)), columns });
});
@@ -107,7 +107,7 @@ const driver = {
async close(dbhan) {
return dbhan.client.close();
},
async query(dbhan, sql) {
async query(dbhan, sql, options) {
if (sql == null || sql.trim() == '') {
return {
rows: [],
@@ -120,7 +120,21 @@ const driver = {
sql = mtrim[1];
}
const res = await dbhan.client.execute(sql);
const commandTimeout = options?.commandTimeout;
let previousCallTimeout;
if (commandTimeout) {
previousCallTimeout = dbhan.client.callTimeout;
dbhan.client.callTimeout = parseInt(commandTimeout);
}
let res;
try {
res = await dbhan.client.execute(sql);
} finally {
if (commandTimeout) {
dbhan.client.callTimeout = previousCallTimeout || 0;
}
}
try {
const columns = extractOracleColumns(res.metaData);
return { rows: (res.rows || []).map(row => modifyRow(zipDataRow(row, columns), columns)), columns };
@@ -178,14 +178,25 @@ const drivers = driverBases.map(driverBase => ({
async close(dbhan) {
return dbhan.client.end();
},
async query(dbhan, sql) {
async query(dbhan, sql, options) {
if (sql == null) {
return {
rows: [],
columns: [],
};
}
const res = await dbhan.client.query({ text: sql, rowMode: 'array' });
const commandTimeout = options?.commandTimeout;
if (commandTimeout) {
await dbhan.client.query({ text: `SET statement_timeout = ${parseInt(commandTimeout)}` });
}
let res;
try {
res = await dbhan.client.query({ text: sql, rowMode: 'array' });
} finally {
if (commandTimeout) {
await dbhan.client.query({ text: 'SET statement_timeout = 0' }).catch(() => {});
}
}
const columns = extractPostgresColumns(res, dbhan);
const transormableTypeNames = Object.values(dbhan.typeIdToName ?? {});
+5
View File
@@ -7247,6 +7247,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-to-image@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
dotenv@^16.0.0:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"