From 1133112a667d29b8436a7d803f6b947bb9a92de2 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Mon, 27 May 2024 10:49:08 +0200 Subject: [PATCH] CB-5004 Do not spam server while typing query in Sql Editor (#2615) * CB-5004 adds debounce for sql editor script set and auto complete * CB-5004 adds correct debounce time for parse script + parses script before its execution * CB-5004 disable script buttons only for empty sql editor or script being executing * CB-5004 fix: executes script parsing before get sub query * CB-5004 reverts getHintProposals throttling * CB-5004 fix: force launch sql editor script actions and removes delay from format script button --------- Co-authored-by: Alexey Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../packages/core-utils/src/debounce.test.ts | 20 +++++++++++-- webapp/packages/core-utils/src/debounce.ts | 24 +++++++++++++++ .../src/SqlEditor/ISQLEditorData.ts | 3 +- .../src/SqlEditor/SQLEditorActions.tsx | 2 +- .../src/SqlEditor/useSqlEditor.ts | 29 +++++++++++-------- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/webapp/packages/core-utils/src/debounce.test.ts b/webapp/packages/core-utils/src/debounce.test.ts index 4dcaa9b79d..d2e8605b8f 100644 --- a/webapp/packages/core-utils/src/debounce.test.ts +++ b/webapp/packages/core-utils/src/debounce.test.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { debounce } from './debounce'; +import { debounce, debounceAsync } from './debounce'; // https://jestjs.io/docs/timer-mocks // Tell Jest to mock all timeout functions @@ -23,6 +23,22 @@ describe('Debounce', () => { // Fast-forward time jest.runAllTimers(); - expect(func).toBeCalledTimes(1); + expect(func).toHaveBeenCalledTimes(1); + }); +}); + +describe('DebounceAsync', () => { + test('function should be executed just once', async () => { + const func = jest.fn(() => Promise.resolve(true)); + const debouncedFunction = debounceAsync(func, 1000); + + debouncedFunction(); + debouncedFunction(); + debouncedFunction(); + + // Fast-forward time + jest.runAllTimers(); + + expect(func).toHaveBeenCalledTimes(1); }); }); diff --git a/webapp/packages/core-utils/src/debounce.ts b/webapp/packages/core-utils/src/debounce.ts index 7778c38326..c273340061 100644 --- a/webapp/packages/core-utils/src/debounce.ts +++ b/webapp/packages/core-utils/src/debounce.ts @@ -19,3 +19,27 @@ export function debounce any>(func: T, delay: numb }, delay); }; } + +export function debounceAsync Promise>(func: T, delay: number): T { + let timeoutId: NodeJS.Timeout | null; + + return function (this: any, ...args: Parameters): Promise> { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const context = this; + + return new Promise((resolve, reject) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(async () => { + try { + const result = await func.apply(context, args); + resolve(result); + } catch (error) { + reject(error); + } + }, delay); + }); + } as T; +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 830ce84b91..185938782d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -27,7 +27,6 @@ export interface ISQLEditorData { readonly cursorSegment: ISQLScriptSegment | undefined; readonly readonly: boolean; readonly editing: boolean; - readonly isLineScriptEmpty: boolean; readonly isScriptEmpty: boolean; readonly isDisabled: boolean; readonly isIncomingChanges: boolean; @@ -42,7 +41,7 @@ export interface ISQLEditorData { /** displays if last getHintProposals call ended with limit */ readonly hintsLimitIsMet: boolean; - updateParserScriptsThrottle(): Promise; + updateParserScriptsDebounced(): Promise; setScript(query: string): void; init(): void; destruct(): void; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx index 6eefe6ec8e..59ba5fd917 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQLEditorActions.tsx @@ -26,7 +26,7 @@ export const SQLEditorActions = observer(function SQLEditorActions({ data const styles = useS(style); const translate = useTranslate(); const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode); - const disabled = getComputed(() => data.isLineScriptEmpty || data.isDisabled); + const disabled = getComputed(() => data.isScriptEmpty || data.isDisabled); const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query); const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 1755d7dbb5..d7e9b87d62 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -15,7 +15,7 @@ import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dial import { NotificationService } from '@cloudbeaver/core-events'; import { SyncExecutor } from '@cloudbeaver/core-executor'; import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; -import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; +import { createLastPromiseGetter, debounceAsync, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils'; import type { ISqlEditorTabState } from '../ISqlEditorTabState'; import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures'; @@ -104,12 +104,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return this.dataSource?.isEditing() ?? false; }, - get isLineScriptEmpty(): boolean { - return !this.activeSegment?.query; - }, - get isScriptEmpty(): boolean { - return this.value === '' || this.parser.scripts.length === 0; + return this.value === ''; }, get isDisabled(): boolean { @@ -166,7 +162,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { untracked(() => { this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async () => { try { - await this.updateParserScriptsThrottle(); + await this.updateParserScriptsDebounced(); } catch {} }); }); @@ -205,13 +201,14 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.hintsLimitIsMet = hints.length >= MAX_HINTS_LIMIT; return hints; - }, 1000 / 3), + }, 300), async formatScript(): Promise { if (this.isDisabled || this.isScriptEmpty || !this.dataSource?.executionContext) { return; } + await this.updateParserScripts(); const query = this.value; const script = this.getExecutingQuery(false); @@ -238,6 +235,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { if (!isQuery || !isExecutable) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -269,6 +268,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { if (!isQuery || !isExecutable) { return; } + + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -286,6 +287,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { return; } + await this.updateParserScripts(); const query = this.getSubQuery(); try { @@ -362,9 +364,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { this.dataSource?.setScript(query); }, - updateParserScriptsThrottle: throttleAsync(async function updateParserScriptsThrottle() { + updateParserScriptsDebounced: debounceAsync(async function updateParserScriptsThrottle() { await data.updateParserScripts(); - }, 1000 / 2), + }, 2000), async updateParserScripts() { if (!this.dataSource?.hasFeature(ESqlDataSourceFeatures.script)) { @@ -402,7 +404,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { passEmpty?: boolean, passDisabled?: boolean, ): Promise { - if (!segment || (this.isDisabled && !passDisabled) || (!passEmpty && this.isLineScriptEmpty)) { + if (!segment || (this.isDisabled && !passDisabled) || (!passEmpty && this.isScriptEmpty)) { return; } @@ -433,6 +435,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { const projectId = this.dataSource?.executionContext?.projectId; const connectionId = this.dataSource?.executionContext?.connectionId; + await data.updateParserScripts(); + if (!projectId || !connectionId || this.cursor.begin !== this.cursor.end) { return this.getSubQuery(); } @@ -469,6 +473,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, }), { + getHintProposals: action.bound, formatScript: action.bound, executeQuery: action.bound, executeQueryNewTab: action.bound, @@ -507,7 +512,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { // ensure that cursor is in script boundaries data.setCursor(data.cursor.begin, data.cursor.end); data.parser.setScript(script); - data.updateParserScriptsThrottle().catch(() => {}); + data.updateParserScriptsDebounced().catch(() => {}); data.onUpdate.execute(); }, ],