Skip to content

Commit 151d766

Browse files
Copilotadameatartemmufazalov
authored
feat: keep plan representation tab selection between query executions (#2625)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: adameat <[email protected]> Co-authored-by: Alexey Efimov <[email protected]> Co-authored-by: artemmufazalov <[email protected]>
1 parent 4222c14 commit 151d766

File tree

4 files changed

+125
-21
lines changed

4 files changed

+125
-21
lines changed

src/containers/Tenant/Query/QueryResult/QueryResultViewer.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
// Query result viewer with tab persistence functionality
23

34
import type {Settings} from '@gravity-ui/react-data-table';
45
import type {ControlGroupOption} from '@gravity-ui/uikit';
@@ -10,13 +11,14 @@ import {Illustration} from '../../../../components/Illustration';
1011
import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper';
1112
import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus';
1213
import {disableFullscreen} from '../../../../store/reducers/fullscreen';
14+
import {selectResultTab, setResultTab} from '../../../../store/reducers/query/query';
1315
import type {QueryResult} from '../../../../store/reducers/query/types';
1416
import type {ValueOf} from '../../../../types/common';
1517
import type {QueryAction} from '../../../../types/store/query';
1618
import {cn} from '../../../../utils/cn';
1719
import {USE_SHOW_PLAN_SVG_KEY} from '../../../../utils/constants';
1820
import {getStringifiedData} from '../../../../utils/dataFormatters/dataFormatters';
19-
import {useSetting, useTypedDispatch} from '../../../../utils/hooks';
21+
import {useSetting, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
2022
import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers';
2123
import {QuerySettingsBanner} from '../QuerySettingsBanner/QuerySettingsBanner';
2224
import {QueryStoppedBanner} from '../QueryStoppedBanner/QueryStoppedBanner';
@@ -98,27 +100,43 @@ export function QueryResultViewer({
98100
onExpandResults,
99101
}: ExecuteResultProps) {
100102
const dispatch = useTypedDispatch();
103+
const selectedTabs = useTypedSelector(selectResultTab);
101104

102105
const isExecute = resultType === 'execute';
103106
const isExplain = resultType === 'explain';
104107

105108
const [selectedResultSet, setSelectedResultSet] = React.useState(0);
106-
const [activeSection, setActiveSection] = React.useState<SectionID>(() => {
107-
return isExecute ? RESULT_OPTIONS_IDS.result : RESULT_OPTIONS_IDS.schema;
108-
});
109109
const [useShowPlanToSvg] = useSetting<boolean>(USE_SHOW_PLAN_SVG_KEY);
110110

111+
// Get the saved tab for the current query type, or use default
112+
const getDefaultSection = (): SectionID => {
113+
return isExecute ? RESULT_OPTIONS_IDS.result : RESULT_OPTIONS_IDS.schema;
114+
};
115+
116+
const activeSection: SectionID = React.useMemo(() => {
117+
const savedTab = selectedTabs?.[resultType];
118+
if (savedTab) {
119+
// Validate that the saved tab is valid for the current result type
120+
const validSections = isExecute ? EXECUTE_SECTIONS : EXPLAIN_SECTIONS;
121+
if (validSections.includes(savedTab as SectionID)) {
122+
return savedTab as SectionID;
123+
}
124+
}
125+
return getDefaultSection();
126+
}, [selectedTabs, resultType, isExecute]);
127+
111128
const {error, isLoading, data = {}} = result;
112129
const {preparedPlan, simplifiedPlan, stats, resultSets, ast} = data;
113130

114131
React.useEffect(() => {
115-
if (resultType === 'execute' && !EXECUTE_SECTIONS.includes(activeSection)) {
116-
setActiveSection('result');
117-
}
118-
if (resultType === 'explain' && !EXPLAIN_SECTIONS.includes(activeSection)) {
119-
setActiveSection('schema');
120-
}
121-
}, [activeSection, resultType]);
132+
return () => {
133+
dispatch(disableFullscreen());
134+
};
135+
}, [dispatch]);
136+
137+
const onSelectSection = (value: SectionID) => {
138+
dispatch(setResultTab({queryType: resultType, tabId: value}));
139+
};
122140

123141
const radioButtonOptions: ControlGroupOption<SectionID>[] = React.useMemo(() => {
124142
let sections: SectionID[] = [];
@@ -137,16 +155,6 @@ export function QueryResultViewer({
137155
});
138156
}, [isExecute, isExplain]);
139157

140-
React.useEffect(() => {
141-
return () => {
142-
dispatch(disableFullscreen());
143-
};
144-
}, [dispatch]);
145-
146-
const onSelectSection = (value: SectionID) => {
147-
setActiveSection(value);
148-
};
149-
150158
const getStatsToCopy = () => {
151159
switch (activeSection) {
152160
case RESULT_OPTIONS_IDS.result: {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import queryReducer, {setResultTab} from '../query';
2+
import type {QueryState} from '../types';
3+
4+
describe('QueryResultViewer tab persistence integration', () => {
5+
const initialState: QueryState = {
6+
input: '',
7+
history: {
8+
queries: [],
9+
currentIndex: -1,
10+
},
11+
};
12+
13+
it('should save and retrieve tab selection for explain queries', () => {
14+
// Test that we can set and get the tab preference
15+
let state = queryReducer(initialState, setResultTab({queryType: 'explain', tabId: 'json'}));
16+
17+
expect(state.selectedResultTab).toEqual({
18+
explain: 'json',
19+
});
20+
21+
// Test updating the same query type
22+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'ast'}));
23+
24+
expect(state.selectedResultTab).toEqual({
25+
explain: 'ast',
26+
});
27+
});
28+
29+
it('should save and retrieve tab selection for execute queries', () => {
30+
const state = queryReducer(
31+
initialState,
32+
setResultTab({queryType: 'execute', tabId: 'stats'}),
33+
);
34+
35+
expect(state.selectedResultTab).toEqual({
36+
execute: 'stats',
37+
});
38+
});
39+
40+
it('should maintain separate preferences for different query types', () => {
41+
let state = initialState;
42+
43+
// Set explain tab
44+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'json'}));
45+
expect(state.selectedResultTab).toEqual({
46+
explain: 'json',
47+
});
48+
49+
// Set execute tab - should not override explain
50+
state = queryReducer(state, setResultTab({queryType: 'execute', tabId: 'stats'}));
51+
expect(state.selectedResultTab).toEqual({
52+
explain: 'json',
53+
execute: 'stats',
54+
});
55+
56+
// Update explain tab - should not affect execute
57+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'ast'}));
58+
expect(state.selectedResultTab).toEqual({
59+
explain: 'ast',
60+
execute: 'stats',
61+
});
62+
});
63+
64+
it('should handle multiple updates to the same query type', () => {
65+
let state = initialState;
66+
67+
// Set initial value
68+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'schema'}));
69+
expect(state.selectedResultTab?.explain).toBe('schema');
70+
71+
// Update to different tab
72+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'json'}));
73+
expect(state.selectedResultTab?.explain).toBe('json');
74+
75+
// Update again
76+
state = queryReducer(state, setResultTab({queryType: 'explain', tabId: 'ast'}));
77+
expect(state.selectedResultTab?.explain).toBe('ast');
78+
});
79+
});

