- {renderTabs()}
{props.error ?
: null}
+ {renderResults()}
{currentResult ? (
- {renderResultHeadWithCount()}
(
+ QUERY_STOPPED_BANNER_CLOSED_KEY,
+ );
+
+ const closeBanner = React.useCallback(() => {
+ setIsQueryStoppedBannerClosed(true);
+ }, [setIsQueryStoppedBannerClosed]);
+
+ return isQueryStoppedBannerClosed ? null : (
+ {i18n('banner.query-stopped.message')}}
+ layout="horizontal"
+ actions={
+
+
+ {i18n('banner.query-stopped.never-show')}
+
+
+ }
+ />
+ );
+}
diff --git a/src/containers/Tenant/Query/i18n/en.json b/src/containers/Tenant/Query/i18n/en.json
index 5e5f6e70dd..d036e3db6f 100644
--- a/src/containers/Tenant/Query/i18n/en.json
+++ b/src/containers/Tenant/Query/i18n/en.json
@@ -43,19 +43,23 @@
"statistics-mode-description.full": "Collect statistics and query plan",
"statistics-mode-description.profile": "Collect statistics for individual tasks",
- "query-duration.description": "Duration of server-side query execution",
-
"action.send-query": "Send query",
"action.send-selected-query": "Send selected query",
"action.previous-query": "Previous query in history",
"action.next-query": "Next query in history",
"action.save-query": "Save query",
"action.stop": "Stop",
+ "action.run": "Run",
+ "action.explain": "Explain",
"filter.text.placeholder": "Search by query text...",
"gear.tooltip": "Query execution settings have been changed for ",
"banner.query-settings.message": "Query was executed with modified settings: ",
+ "banner.query-stopped.message": "Data is not up to date because the request was not completed.",
+ "banner.query-stopped.never-show": "Never show again",
+
+ "toaster.stop-error": "Something went wrong. Unable to stop request processing. Please wait.",
"history.queryText": "Query text",
"history.endTime": "End time",
diff --git a/src/services/settings.ts b/src/services/settings.ts
index a33095d7f7..594ebf70f1 100644
--- a/src/services/settings.ts
+++ b/src/services/settings.ts
@@ -17,6 +17,7 @@ import {
PARTITIONS_HIDDEN_COLUMNS_KEY,
QUERY_EXECUTION_SETTINGS_KEY,
QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY,
+ QUERY_STOPPED_BANNER_CLOSED_KEY,
SAVED_QUERIES_KEY,
SHOW_DOMAIN_DATABASE_KEY,
TENANT_INITIAL_PAGE_KEY,
@@ -51,6 +52,7 @@ export const DEFAULT_USER_SETTINGS = {
[AUTO_REFRESH_INTERVAL]: 0,
[CASE_SENSITIVE_JSON_SEARCH]: false,
[SHOW_DOMAIN_DATABASE_KEY]: false,
+ [QUERY_STOPPED_BANNER_CLOSED_KEY]: false,
[LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined,
[QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined,
[QUERY_EXECUTION_SETTINGS_KEY]: DEFAULT_QUERY_SETTINGS,
diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts
index ee45dfec3d..1e3bff38c4 100644
--- a/src/store/configureStore.ts
+++ b/src/store/configureStore.ts
@@ -28,13 +28,17 @@ function _configureStore<
preloadedState,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
- immutableCheck: {
- ignoredPaths: ['tooltip.currentHoveredRef'],
- },
- serializableCheck: {
- ignoredPaths: ['tooltip.currentHoveredRef', 'api'],
- ignoredActions: [UPDATE_REF, 'api/sendQuery/rejected'],
- },
+ immutableCheck: process.env.REACT_APP_DISABLE_CHECKS
+ ? false
+ : {
+ ignoredPaths: ['tooltip.currentHoveredRef'],
+ },
+ serializableCheck: process.env.REACT_APP_DISABLE_CHECKS
+ ? false
+ : {
+ ignoredPaths: ['tooltip.currentHoveredRef', 'api'],
+ ignoredActions: [UPDATE_REF, 'api/sendQuery/rejected'],
+ },
}).concat(locationMiddleware, ...middleware),
});
diff --git a/src/store/reducers/query/query.ts b/src/store/reducers/query/query.ts
index 1b5e6d3e50..0362dc5390 100644
--- a/src/store/reducers/query/query.ts
+++ b/src/store/reducers/query/query.ts
@@ -131,6 +131,10 @@ const slice = createSlice({
selectors: {
selectQueriesHistoryFilter: (state) => state.history.filter || '',
selectTenantPath: (state) => state.tenantPath,
+ selectQueryDuration: (state) => ({
+ startTime: state.result?.startTime,
+ endTime: state.result?.endTime,
+ }),
selectResult: (state) => state.result,
selectQueriesHistory: (state) => {
const items = state.history.queries;
@@ -167,6 +171,7 @@ export const {
selectTenantPath,
selectResult,
selectUserInput,
+ selectQueryDuration,
} = slice.selectors;
interface SendQueryParams extends QueryRequestParams {
@@ -178,6 +183,9 @@ interface SendQueryParams extends QueryRequestParams {
enableTracingLevel?: boolean;
}
+// Stream query receives queryId from session chunk.
+type StreamQueryParams = Omit
;
+
interface QueryStats {
durationUs?: string | number;
endTime?: string | number;
@@ -188,12 +196,20 @@ const DEFAULT_CONCURRENT_RESULTS = false;
export const queryApi = api.injectEndpoints({
endpoints: (build) => ({
- useStreamQuery: build.mutation({
+ useStreamQuery: build.mutation({
queryFn: async (
- {query, database, querySettings = {}, enableTracingLevel, queryId},
+ {query, database, querySettings = {}, enableTracingLevel},
{signal, dispatch, getState},
) => {
- dispatch(setQueryResult({type: 'execute', queryId, isLoading: true}));
+ const startTime = Date.now();
+ dispatch(
+ setQueryResult({
+ type: 'execute',
+ queryId: '',
+ isLoading: true,
+ startTime,
+ }),
+ );
const {action, syntax} = getActionAndSyntaxFromQueryMode(
'execute',
@@ -238,18 +254,21 @@ export const queryApi = api.injectEndpoints({
},
{
signal,
- onQueryResponseChunk: (chunk) => {
- dispatch(setStreamQueryResponse(chunk));
- },
+ // First chunk is session chunk
onSessionChunk: (chunk) => {
dispatch(setStreamSession(chunk));
},
+ // Data chunks follow session chunk
onStreamDataChunk: (chunk) => {
streamDataChunkBatch.push(chunk);
if (!batchTimeout) {
batchTimeout = window.requestAnimationFrame(flushBatch);
}
},
+ // Last chunk is query response chunk
+ onQueryResponseChunk: (chunk) => {
+ dispatch(setStreamQueryResponse(chunk));
+ },
},
);
@@ -268,7 +287,9 @@ export const queryApi = api.injectEndpoints({
type: 'execute',
error,
isLoading: false,
- queryId,
+ startTime,
+ endTime: Date.now(),
+ queryId: state.query.result?.queryId || '',
}),
);
return {error};
@@ -287,7 +308,15 @@ export const queryApi = api.injectEndpoints({
},
{signal, dispatch},
) => {
- dispatch(setQueryResult({type: actionType, queryId, isLoading: true}));
+ const startTime = Date.now();
+ dispatch(
+ setQueryResult({
+ type: actionType,
+ queryId,
+ isLoading: true,
+ startTime,
+ }),
+ );
const {action, syntax} = getActionAndSyntaxFromQueryMode(
actionType,
@@ -329,6 +358,8 @@ export const queryApi = api.injectEndpoints({
error: response,
isLoading: false,
queryId,
+ startTime,
+ endTime: Date.now(),
}),
);
return {error: response};
@@ -358,6 +389,8 @@ export const queryApi = api.injectEndpoints({
data,
isLoading: false,
queryId,
+ startTime,
+ endTime: Date.now(),
}),
);
return {data: null};
@@ -368,6 +401,8 @@ export const queryApi = api.injectEndpoints({
error,
isLoading: false,
queryId,
+ startTime,
+ endTime: Date.now(),
}),
);
return {error};
diff --git a/src/store/reducers/query/streamingReducers.ts b/src/store/reducers/query/streamingReducers.ts
index 493d200ae8..d807d01f04 100644
--- a/src/store/reducers/query/streamingReducers.ts
+++ b/src/store/reducers/query/streamingReducers.ts
@@ -1,6 +1,5 @@
import type {PayloadAction} from '@reduxjs/toolkit';
-import type {StreamMetrics} from '../../../types/store/query';
import type {
QueryResponseChunk,
SessionChunk,
@@ -56,28 +55,8 @@ export const setStreamQueryResponse = (
state.result.data.plan = chunk.plan;
state.result.data.stats = chunk.stats;
}
-};
-
-const updateStreamMetrics = (metrics: StreamMetrics, totalNewRows: number) => {
- const currentTime = Date.now();
- const WINDOW_SIZE = 5000; // 5 seconds in milliseconds
-
- metrics.recentChunks.push({timestamp: currentTime, rowCount: totalNewRows});
- metrics.recentChunks = metrics.recentChunks.filter(
- (chunk) => currentTime - chunk.timestamp <= WINDOW_SIZE,
- );
-
- if (metrics.recentChunks.length > 0) {
- const oldestChunkTime = metrics.recentChunks[0].timestamp;
- const timeWindow = (currentTime - oldestChunkTime) / 1000;
- const totalRows = metrics.recentChunks.reduce(
- (sum: number, chunk) => sum + chunk.rowCount,
- 0,
- );
- metrics.rowsPerSecond = timeWindow > 0 ? totalRows / timeWindow : 0;
- }
- metrics.lastUpdateTime = currentTime;
+ state.result.endTime = Date.now();
};
const getEmptyResultSet = () => {
@@ -85,11 +64,6 @@ const getEmptyResultSet = () => {
columns: [],
result: [],
truncated: false,
- streamMetrics: {
- rowsPerSecond: 0,
- lastUpdateTime: Date.now(),
- recentChunks: [],
- },
};
};
@@ -123,11 +97,6 @@ export const addStreamingChunks = (state: QueryState, action: PayloadAction());
- const totalNewRows = action.payload.reduce(
- (sum: number, chunk) => sum + (chunk.result.rows?.length || 0),
- 0,
- );
-
// Process merged chunks
for (const [resultIndex, chunk] of mergedChunks.entries()) {
const {columns, rows} = chunk.result;
@@ -149,9 +118,5 @@ export const addStreamingChunks = (state: QueryState, action: PayloadAction;
-}
-
export interface ParsedResultSet {
columns?: ColumnType[];
result?: KeyValueRow[];
truncated?: boolean;
- streamMetrics?: StreamMetrics;
}
export interface IQueryResult {
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index a74b6a4398..98957ef76c 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -98,6 +98,7 @@ export const TENANT_OVERVIEW_TABLES_SETTINGS = {
export const QUERY_EXECUTION_SETTINGS_KEY = 'queryExecutionSettings';
export const LAST_QUERY_EXECUTION_SETTINGS_KEY = 'last_query_execution_settings';
export const QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY = 'querySettingsBannerLastClosed';
+export const QUERY_STOPPED_BANNER_CLOSED_KEY = 'queryStoppedBannerClosed';
export const LAST_USED_QUERY_ACTION_KEY = 'last_used_query_action';
diff --git a/src/utils/createToast.tsx b/src/utils/createToast.tsx
index d2a0e3c3af..35853fda05 100644
--- a/src/utils/createToast.tsx
+++ b/src/utils/createToast.tsx
@@ -7,16 +7,17 @@ interface CreateToastProps {
title?: string;
content?: string;
type: 'error' | 'success';
+ autoHiding?: number;
}
-function createToast({name, title, type, content}: CreateToastProps) {
+function createToast({name, title, type, content, autoHiding}: CreateToastProps) {
return toaster.add({
name: name ?? 'Request succeeded',
title: title ?? 'Request succeeded',
theme: type === 'error' ? 'danger' : 'success',
content: content,
isClosable: true,
- autoHiding: type === 'success' ? 5000 : false,
+ autoHiding: autoHiding || (type === 'success' ? 5000 : false),
});
}
diff --git a/tests/suites/tenant/constants.ts b/tests/suites/tenant/constants.ts
index 5dfcc89eca..265e859aec 100644
--- a/tests/suites/tenant/constants.ts
+++ b/tests/suites/tenant/constants.ts
@@ -2,10 +2,20 @@
// May cause Memory exceed on real database
export const simpleQuery = 'SELECT 1;';
-export const longTableSelect = 'SELECT * FROM `.sys/pg_class`';
+export const longTableSelect = (limit?: number) =>
+ 'SELECT * FROM `.sys/pg_class`' + (limit ? ` LIMIT ${limit};` : ';');
// 400 is pretty enough
export const longRunningQuery = new Array(400).fill(simpleQuery).join('');
+export const longRunningStreamQuery = `$sample = AsList(AsStruct(ListFromRange(1, 100000) AS value1, ListFromRange(1, 1000) AS value2, CAST(1 AS Uint32) AS id));
+
+SELECT value1, value2, id FROM as_table($sample) FLATTEN BY (value1);
+`;
+export const longerRunningStreamQuery = `$sample = AsList(AsStruct(ListFromRange(1, 1000000) AS value1, ListFromRange(1, 10000) AS value2, CAST(1 AS Uint32) AS id));
+
+SELECT value1, value2, id FROM as_table($sample) FLATTEN BY (value1);
+`;
+export const selectFromMyRowTableQuery = 'select * from `my_row_table`';
export const createTableQuery = `
CREATE TABLE \`/local/ydb_row_table\` (
diff --git a/tests/suites/tenant/queryEditor/models/QueryEditor.ts b/tests/suites/tenant/queryEditor/models/QueryEditor.ts
index dbc7c8d08e..e37036cddd 100644
--- a/tests/suites/tenant/queryEditor/models/QueryEditor.ts
+++ b/tests/suites/tenant/queryEditor/models/QueryEditor.ts
@@ -50,9 +50,9 @@ export class QueryEditor {
private runButton: Locator;
private explainButton: Locator;
private stopButton: Locator;
+ private stopBanner: Locator;
private saveButton: Locator;
private gearButton: Locator;
- private indicatorIcon: Locator;
private banner: Locator;
private executionStatus: Locator;
private radioButton: Locator;
@@ -65,15 +65,13 @@ export class QueryEditor {
this.editorTextArea = this.selector.locator('.query-editor__monaco textarea');
this.runButton = this.selector.getByRole('button', {name: ButtonNames.Run});
this.stopButton = this.selector.getByRole('button', {name: ButtonNames.Stop});
+ this.stopBanner = this.selector.locator('.ydb-query-stopped-banner');
this.explainButton = this.selector.getByRole('button', {name: ButtonNames.Explain});
this.saveButton = this.selector.getByRole('button', {name: ButtonNames.Save});
- this.gearButton = this.selector.locator('.ydb-query-editor-controls__gear-button');
- this.executionStatus = this.selector.locator('.kv-query-execution-status');
+ this.gearButton = this.selector.locator('.ydb-query-editor-button__gear-button');
+ this.executionStatus = this.selector.locator('.kv-query-execution-status .g-text');
this.resultsControls = this.selector.locator('.ydb-query-result__controls');
- this.indicatorIcon = this.selector.locator(
- '.kv-query-execution-status__query-settings-icon',
- );
- this.elapsedTimeLabel = this.selector.locator('.ydb-query-elapsed-time');
+ this.elapsedTimeLabel = this.selector.locator('.kv-query-execution-status .g-label__value');
this.radioButton = this.selector.locator('.query-editor__pane-wrapper .g-radio-button');
this.banner = this.page.locator('.ydb-query-settings-banner');
@@ -237,6 +235,11 @@ export class QueryEditor {
return true;
}
+ async isStopBannerVisible() {
+ await this.stopBanner.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ return true;
+ }
+
async isResultsControlsVisible() {
await this.resultsControls.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
return true;
@@ -295,16 +298,6 @@ export class QueryEditor {
return true;
}
- async isIndicatorIconVisible() {
- await this.indicatorIcon.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
- return true;
- }
-
- async isIndicatorIconHidden() {
- await this.indicatorIcon.waitFor({state: 'hidden', timeout: VISIBILITY_TIMEOUT});
- return true;
- }
-
async waitForStatus(expectedStatus: string, timeout = VISIBILITY_TIMEOUT) {
await this.executionStatus.waitFor({state: 'visible', timeout});
diff --git a/tests/suites/tenant/queryEditor/models/ResultTable.ts b/tests/suites/tenant/queryEditor/models/ResultTable.ts
index 23b69b2c23..dfd36a5941 100644
--- a/tests/suites/tenant/queryEditor/models/ResultTable.ts
+++ b/tests/suites/tenant/queryEditor/models/ResultTable.ts
@@ -25,11 +25,13 @@ export class ResultTable {
private preview: Locator;
private resultHead: Locator;
private resultWrapper: Locator;
+ private resultTitle: Locator;
constructor(selector: Locator) {
this.table = selector.locator('.ydb-query-result-sets-viewer__result');
this.preview = selector.locator('.kv-preview__result');
this.resultHead = selector.locator('.ydb-query-result-sets-viewer__head');
+ this.resultTitle = selector.locator('.ydb-query-result-sets-viewer__title');
this.resultWrapper = selector.locator('.ydb-query-result-sets-viewer__result-wrapper');
}
@@ -68,11 +70,6 @@ export class ResultTable {
return true;
}
- async getResultHeadText() {
- await this.resultHead.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
- return this.resultHead.innerText();
- }
-
async getResultTabs() {
const tabs = this.resultWrapper.locator(
'.ydb-query-result-sets-viewer__tabs .g-tabs__item',
@@ -86,20 +83,27 @@ export class ResultTable {
return tabs.count();
}
- async getResultTabTitle(index: number) {
+ async getResultTitleText() {
+ await this.resultTitle.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ return this.resultTitle.locator('.g-text').first().textContent();
+ }
+
+ async getResultTitleCount() {
+ await this.resultTitle.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ return this.resultTitle.locator('.g-text').nth(1).textContent();
+ }
+
+ async getResultTabTitleText(index: number) {
const tabs = await this.getResultTabs();
const tab = tabs.nth(index);
await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
- return tab.getAttribute('title');
+ return tab.locator('.g-text').first().textContent();
}
- async hasMultipleResultTabs() {
- const tabs = this.resultWrapper.locator('.ydb-query-result-sets-viewer__tabs');
- try {
- await tabs.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
- return true;
- } catch {
- return false;
- }
+ async getResultTabTitleCount(index: number) {
+ const tabs = await this.getResultTabs();
+ const tab = tabs.nth(index);
+ await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ return tab.locator('.g-text').nth(1).textContent();
}
}
diff --git a/tests/suites/tenant/queryEditor/queryEditor.test.ts b/tests/suites/tenant/queryEditor/queryEditor.test.ts
index 74352efe3c..1a305d5519 100644
--- a/tests/suites/tenant/queryEditor/queryEditor.test.ts
+++ b/tests/suites/tenant/queryEditor/queryEditor.test.ts
@@ -3,8 +3,15 @@ import {expect, test} from '@playwright/test';
import {QUERY_MODES, STATISTICS_MODES} from '../../../../src/utils/query';
import {getClipboardContent} from '../../../utils/clipboard';
import {tenantName} from '../../../utils/constants';
+import {toggleExperiment} from '../../../utils/toggleExperiment';
import {NavigationTabs, TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage';
-import {createTableQuery, longRunningQuery, longTableSelect} from '../constants';
+import {
+ createTableQuery,
+ longRunningQuery,
+ longRunningStreamQuery,
+ longTableSelect,
+ longerRunningStreamQuery,
+} from '../constants';
import {
ButtonNames,
@@ -68,7 +75,7 @@ test.describe('Test Query Editor', async () => {
await expect(explainAST).toBeVisible({timeout: VISIBILITY_TIMEOUT});
});
- test('Error is displayed for invalid query', async ({page}) => {
+ test('Error is displayed for invalid query for run', async ({page}) => {
const queryEditor = new QueryEditor(page);
const invalidQuery = 'Select d';
@@ -80,6 +87,18 @@ test.describe('Test Query Editor', async () => {
await expect(errorMessage).toContain('Column references are not allowed without FROM');
});
+ test('Error is displayed for invalid query for explain', async ({page}) => {
+ const queryEditor = new QueryEditor(page);
+
+ const invalidQuery = 'Select d';
+ await queryEditor.setQuery(invalidQuery);
+ await queryEditor.clickExplainButton();
+
+ await expect(queryEditor.waitForStatus('Failed')).resolves.toBe(true);
+ const errorMessage = await queryEditor.getErrorMessage();
+ await expect(errorMessage).toContain('Column references are not allowed without FROM');
+ });
+
test('Run and Explain buttons are disabled when query is empty', async ({page}) => {
const queryEditor = new QueryEditor(page);
@@ -102,29 +121,51 @@ test.describe('Test Query Editor', async () => {
await expect(queryEditor.isElapsedTimeVisible()).resolves.toBe(true);
});
- test('Stop button and elapsed time label disappear after query is stopped', async ({page}) => {
+ test('Query streaming finishes in reasonable time', async ({page}) => {
+ const queryEditor = new QueryEditor(page);
+ await toggleExperiment(page, 'on', 'Query Streaming');
+
+ await queryEditor.setQuery(longRunningStreamQuery);
+ await queryEditor.clickRunButton();
+
+ await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
+ });
+
+ test('Query execution is terminated when stop button is clicked', async ({page}) => {
const queryEditor = new QueryEditor(page);
await queryEditor.setQuery(longRunningQuery);
await queryEditor.clickRunButton();
await expect(queryEditor.isStopButtonVisible()).resolves.toBe(true);
-
await queryEditor.clickStopButton();
- await expect(queryEditor.isStopButtonHidden()).resolves.toBe(true);
- await expect(queryEditor.isElapsedTimeHidden()).resolves.toBe(true);
+ await expect(queryEditor.waitForStatus('Stopped')).resolves.toBe(true);
});
- test('Query execution is terminated when stop button is clicked', async ({page}) => {
+ test('Streaming query shows some results and banner when stop button is clicked', async ({
+ page,
+ browserName,
+ }) => {
+ // For some reason Safari handles large numbers list bad in Safari
+ // Will be investigated here https://github.com/ydb-platform/ydb-embedded-ui/issues/1989
+ test.skip(browserName === 'webkit', 'This test is skipped in Safari');
const queryEditor = new QueryEditor(page);
+ await toggleExperiment(page, 'on', 'Query Streaming');
- await queryEditor.setQuery(longRunningQuery);
+ await queryEditor.setQuery(longerRunningStreamQuery);
await queryEditor.clickRunButton();
await expect(queryEditor.isStopButtonVisible()).resolves.toBe(true);
+ await page.waitForTimeout(1000);
+
await queryEditor.clickStopButton();
+ await expect(queryEditor.isStopBannerVisible()).resolves.toBe(true);
+ await expect(queryEditor.resultTable.getResultTitleText()).resolves.toBe('Result');
+ await expect(
+ Promise.resolve(Number(await queryEditor.resultTable.getResultTitleCount())),
+ ).resolves.toBeGreaterThan(100);
await expect(queryEditor.waitForStatus('Stopped')).resolves.toBe(true);
});
@@ -215,7 +256,8 @@ test.describe('Test Query Editor', async () => {
const queryEditor = new QueryEditor(page);
await queryEditor.setQuery(testQuery);
await queryEditor.clickRunButton();
- await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Result(1)');
+ await expect(queryEditor.resultTable.getResultTitleText()).resolves.toBe('Result');
+ await expect(queryEditor.resultTable.getResultTitleCount()).resolves.toBe('1');
});
test('No result head value for no result', async ({page}) => {
@@ -228,12 +270,27 @@ test.describe('Test Query Editor', async () => {
test('Truncated head value is 1 for 1 row truncated result', async ({page}) => {
const queryEditor = new QueryEditor(page);
- await queryEditor.setQuery(longTableSelect);
+ await queryEditor.setQuery(longTableSelect());
await queryEditor.clickGearButton();
await queryEditor.settingsDialog.changeLimitRows(1);
await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
await queryEditor.clickRunButton();
- await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Truncated(1)');
+ await expect(queryEditor.resultTable.getResultTitleText()).resolves.toBe('Truncated');
+ await expect(queryEditor.resultTable.getResultTitleCount()).resolves.toBe('1');
+ });
+
+ test('Truncated results for multiple tabs', async ({page}) => {
+ const queryEditor = new QueryEditor(page);
+ await queryEditor.setQuery(`${longTableSelect(2)}${longTableSelect(2)}`);
+ await queryEditor.clickGearButton();
+ await queryEditor.settingsDialog.changeLimitRows(3);
+ await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
+ await queryEditor.clickRunButton();
+ await expect(queryEditor.resultTable.getResultTabsCount()).resolves.toBe(2);
+ await expect(queryEditor.resultTable.getResultTabTitleText(1)).resolves.toBe(
+ 'Result #2(T)',
+ );
+ await expect(queryEditor.resultTable.getResultTabTitleCount(1)).resolves.toBe('1');
});
test('Query execution status changes correctly', async ({page}) => {
@@ -257,8 +314,8 @@ test.describe('Test Query Editor', async () => {
// Verify there are two result tabs
await expect(queryEditor.resultTable.getResultTabsCount()).resolves.toBe(2);
- await expect(queryEditor.resultTable.getResultTabTitle(0)).resolves.toBe('Result #1');
- await expect(queryEditor.resultTable.getResultTabTitle(1)).resolves.toBe('Result #2');
+ await expect(queryEditor.resultTable.getResultTabTitleText(0)).resolves.toBe('Result #1');
+ await expect(queryEditor.resultTable.getResultTabTitleText(1)).resolves.toBe('Result #2');
// Then verify running only selected part produces one result
await queryEditor.focusEditor();
@@ -268,8 +325,8 @@ test.describe('Test Query Editor', async () => {
await executeSelectedQueryWithKeybinding(page);
await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
- await expect(queryEditor.resultTable.hasMultipleResultTabs()).resolves.toBe(false);
- await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Result(1)');
+ await expect(queryEditor.resultTable.getResultTitleText()).resolves.toBe('Result');
+ await expect(queryEditor.resultTable.getResultTitleCount()).resolves.toBe('1');
});
test('Running selected query via context menu executes only selected part', async ({page}) => {
@@ -283,8 +340,8 @@ test.describe('Test Query Editor', async () => {
// Verify there are two result tabs
await expect(queryEditor.resultTable.getResultTabsCount()).resolves.toBe(2);
- await expect(queryEditor.resultTable.getResultTabTitle(0)).resolves.toBe('Result #1');
- await expect(queryEditor.resultTable.getResultTabTitle(1)).resolves.toBe('Result #2');
+ await expect(queryEditor.resultTable.getResultTabTitleText(0)).resolves.toBe('Result #1');
+ await expect(queryEditor.resultTable.getResultTabTitleText(1)).resolves.toBe('Result #2');
// Then verify running only selected part produces one result without tabs
await queryEditor.focusEditor();
@@ -294,8 +351,8 @@ test.describe('Test Query Editor', async () => {
await queryEditor.runSelectedQueryViaContextMenu();
await expect(queryEditor.waitForStatus('Completed')).resolves.toBe(true);
- await expect(queryEditor.resultTable.hasMultipleResultTabs()).resolves.toBe(false);
- await expect(queryEditor.resultTable.getResultHeadText()).resolves.toBe('Result(1)');
+ await expect(queryEditor.resultTable.getResultTitleText()).resolves.toBe('Result');
+ await expect(queryEditor.resultTable.getResultTitleCount()).resolves.toBe('1');
});
test('Results controls collapse and expand functionality', async ({page}) => {
diff --git a/tests/suites/tenant/queryEditor/querySettings.test.ts b/tests/suites/tenant/queryEditor/querySettings.test.ts
index 57946127bc..e950602304 100644
--- a/tests/suites/tenant/queryEditor/querySettings.test.ts
+++ b/tests/suites/tenant/queryEditor/querySettings.test.ts
@@ -77,45 +77,6 @@ test.describe('Test Query Settings', async () => {
await expect(queryEditor.isBannerHidden()).resolves.toBe(true);
});
- test('Indicator icon appears after closing banner', async ({page}) => {
- const queryEditor = new QueryEditor(page);
-
- // Change a setting
- await queryEditor.clickGearButton();
- await queryEditor.settingsDialog.changeQueryMode(QUERY_MODES.scan);
- await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
-
- // Execute a script to make the banner appear
- await queryEditor.setQuery(testQuery);
- await queryEditor.clickRunButton();
-
- // Close the banner
- await queryEditor.closeBanner();
-
- await expect(queryEditor.isIndicatorIconVisible()).resolves.toBe(true);
- });
-
- test('Indicator not appears for running query', async ({page}) => {
- const queryEditor = new QueryEditor(page);
-
- // Change a setting
- await queryEditor.clickGearButton();
- await queryEditor.settingsDialog.changeTransactionMode(TRANSACTION_MODES.snapshot);
- await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
-
- // Execute a script to make the banner appear
- await queryEditor.setQuery(testQuery);
- await queryEditor.clickRunButton();
-
- // Close the banner
- await queryEditor.closeBanner();
- await queryEditor.setQuery(longRunningQuery);
- await queryEditor.clickRunButton();
- await page.waitForTimeout(500);
-
- await expect(queryEditor.isIndicatorIconHidden()).resolves.toBe(true);
- });
-
test('Gear button shows number of changed settings', async ({page}) => {
const queryEditor = new QueryEditor(page);
await queryEditor.clickGearButton();