Skip to content

Commit 8be55f1

Browse files
authored
fix: do not reset query results when switch query tabs (#1198)
1 parent 763fab2 commit 8be55f1

File tree

11 files changed

+201
-50
lines changed

11 files changed

+201
-50
lines changed

package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"path-to-regexp": "^3.0.0",
3939
"qs": "^6.12.0",
4040
"react-error-boundary": "^4.0.13",
41+
"react-freeze": "^1.0.4",
4142
"react-helmet-async": "^2.0.5",
4243
"react-hook-form": "^7.52.1",
4344
"react-json-inspector": "^7.1.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.ydb-not-render-until-first-visible {
2+
display: contents;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
3+
import {Freeze} from 'react-freeze';
4+
5+
import {cn} from '../../utils/cn';
6+
7+
import './NotRenderUntilFirstVisible.scss';
8+
9+
const block = cn('ydb-not-render-until-first-visible');
10+
11+
interface Props {
12+
show?: boolean;
13+
className?: string;
14+
children: React.ReactNode;
15+
}
16+
17+
export default function NotRenderUntilFirstVisible({show, className, children}: Props) {
18+
return (
19+
<div style={show ? undefined : {display: 'none'}} className={block(null, className)}>
20+
<Freeze freeze={!show}>{children}</Freeze>
21+
</div>
22+
);
23+
}

src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import React from 'react';
2+
13
import {useThemeValue} from '@gravity-ui/uikit';
24

5+
import NotRenderUntilFirstVisible from '../../../components/NotRenderUntilFirstVisible/NotRenderUntilFirstVisible';
36
import {TENANT_PAGES_IDS} from '../../../store/reducers/tenant/constants';
47
import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../types/additionalProps';
58
import type {EPathType} from '../../../types/api/schema';
@@ -28,22 +31,23 @@ function ObjectGeneral(props: ObjectGeneralProps) {
2831

2932
const renderPageContent = () => {
3033
const {type, additionalTenantProps, additionalNodesProps, tenantName, path} = props;
31-
switch (tenantPage) {
32-
case TENANT_PAGES_IDS.query: {
33-
return <Query tenantName={tenantName} path={path} theme={theme} type={type} />;
34-
}
35-
default: {
36-
return (
34+
35+
return (
36+
<React.Fragment>
37+
<NotRenderUntilFirstVisible show={tenantPage === TENANT_PAGES_IDS.query}>
38+
<Query tenantName={tenantName} path={path} theme={theme} type={type} />
39+
</NotRenderUntilFirstVisible>
40+
<NotRenderUntilFirstVisible show={tenantPage === TENANT_PAGES_IDS.diagnostics}>
3741
<Diagnostics
3842
type={type}
3943
tenantName={tenantName}
4044
path={path}
4145
additionalTenantProps={additionalTenantProps}
4246
additionalNodesProps={additionalNodesProps}
4347
/>
44-
);
45-
}
46-
}
48+
</NotRenderUntilFirstVisible>
49+
</React.Fragment>
50+
);
4751
};
4852

4953
return (

src/containers/Tenant/Query/Query.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22

33
import {Helmet} from 'react-helmet-async';
44

5+
import NotRenderUntilFirstVisible from '../../../components/NotRenderUntilFirstVisible/NotRenderUntilFirstVisible';
56
import {changeUserInput} from '../../../store/reducers/executeQuery';
67
import {TENANT_QUERY_TABS_ID} from '../../../store/reducers/tenant/constants';
78
import type {EPathType} from '../../../types/api/schema';
@@ -39,20 +40,19 @@ export const Query = (props: QueryProps) => {
3940
);
4041

4142
const renderContent = () => {
42-
switch (queryTab) {
43-
case TENANT_QUERY_TABS_ID.newQuery: {
44-
return <QueryEditor changeUserInput={handleUserInputChange} {...props} />;
45-
}
46-
case TENANT_QUERY_TABS_ID.history: {
47-
return <QueriesHistory changeUserInput={handleUserInputChange} />;
48-
}
49-
case TENANT_QUERY_TABS_ID.saved: {
50-
return <SavedQueries changeUserInput={handleUserInputChange} />;
51-
}
52-
default: {
53-
return null;
54-
}
55-
}
43+
return (
44+
<React.Fragment>
45+
<NotRenderUntilFirstVisible show={queryTab === TENANT_QUERY_TABS_ID.newQuery}>
46+
<QueryEditor changeUserInput={handleUserInputChange} {...props} />
47+
</NotRenderUntilFirstVisible>
48+
<NotRenderUntilFirstVisible show={queryTab === TENANT_QUERY_TABS_ID.history}>
49+
<QueriesHistory changeUserInput={handleUserInputChange} />
50+
</NotRenderUntilFirstVisible>
51+
<NotRenderUntilFirstVisible show={queryTab === TENANT_QUERY_TABS_ID.saved}>
52+
<SavedQueries changeUserInput={handleUserInputChange} />
53+
</NotRenderUntilFirstVisible>
54+
</React.Fragment>
55+
);
5656
};
5757

5858
return (

tests/suites/tenant/TenantPage.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1-
import type {Page} from '@playwright/test';
1+
import type {Locator, Page} from '@playwright/test';
22

33
import {PageModel} from '../../models/PageModel';
44
import {tenantPage} from '../../utils/constants';
55

6+
export const VISIBILITY_TIMEOUT = 5000;
7+
8+
export enum NavigationTabs {
9+
Query = 'Query',
10+
Diagnostics = 'Diagnostics',
11+
}
12+
613
export class TenantPage extends PageModel {
14+
private navigation: Locator;
15+
private radioGroup: Locator;
16+
717
constructor(page: Page) {
818
super(page, tenantPage);
19+
20+
this.navigation = page.locator('.ydb-tenant-navigation');
21+
this.radioGroup = this.navigation.locator('.g-radio-button');
22+
}
23+
24+
async selectNavigationTab(tabName: NavigationTabs) {
25+
const tabInput = this.radioGroup.locator(`input[value="${tabName.toLowerCase()}"]`);
26+
await tabInput.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
27+
await tabInput.click();
928
}
1029
}

tests/suites/tenant/queryEditor/QueryEditor.ts

+62-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {Locator, Page} from '@playwright/test';
22

3-
export const VISIBILITY_TIMEOUT = 5000;
3+
import {VISIBILITY_TIMEOUT} from '../TenantPage';
44

55
export enum QueryMode {
66
YQLScript = 'YQL Script',
@@ -22,6 +22,46 @@ export enum ButtonNames {
2222
Stop = 'Stop',
2323
}
2424

25+
export enum ResultTabNames {
26+
Result = 'Result',
27+
Stats = 'Stats',
28+
Schema = 'Schema',
29+
ExplainPlan = 'Explain Plan',
30+
}
31+
32+
export enum QueryTabs {
33+
Editor = 'Editor',
34+
History = 'History',
35+
Saved = 'Saved',
36+
}
37+
38+
export class QueryTabsNavigation {
39+
private tabsContainer: Locator;
40+
41+
constructor(page: Page) {
42+
this.tabsContainer = page.locator('.ydb-query__tabs');
43+
}
44+
45+
async selectTab(tabName: QueryTabs) {
46+
const tab = this.tabsContainer.locator(`role=tab[name="${tabName}"]`);
47+
await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
48+
await tab.click();
49+
}
50+
51+
async isTabSelected(tabName: QueryTabs): Promise<boolean> {
52+
const tab = this.tabsContainer.locator(`role=tab[name="${tabName}"]`);
53+
await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
54+
const isSelected = await tab.getAttribute('aria-selected');
55+
return isSelected === 'true';
56+
}
57+
58+
async getTabHref(tabName: QueryTabs): Promise<string | null> {
59+
const link = this.tabsContainer.locator(`a:has(div[role="tab"][title="${tabName}"])`);
60+
await link.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
61+
return link.getAttribute('href');
62+
}
63+
}
64+
2565
export class SettingsDialog {
2666
private dialog: Locator;
2767
private page: Page;
@@ -81,6 +121,22 @@ export class SettingsDialog {
81121
}
82122
}
83123

124+
class PaneWrapper {
125+
paneWrapper: Locator;
126+
private radioButton: Locator;
127+
128+
constructor(page: Page) {
129+
this.paneWrapper = page.locator('.query-editor__pane-wrapper');
130+
this.radioButton = this.paneWrapper.locator('.g-radio-button');
131+
}
132+
133+
async selectTab(tabName: ResultTabNames) {
134+
const tab = this.radioButton.getByLabel(tabName);
135+
await tab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
136+
await tab.click();
137+
}
138+
}
139+
84140
export class ResultTable {
85141
private table: Locator;
86142
private preview: Locator;
@@ -123,8 +179,10 @@ export class ResultTable {
123179

124180
export class QueryEditor {
125181
settingsDialog: SettingsDialog;
182+
paneWrapper: PaneWrapper;
183+
queryTabs: QueryTabsNavigation;
184+
resultTable: ResultTable;
126185

127-
private resultTable: ResultTable;
128186
private page: Page;
129187
private selector: Locator;
130188
private editorTextArea: Locator;
@@ -158,6 +216,8 @@ export class QueryEditor {
158216

159217
this.settingsDialog = new SettingsDialog(page);
160218
this.resultTable = new ResultTable(this.selector);
219+
this.paneWrapper = new PaneWrapper(page);
220+
this.queryTabs = new QueryTabsNavigation(page);
161221
}
162222

163223
async run(query: string, mode: QueryMode) {
@@ -265,22 +325,6 @@ export class QueryEditor {
265325
return true;
266326
}
267327

268-
async isResultTableVisible() {
269-
return await this.resultTable.isVisible();
270-
}
271-
272-
async isResultTableHidden() {
273-
return await this.resultTable.isHidden();
274-
}
275-
276-
async isPreviewVisible() {
277-
return await this.resultTable.isPreviewVisible();
278-
}
279-
280-
async isPreviewHidden() {
281-
return await this.resultTable.isPreviewHidden();
282-
}
283-
284328
async isStopButtonVisible() {
285329
await this.stopButton.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
286330
return true;

tests/suites/tenant/queryEditor/queryEditor.test.ts

+48-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import {expect, test} from '@playwright/test';
22

33
import {tenantName} from '../../../utils/constants';
4-
import {TenantPage} from '../TenantPage';
4+
import {NavigationTabs, TenantPage, VISIBILITY_TIMEOUT} from '../TenantPage';
55

66
import {
77
ButtonNames,
88
ExplainResultType,
99
QueryEditor,
1010
QueryMode,
11-
VISIBILITY_TIMEOUT,
11+
QueryTabs,
12+
ResultTabNames,
1213
} from './QueryEditor';
1314
import {longRunningQuery} from './constants';
1415

@@ -53,14 +54,14 @@ test.describe('Test Query Editor', async () => {
5354
const queryEditor = new QueryEditor(page);
5455
await queryEditor.run(testQuery, QueryMode.YQLScript);
5556

56-
await expect(queryEditor.isResultTableVisible()).resolves.toBe(true);
57+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
5758
});
5859

5960
test('Run button executes Scan', async ({page}) => {
6061
const queryEditor = new QueryEditor(page);
6162
await queryEditor.run(testQuery, QueryMode.Scan);
6263

63-
await expect(queryEditor.isResultTableVisible()).resolves.toBe(true);
64+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
6465
});
6566

6667
test('Explain button executes YQL script explanation', async ({page}) => {
@@ -333,4 +334,47 @@ test.describe('Test Query Editor', async () => {
333334
const statusElement = await queryEditor.getExecutionStatus();
334335
await expect(statusElement).toBe('Failed');
335336
});
337+
338+
test('Changing tab inside results pane doesnt change results view', async ({page}) => {
339+
const queryEditor = new QueryEditor(page);
340+
await queryEditor.setQuery(testQuery);
341+
await queryEditor.clickGearButton();
342+
await queryEditor.settingsDialog.changeStatsLevel('Profile');
343+
await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
344+
await queryEditor.clickRunButton();
345+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
346+
await queryEditor.paneWrapper.selectTab(ResultTabNames.Schema);
347+
await expect(queryEditor.resultTable.isHidden()).resolves.toBe(true);
348+
await queryEditor.paneWrapper.selectTab(ResultTabNames.Result);
349+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
350+
});
351+
352+
test('Changing tab inside editor doesnt change results view', async ({page}) => {
353+
const queryEditor = new QueryEditor(page);
354+
await queryEditor.setQuery(testQuery);
355+
await queryEditor.clickGearButton();
356+
await queryEditor.settingsDialog.changeStatsLevel('Profile');
357+
await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
358+
await queryEditor.clickRunButton();
359+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
360+
await queryEditor.queryTabs.selectTab(QueryTabs.History);
361+
await expect(queryEditor.resultTable.isHidden()).resolves.toBe(true);
362+
await queryEditor.queryTabs.selectTab(QueryTabs.Editor);
363+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
364+
});
365+
366+
test('Changing tab to diagnostics doesnt change results view', async ({page}) => {
367+
const queryEditor = new QueryEditor(page);
368+
const tenantPage = new TenantPage(page);
369+
await queryEditor.setQuery(testQuery);
370+
await queryEditor.clickGearButton();
371+
await queryEditor.settingsDialog.changeStatsLevel('Profile');
372+
await queryEditor.settingsDialog.clickButton(ButtonNames.Save);
373+
await queryEditor.clickRunButton();
374+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
375+
await tenantPage.selectNavigationTab(NavigationTabs.Diagnostics);
376+
await expect(queryEditor.resultTable.isHidden()).resolves.toBe(true);
377+
await tenantPage.selectNavigationTab(NavigationTabs.Query);
378+
await expect(queryEditor.resultTable.isVisible()).resolves.toBe(true);
379+
});
336380
});

tests/suites/tenant/summary/ObjectSummary.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {Locator, Page} from '@playwright/test';
22

3-
export const VISIBILITY_TIMEOUT = 5000;
3+
import {VISIBILITY_TIMEOUT} from '../TenantPage';
44

55
export class ObjectSummary {
66
private tree: Locator;

0 commit comments

Comments
 (0)