src/store/reducers/query/query.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ const slice = createSlice({
128128
setQueryHistoryFilter: (state, action: PayloadAction<string>) => {
129129
state.history.filter = action.payload;
130130
},
131+
setResultTab: (
132+
state,
133+
action: PayloadAction<{queryType: 'execute' | 'explain'; tabId: string}>,
134+
) => {
135+
const {queryType, tabId} = action.payload;
136+
if (!state.selectedResultTab) {
137+
state.selectedResultTab = {};
138+
}
139+
state.selectedResultTab[queryType] = tabId;
140+
},
131141
setStreamSession: setStreamSessionReducer,
132142
addStreamingChunks: addStreamingChunksReducer,
133143
setStreamQueryResponse: setStreamQueryResponseReducer,
@@ -149,6 +159,7 @@ const slice = createSlice({
149159
selectUserInput: (state) => state.input,
150160
selectIsDirty: (state) => state.isDirty,
151161
selectQueriesHistoryCurrentIndex: (state) => state.history?.currentIndex,
162+
selectResultTab: (state) => state.selectedResultTab,
152163
},
153164
});
154165

@@ -177,6 +188,7 @@ export const {
177188
setStreamQueryResponse,
178189
setStreamSession,
179190
setIsDirty,
191+
setResultTab,
180192
} = slice.actions;
181193

182194
export const {
@@ -187,6 +199,7 @@ export const {
187199
selectResult,
188200
selectUserInput,
189201
selectIsDirty,
202+
selectResultTab,
190203
} = slice.selectors;
191204

192205
interface SendQueryParams extends QueryRequestParams {

src/store/reducers/query/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ export interface QueryState {
6868
filter?: string;
6969
};
7070
tenantPath?: string;
71+
selectedResultTab?: {
72+
execute?: string;
73+
explain?: string;
74+
};
7175
}

0 commit comments

Comments
 (0)