This commit is contained in:
@@ -171,6 +171,10 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||
};
|
||||
const isDirtyRef = useRef(false); // usefull when conn change.
|
||||
const qtStateRef = useRef(qtState);
|
||||
// The toolbar's check menu options (execute, explain and open file
|
||||
// options) for this tab, kept up to date by MainToolBar so that they are
|
||||
// saved and restored with the rest of the tab's state.
|
||||
const toolbarOptionsRef = useRef(null);
|
||||
const eventBus = useRef(eventBusObj || (new EventBus()));
|
||||
const docker = useRef(null);
|
||||
const api = useMemo(()=>getApiInstance(), []);
|
||||
@@ -343,6 +347,10 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||
},
|
||||
}));
|
||||
}
|
||||
const toolbarOptions = toolContent.connectionInfo?.toolbar_options;
|
||||
if (toolbarOptions && typeof toolbarOptions === 'object') {
|
||||
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, toolbarOptions);
|
||||
}
|
||||
if (toolContent?.modifiedExternally) {
|
||||
toolContent = await fmUtilsObj.warnFileReload(toolContent?.fileName, toolContent.data, '');
|
||||
}
|
||||
@@ -1014,6 +1022,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
||||
eol: qtState.eol,
|
||||
connection_list: qtState.connection_list,
|
||||
current_file: qtState.current_file,
|
||||
toolbarOptionsRef: toolbarOptionsRef,
|
||||
toggleQueryTool: () => {
|
||||
let panel = qtPanelDocker?.find(qtPanelId);
|
||||
if (!panel) {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const QUERY_TOOL_EVENTS = {
|
||||
TRIGGER_GRAPH_VISUALISER: 'TRIGGER_GRAPH_VISUALISER',
|
||||
TRIGGER_SELECT_ALL: 'TRIGGER_SELECT_ALL',
|
||||
TRIGGER_SAVE_QUERY_TOOL_DATA: 'TRIGGER_SAVE_QUERY_TOOL_DATA',
|
||||
RESTORE_TOOLBAR_OPTIONS: 'RESTORE_TOOLBAR_OPTIONS',
|
||||
TRIGGER_GET_QUERY_CONTENT: 'TRIGGER_GET_QUERY_CONTENT',
|
||||
TRIGGER_SAVE_BINARY_DATA: 'TRIGGER_SAVE_BINARY_DATA',
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
|
||||
import ConfirmTransactionContent from '../dialogs/ConfirmTransactionContent';
|
||||
import { LayoutDocker } from '../../../../../../static/js/helpers/Layout';
|
||||
import CloseRunningDialog from '../dialogs/CloseRunningDialog';
|
||||
import { useApplicationState } from '../../../../../../settings/static/ApplicationStateProvider';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
padding: '2px 4px',
|
||||
@@ -52,6 +53,30 @@ function changeQueryExecutionSettings(type, api, transId, value) {
|
||||
return api.post(url, JSON.stringify(value));
|
||||
}
|
||||
|
||||
// Execute options that also live on the server side transaction object, so
|
||||
// they must be sent to the server when changed.
|
||||
const EXECUTION_OPTIONS = ['auto_commit', 'auto_rollback', 'server_cursor'];
|
||||
|
||||
// Check menu options that are saved and restored with the tab's state.
|
||||
export const RESTORABLE_TOOLBAR_OPTIONS = [
|
||||
...EXECUTION_OPTIONS,
|
||||
'open_in_new_tab',
|
||||
'explain_verbose', 'explain_costs', 'explain_buffers', 'explain_timing',
|
||||
'explain_summary', 'explain_settings', 'explain_wal', 'explain_generic_plan',
|
||||
'explain_memory', 'explain_serialize_text', 'explain_serialize_binary',
|
||||
];
|
||||
|
||||
// Keep only the known options with boolean values from saved tab state.
|
||||
export function getRestorableToolbarOptions(options) {
|
||||
return _.pickBy(_.pick(options ?? {}, RESTORABLE_TOOLBAR_OPTIONS), _.isBoolean);
|
||||
}
|
||||
|
||||
// The execute options whose restored value differs from the current one.
|
||||
export function getChangedExecutionOptions(restored, current) {
|
||||
return _.pickBy(_.pick(restored, EXECUTION_OPTIONS),
|
||||
(val, key)=>val !== Boolean(current?.[key]));
|
||||
}
|
||||
|
||||
export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddToMacros}) {
|
||||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const queryToolCtx = useContext(QueryToolContext);
|
||||
@@ -73,6 +98,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
|
||||
});
|
||||
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
|
||||
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
|
||||
const checkedMenuItemsRef = React.useRef(checkedMenuItems);
|
||||
// Restored execute options waiting for the connection to be established.
|
||||
const [pendingExecOptions, setPendingExecOptions] = useState(null);
|
||||
const {isSaveToolDataEnabled} = useApplicationState() ?? {};
|
||||
/* Menu button refs */
|
||||
const saveAsMenuRef = React.useRef(null);
|
||||
const openInNewTabMenuRef = React.useRef(null);
|
||||
@@ -153,8 +182,66 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
|
||||
...otherVars,
|
||||
};
|
||||
});
|
||||
if(isSaveToolDataEnabled?.('sqleditor')) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
checkedMenuItemsRef.current = checkedMenuItems;
|
||||
if(queryToolCtx.toolbarOptionsRef) {
|
||||
queryToolCtx.toolbarOptionsRef.current = checkedMenuItems;
|
||||
}
|
||||
}, [checkedMenuItems]);
|
||||
|
||||
useEffect(()=>{
|
||||
const restoreToolbarOptions = (options)=>{
|
||||
let restored = getRestorableToolbarOptions(options);
|
||||
// Setting execute options on a View/Edit Data transaction would
|
||||
// promote it to a Query Tool, so only restore them for a Query Tool.
|
||||
if(!queryToolCtx.params.is_query_tool) {
|
||||
restored = _.omit(restored, EXECUTION_OPTIONS);
|
||||
}
|
||||
const changedExecOptions = getChangedExecutionOptions(restored, checkedMenuItemsRef.current);
|
||||
if(!_.isEmpty(changedExecOptions)) {
|
||||
if('server_cursor' in changedExecOptions) {
|
||||
queryToolCtx.updateServerCursor({server_cursor: changedExecOptions.server_cursor});
|
||||
}
|
||||
setPendingExecOptions((prev)=>({...prev, ...changedExecOptions}));
|
||||
}
|
||||
setCheckedMenuItems((prev)=>({...prev, ...restored}));
|
||||
};
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, restoreToolbarOptions);
|
||||
return ()=>{
|
||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, restoreToolbarOptions);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
/* The transaction is created with the preference values, so restored
|
||||
execute options can only be applied once it exists. */
|
||||
if(_.isEmpty(pendingExecOptions) || !queryToolConnCtx.connected) {
|
||||
return;
|
||||
}
|
||||
setPendingExecOptions(null);
|
||||
Object.entries(pendingExecOptions).forEach(([option, value])=>{
|
||||
changeQueryExecutionSettings(option, queryToolCtx.api, queryToolCtx.params.trans_id, value)
|
||||
.catch((error)=>{
|
||||
setCheckedMenuItems((prev)=>({...prev, [option]: !value}));
|
||||
if(option === 'server_cursor') {
|
||||
queryToolCtx.updateServerCursor({server_cursor: !value});
|
||||
}
|
||||
// Save the reverted value so it is not restored again next time.
|
||||
if(isSaveToolDataEnabled?.('sqleditor')) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
|
||||
}
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
||||
checkTransaction: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [pendingExecOptions, queryToolConnCtx.connected]);
|
||||
|
||||
const openFile = useCallback(()=>{
|
||||
confirmDiscard(()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE, Boolean(checkedMenuItems['open_in_new_tab']));
|
||||
|
||||
@@ -411,7 +411,8 @@ export default function Query({onTextSelect, setQtStatePartial}) {
|
||||
let connectionInfo = {
|
||||
connection_list: currentConnList,
|
||||
open_file_name: currentFile,
|
||||
is_editor_dirty: editor.current.isDirty()
|
||||
is_editor_dirty: editor.current.isDirty(),
|
||||
toolbar_options: queryToolCtx.toolbarOptionsRef?.current,
|
||||
};
|
||||
saveToolData('sqleditor', connectionInfo, currentTransId, editor.current.getValue());
|
||||
setSaveQtData(false);
|
||||
|
||||
197
web/regression/javascript/sqleditor/MainToolBarRestore.spec.js
Normal file
197
web/regression/javascript/sqleditor/MainToolBarRestore.spec.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
jest.mock('sources/url_for', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn((endpoint, args) => `/mock/${endpoint}/${args?.trans_id}`),
|
||||
}));
|
||||
|
||||
// Avoid pulling in everything QueryToolComponent imports.
|
||||
jest.mock('../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx', () => {
|
||||
const React = require('react');
|
||||
return {
|
||||
QueryToolContext: React.createContext(null),
|
||||
QueryToolConnectionContext: React.createContext(null),
|
||||
QueryToolEventsContext: React.createContext(null),
|
||||
};
|
||||
});
|
||||
|
||||
const mockIsSaveToolDataEnabled = jest.fn(() => true);
|
||||
jest.mock('../../../pgadmin/settings/static/ApplicationStateProvider', () => ({
|
||||
useApplicationState: () => ({isSaveToolDataEnabled: mockIsSaveToolDataEnabled}),
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import { render, act, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { withTheme } from '../fake_theme';
|
||||
import EventBus from '../../../pgadmin/static/js/helpers/EventBus';
|
||||
import {
|
||||
MainToolBar, getRestorableToolbarOptions, getChangedExecutionOptions,
|
||||
} from '../../../pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar';
|
||||
import {
|
||||
QueryToolContext, QueryToolConnectionContext, QueryToolEventsContext,
|
||||
} from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx';
|
||||
import { QUERY_TOOL_EVENTS } from '../../../pgadmin/tools/sqleditor/static/js/components/QueryToolConstants';
|
||||
|
||||
const sqleditorPrefs = {
|
||||
auto_commit: true,
|
||||
auto_rollback: false,
|
||||
server_cursor: false,
|
||||
open_in_new_tab: false,
|
||||
explain_verbose: false,
|
||||
explain_costs: false,
|
||||
explain_buffers: false,
|
||||
explain_timing: false,
|
||||
explain_summary: false,
|
||||
explain_settings: false,
|
||||
explain_wal: false,
|
||||
explain_generic_plan: false,
|
||||
explain_memory: false,
|
||||
};
|
||||
|
||||
describe('getRestorableToolbarOptions', () => {
|
||||
it('keeps only known options with boolean values', () => {
|
||||
expect(getRestorableToolbarOptions({
|
||||
auto_commit: false,
|
||||
open_in_new_tab: true,
|
||||
explain_verbose: 'yes',
|
||||
unknown_option: true,
|
||||
})).toEqual({auto_commit: false, open_in_new_tab: true});
|
||||
});
|
||||
|
||||
it('copes with missing saved options', () => {
|
||||
expect(getRestorableToolbarOptions(undefined)).toEqual({});
|
||||
expect(getRestorableToolbarOptions(null)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChangedExecutionOptions', () => {
|
||||
it('returns only the execute options that differ', () => {
|
||||
expect(getChangedExecutionOptions(
|
||||
{auto_commit: false, auto_rollback: false, server_cursor: true, open_in_new_tab: true},
|
||||
{auto_commit: true, auto_rollback: false, server_cursor: false},
|
||||
)).toEqual({auto_commit: false, server_cursor: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MainToolBar toolbar options restore', () => {
|
||||
let eventBus, queryToolCtx;
|
||||
const ThemedMainToolBar = withTheme(MainToolBar);
|
||||
|
||||
const renderToolBar = (connected, isQueryTool=true) => {
|
||||
const containerRef = React.createRef();
|
||||
const ui = (conn) => (
|
||||
<QueryToolContext.Provider value={queryToolCtx}>
|
||||
<QueryToolConnectionContext.Provider value={{connected: conn, obtainingConn: false, connectionStatus: null}}>
|
||||
<QueryToolEventsContext.Provider value={eventBus}>
|
||||
<div ref={containerRef}>
|
||||
<ThemedMainToolBar containerRef={containerRef} onFilterClick={()=>{/* noop */}}
|
||||
onManageMacros={()=>{/* noop */}} onAddToMacros={()=>{/* noop */}} />
|
||||
</div>
|
||||
</QueryToolEventsContext.Provider>
|
||||
</QueryToolConnectionContext.Provider>
|
||||
</QueryToolContext.Provider>
|
||||
);
|
||||
queryToolCtx.params.is_query_tool = isQueryTool;
|
||||
const result = render(ui(connected));
|
||||
return {...result, setConnected: (conn) => result.rerender(ui(conn))};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
eventBus = new EventBus();
|
||||
queryToolCtx = {
|
||||
params: {trans_id: 1234, is_query_tool: true, macros: []},
|
||||
preferences: {sqleditor: {...sqleditorPrefs}, editor: {}},
|
||||
api: {post: jest.fn(() => Promise.resolve({data: {}}))},
|
||||
modal: {},
|
||||
toolbarOptionsRef: {current: null},
|
||||
updateServerCursor: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('publishes the current options for saving with the tab state', async () => {
|
||||
renderToolBar(true);
|
||||
await waitFor(() => {
|
||||
expect(queryToolCtx.toolbarOptionsRef.current).toEqual(expect.objectContaining({
|
||||
auto_commit: true, open_in_new_tab: false,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('restores saved options and applies changed execute options once connected', async () => {
|
||||
const {setConnected} = renderToolBar(false);
|
||||
await waitFor(() => expect(queryToolCtx.toolbarOptionsRef.current).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, {
|
||||
auto_commit: false,
|
||||
auto_rollback: false,
|
||||
server_cursor: true,
|
||||
open_in_new_tab: true,
|
||||
explain_verbose: true,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryToolCtx.toolbarOptionsRef.current).toEqual(expect.objectContaining({
|
||||
auto_commit: false, server_cursor: true, open_in_new_tab: true, explain_verbose: true,
|
||||
}));
|
||||
});
|
||||
expect(queryToolCtx.updateServerCursor).toHaveBeenCalledWith({server_cursor: true});
|
||||
// Nothing can be sent to the server until the transaction exists.
|
||||
expect(queryToolCtx.api.post).not.toHaveBeenCalled();
|
||||
|
||||
setConnected(true);
|
||||
await waitFor(() => expect(queryToolCtx.api.post).toHaveBeenCalledTimes(2));
|
||||
expect(queryToolCtx.api.post).toHaveBeenCalledWith('/mock/sqleditor.auto_commit/1234', 'false');
|
||||
expect(queryToolCtx.api.post).toHaveBeenCalledWith('/mock/sqleditor.server_cursor/1234', 'true');
|
||||
});
|
||||
|
||||
it('reverts and saves a restored execute option the server rejects', async () => {
|
||||
queryToolCtx.api.post = jest.fn(() => Promise.reject(new Error('failed')));
|
||||
const fireEvent = jest.spyOn(eventBus, 'fireEvent');
|
||||
renderToolBar(true);
|
||||
await waitFor(() => expect(queryToolCtx.toolbarOptionsRef.current).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, {auto_commit: false});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fireEvent).toHaveBeenCalledWith(QUERY_TOOL_EVENTS.TRIGGER_SAVE_QUERY_TOOL_DATA);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(queryToolCtx.toolbarOptionsRef.current.auto_commit).toBe(true);
|
||||
});
|
||||
fireEvent.mockRestore();
|
||||
});
|
||||
|
||||
it('does not restore execute options for View/Edit Data', async () => {
|
||||
renderToolBar(true, false);
|
||||
await waitFor(() => expect(queryToolCtx.toolbarOptionsRef.current).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESTORE_TOOLBAR_OPTIONS, {
|
||||
auto_commit: false, explain_costs: true,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryToolCtx.toolbarOptionsRef.current).toEqual(expect.objectContaining({
|
||||
auto_commit: true, explain_costs: true,
|
||||
}));
|
||||
});
|
||||
expect(queryToolCtx.api.post).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user