Added support for executing the query at the cursor position in the query tool. #6841

This commit is contained in:
Anil Sahoo 2024-05-27 16:11:59 +05:30 committed by GitHub
parent 16b9b103a2
commit 6690b16f8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 543 additions and 110 deletions

View File

@ -8,6 +8,7 @@ Use *Clear Saved Password* functionality to clear the saved password for the
database server.
.. image:: images/clear_saved_password.png
:alt: Clear saved password
:align: center
*Clear Saved Password* shows in the context menu for the selected server as well
@ -17,6 +18,7 @@ Use *Clear SSH Tunnel Password* functionality to clear the saved password of SSH
Tunnel to connect to the database server.
.. image:: images/clear_tunnel_password.png
:alt: Clear SSH tunnel password
:align: center
*Clear SSH Tunnel Password* shows in the context menu for the selected server as

View File

@ -11,6 +11,7 @@ and select *Connect Server...* from the context menu.
.. image:: images/connect_to_server.png
:alt: Connect to server dialog
:align: center
Provide authentication information for the selected server:
@ -26,6 +27,7 @@ Tunnel and Database server passwords if not already saved.
.. image:: images/connect_to_tunneled_server.png
:alt: Connect to server dialog
:align: center
Provide authentication information for the selected server:

View File

@ -61,6 +61,7 @@ values required by the program:
.. image:: images/debug_params.png
:alt: Debugger parameter dialog
:align: center
Use the fields on the *Debugger* dialog to provide a value for each parameter:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -399,9 +399,6 @@ Use the fields on the *Editor* panel to change settings of the query editor.
changed to text/plain. Keyword highlighting and code folding will be disabled.
This will improve editor performance with large files.
* When the *Show View/Edit Data Promotion Warning?* switch is set to *True*
View/Edit Data tool will show promote to Query tool confirm dialog on query edit.
.. image:: images/preferences_sql_explain.png
:alt: Preferences dialog sqleditor explain options
:align: center
@ -474,6 +471,16 @@ Use the fields on the *Options* panel to manage editor preferences.
by the Primary Key columns by default. When using the First/Last 100 Rows options,
data is always sorted.
* When the *Show View/Edit Data Promotion Warning?* switch is set to *True*
View/Edit Data tool will show promote to Query tool confirm dialog on query edit.
* When the *Underline query at cursor?* switch is set to *True*, query tool will
parse and underline the query at the cursor position.
* When the *Underlined query execute warning?* switch is set to *True*, query tool
will warn upon clicking the *Execute Query* button in the query tool. The warning
will appear only if *Underline query at cursor?* is set to *False*.
.. image:: images/preferences_sql_results_grid.png
:alt: Preferences dialog sql results grid section
:align: center
@ -503,7 +510,7 @@ preferences for copied data.
:align: center
Use the fields on the *Keyboard shortcuts* panel to configure shortcuts for the
Query Tool window navigation:
Query Tool window navigation.
.. image:: images/preferences_sql_formatting.png
:alt: Preferences dialog SQL Formatting section

View File

@ -26,13 +26,12 @@ allows you to:
:align: center
You can open multiple copies of the Query tool in individual tabs
simultaneously. To close a copy of the Query tool, click the *X* in the
upper-right hand corner of the tab bar.
simultaneously. To close a copy of the Query tool, click the *X* of the tab.
The Query Tool features two panels:
* The upper panel displays the *SQL Editor*. You can use the panel to enter,
edit, or execute a query. It also shows the *History* tab which can be used
edit, or execute a query or a script. It also shows the *History* tab which can be used
to view the queries that have been executed in the session, and a *Scratch Pad*
which can be used to hold text snippets during editing. If the Scratch Pad is
closed, it can be re-opened (or additional ones opened) by right-clicking in
@ -75,14 +74,30 @@ key combination to select from a popup menu of autocomplete options.
After entering a query, select the *Execute script* icon from the toolbar. The
complete contents of the SQL editor panel will be sent to the database server
for execution. To execute only a section of the code that is displayed in the
SQL editor, highlight the text that you want the server to execute, and click
the *Execute script* icon.
for execution. To execute only a section of the code that is displayed in the
SQL editor, highlight the text that you want the server to execute, and click the
*Execute script* icon.
.. image:: images/query_execute_section.png
.. image:: images/query_execute_script.png
:alt: Query tool execute script section
:align: center
You can also execute a query based on cursor position. Query tool will detect
a query and underline it when cursor position changes. Now, to execute the
current underlined query, hit the *Execute query* button on the toolbar. If a section
is highlighted then it will behave like normal execute.
.. image:: images/query_execute_query.png
:alt: Query tool execute query section
:align: center
The warning will appear only if *Underline query at cursor?* is set to *False* and
the *Underlined query execute warning?* switch is set to *True* Preferences Query tool's Options.
.. image:: images/query_execute_warning.png
:alt: Query tool execute query warning
:align: center
The message returned by the server when a command executes is displayed on the
*Messages* tab. If the command is successful, the *Messages* tab displays
execution details.
@ -159,8 +174,6 @@ The *Data Output* tab displays the result set of the query in a table format.
You can:
* Select and copy from the displayed result set.
* Use the *Execute script* options to retrieve query execution information and
set query execution options.
* Use the *Save results to file* icon to save the content of the *Data Output*
tab as a comma-delimited file.
* Edit the data in the result set of a SELECT query if it is updatable.

View File

@ -128,6 +128,9 @@ Query Execution
| | transaction. Any changes made by the transaction will be visible to others, and | |
| | durable in the event of a crash. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Execute query* | Click the *Execute query* icon to either execute the query where the cursor is present or | Option+F5 (MAC)|
| | refresh the query highlighted in the SQL editor panel. | Alt+F5 (Others)|
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| *Explain* | Click the *Explain* icon to view an explanation plan for the current query. The result of the | F7 |
| | EXPLAIN is displayed graphically on the *Explain* tab of the output panel, and in text | |
| | form on the *Data Output* tab. | |
@ -206,6 +209,8 @@ Data Editing Options
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| Graph Visualiser | Use the Graph Visualiser button to generate graphs of the query results. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
| SQL | Use the SQL button to check the current query that gave the data. | |
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
Status Bar
**********

View File

@ -8,6 +8,8 @@ Create Date: 2024-05-17 19:35:03.700104
"""
from alembic import op
import sqlalchemy as sa
from pgadmin.model import Preferences
from pgadmin.model import db
# revision identifiers, used by Alembic.
revision = 'ac2c2e27dc2d'
@ -17,6 +19,10 @@ depends_on = None
def upgrade():
db.session.query(Preferences).filter(
Preferences.name == 'execute_query').update({'name': 'execute_script'})
db.session.commit()
meta = sa.MetaData()
meta.reflect(op.get_bind(), only=('user_macros',))
user_macros_table = sa.Table('user_macros', meta)

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15px" height="15px" viewBox="0 0 15 15" version="1.1">
<g id="surface1">
<path d="M 1.5625 2.144531 L 1.5625 13.011719 C 1.5625 13.308594 1.679688 13.558594 1.910156 13.761719 C 2.140625 13.960938 2.410156 14.0625 2.71875 14.0625 C 2.816406 14.0625 2.917969 14.050781 3.023438 14.023438 C 3.128906 13.996094 3.230469 13.957031 3.328125 13.90625 L 5.53125 12.636719 L 5.53125 9.910156 L 4.0625 10.339844 L 4.0625 7.609375 L 7.535156 6.40625 L 8.585938 6.40625 L 8.585938 10.875 L 12.761719 8.472656 C 12.933594 8.367188 13.0625 8.234375 13.152344 8.078125 C 13.238281 7.917969 13.28125 7.753906 13.28125 7.578125 C 13.28125 7.402344 13.238281 7.238281 13.152344 7.078125 C 13.0625 6.921875 12.933594 6.789062 12.761719 6.683594 L 3.328125 1.25 C 3.230469 1.199219 3.128906 1.160156 3.023438 1.132812 C 2.917969 1.105469 2.816406 1.09375 2.71875 1.09375 C 2.410156 1.09375 2.140625 1.195312 1.910156 1.394531 C 1.679688 1.597656 1.5625 1.847656 1.5625 2.144531 Z M 7.804688 11.324219 L 7.804688 7.1875 L 7.667969 7.1875 L 4.84375 8.164062 L 4.84375 9.296875 L 6.3125 8.867188 L 6.3125 12.183594 Z M 7.804688 11.324219 "/>
<path d="M 7.804688 7.1875 L 7.804688 13.628906 L 6.3125 13.628906 L 6.3125 8.867188 L 4.84375 9.296875 L 4.84375 8.164062 L 7.667969 7.1875 Z M 7.804688 7.1875 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>SQL</title><polygon points="24 21 24 9 22 9 22 23 30 23 30 21 24 21"/><path d="M18,9H14a2,2,0,0,0-2,2V21a2,2,0,0,0,2,2h1v2a2,2,0,0,0,2,2h2V25H17V23h1a2,2,0,0,0,2-2V11A2,2,0,0,0,18,9ZM14,21V11h4V21Z"/><path d="M8,23H2V21H8V17H4a2,2,0,0,1-2-2V11A2,2,0,0,1,4,9h6v2H4v4H8a2,2,0,0,1,2,2v4A2,2,0,0,1,8,23Z"/><rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/></svg>

After

Width:  |  Height:  |  Size: 709 B

View File

@ -133,7 +133,7 @@ export default function(basicSettings) {
foldmarker: '#0000FF',
activeline: '#323e43',
activelineLight: '#323e43',
activelineBorderColor: 'none',
currentQueryBorderColor: '#A5CBE2',
guttersBg: '#303030',
guttersFg: '#8A8A8A',
},

View File

@ -132,7 +132,7 @@ export default function(basicSettings) {
foldmarker: '#FFFFFF',
activeline: '#063057',
activelineLight: '#063057',
activelineBorderColor: 'none',
currentQueryBorderColor: '#A5CBE2',
guttersBg: '#2d3a48',
guttersFg: '#8b9cac',
},

View File

@ -335,6 +335,9 @@ function getFinalTheme(baseTheme) {
},
right: {
borderRight: '1px solid '+baseTheme.otherVars.borderColor,
},
left: {
borderLeft: '1px solid '+baseTheme.otherVars.borderColor,
}
},
nodeIcon: {

View File

@ -39,6 +39,10 @@ export default function cmOverride(theme) {
backgroundColor: editor.activeline,
},
'& .cm-current-query': {
borderBottom: `1px solid ${editor.currentQueryBorderColor}`
},
'& .tok-keyword': {
color: editor.keyword,
fontWeight: 600

View File

@ -156,7 +156,7 @@ export default function(basicSettings) {
foldmarker: '#0000FF',
activeline: '#EDF9FF',
activelineLight: '#EDF9FF',
activelineBorderColor: '#BCDEF3',
currentQueryBorderColor: '#A5CBE2',
guttersBg: '#f3f5f9',
guttersFg: '#848ea0',
},

View File

@ -18,6 +18,8 @@ import AWS from '../../img/aws.svg?svgr';
import BigAnimal from '../../img/biganimal.svg?svgr';
import Azure from '../../img/azure.svg?svgr';
import SQLFileSvg from '../../img/sql_file.svg?svgr';
import SQLQuerySvg from '../../img/sql_query.svg?svgr';
import ExecuteQuerySvg from '../../img/execute_query.svg?svgr';
import MagicSvg from '../../img/magic.svg?svgr';
import MsAzure from '../../img/ms_azure.svg?svgr';
import GoogleCloud from '../../img/google-cloud-1.svg?svgr';
@ -96,6 +98,12 @@ GoogleCloudIcon.propTypes = {style: PropTypes.object};
export const SQLFileIcon = ({style})=><ExternalIcon Icon={SQLFileSvg} style={{height: '1rem', ...style}} data-label="SQLFileIcon" />;
SQLFileIcon.propTypes = {style: PropTypes.object};
export const SQLQueryIcon = ({style})=><ExternalIcon Icon={SQLQuerySvg} style={{height: '2rem', ...style}} data-label="SQLQueryIcon" />;
SQLQueryIcon.propTypes = {style: PropTypes.object};
export const ExecuteQueryIcon = ({style})=><ExternalIcon Icon={ExecuteQuerySvg} style={style} data-label="ExecuteQueryIcon" />;
ExecuteQueryIcon.propTypes = {style: PropTypes.object};
export const MagicIcon = ({style})=><ExternalIcon Icon={MagicSvg} style={{height: '1rem', ...style}} data-label="MagicIcon" />;
MagicIcon.propTypes = {style: PropTypes.object};

View File

@ -6,6 +6,7 @@ import { syntaxTree } from '@codemirror/language';
import { autocompletion } from '@codemirror/autocomplete';
import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands';
import { errorMarkerEffect } from './extensions/errorMarker';
import { currentQueryHighlighterEffect } from './extensions/currentQueryHighlighter';
import { activeLineEffect, activeLineField } from './extensions/activeLineMarker';
import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter';
@ -102,7 +103,7 @@ export default class CustomEditorView extends EditorView {
// We already had found valid text
if(validTextFound) {
// continue till it reaches start so we can check for empty lines, etc.
if(statementStartPos > 0 && statementStartPos < startPos) {
if(statementStartPos >= 0 && statementStartPos < startPos) {
startPos -= 1;
continue;
}
@ -166,10 +167,18 @@ export default class CustomEditorView extends EditorView {
if(startPos < 0) startPos = 0;
if(endPos > this.state.doc.length) endPos = this.state.doc.length;
return this.state.sliceDoc(startPos, endPos).trim();
return {
value: this.state.sliceDoc(startPos, endPos).trim(),
from: startPos,
to: endPos,
};
} catch (error) {
console.error(error);
return this.getValue();
return {
value: '',
from: 0,
to: 0,
};
}
}
@ -314,4 +323,8 @@ export default class CustomEditorView extends EditorView {
let scrollEffect = line >= 0 ? [EditorView.scrollIntoView(this.state.doc.line(line).from, {y: 'center'})] : [];
this.dispatch({ effects: [activeLineEffect.of({ from: line, to: line })].concat(scrollEffect) });
}
setQueryHighlightMark(from,to) {
this.dispatch({ effects: currentQueryHighlighterEffect.of({ from, to }) });
}
}

View File

@ -45,6 +45,7 @@ import errorMarkerExtn from '../extensions/errorMarker';
import CustomEditorView from '../CustomEditorView';
import breakpointGutter, { breakpointEffect } from '../extensions/breakpointGutter';
import activeLineExtn from '../extensions/activeLineMarker';
import currentQueryHighlighterExtn from '../extensions/currentQueryHighlighter';
const arrowRightHtml = ReactDOMServer.renderToString(<KeyboardArrowRightRoundedIcon style={{fontSize: '1.2em'}} />);
const arrowDownHtml = ReactDOMServer.renderToString(<ExpandMoreRoundedIcon style={{fontSize: '1.2em'}} />);
@ -329,6 +330,9 @@ export default function Editor({
if (pref.brace_matching) {
newConfigExtn.push(bracketMatching());
}
if (pref.underline_query_cursor){
newConfigExtn.push(currentQueryHighlighterExtn());
}
editor.current.dispatch({
effects: configurables.current.reconfigure(newConfigExtn)

View File

@ -0,0 +1,32 @@
import {
EditorView,
Decoration,
} from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export const currentQueryHighlighterEffect = StateEffect.define({
map: ({ from, to }, change) => ({ from: change.mapPos(from), to: change.mapPos(to) })
});
const currentQueryHighlighterDeco = Decoration.mark({ class: 'cm-current-query' });
export const currentQueryHighlighterField = StateField.define({
create() {
return Decoration.none;
},
update(value, tr) {
value = value.map(tr.changes);
for (let e of tr.effects) if (e.is(currentQueryHighlighterEffect)) {
if (e.value.from < e.value.to) {
return Decoration.set([currentQueryHighlighterDeco.range(e.value.from, e.value.to)]);
}
return Decoration.none;
}
return value;
},
provide: f => EditorView.decorations.from(f)
});
export default function currentQueryHighlighterExtn() {
return [currentQueryHighlighterField];
}

View File

@ -26,6 +26,7 @@ export const QUERY_TOOL_EVENTS = {
TRIGGER_REMOVE_FILTER: 'TRIGGER_REMOVE_FILTER',
TRIGGER_SET_LIMIT: 'TRIGGER_SET_LIMIT',
TRIGGER_FORMAT_SQL: 'TRIGGER_FORMAT_SQL',
TRIGGER_GRAPH_VISUALISER: 'TRIGGER_GRAPH_VISUALISER',
COPY_DATA: 'COPY_DATA',
SET_LIMIT_VALUE: 'SET_LIMIT_VALUE',
@ -67,6 +68,7 @@ export const QUERY_TOOL_EVENTS = {
WARN_SAVE_DATA_CLOSE: 'WARN_SAVE_DATA_CLOSE',
WARN_SAVE_TEXT_CLOSE: 'WARN_SAVE_TEXT_CLOSE',
WARN_TXN_CLOSE: 'WARN_TXN_CLOSE',
EXECUTE_CURSOR_WARNING: 'EXECUTE_CURSOR_WARNING',
RESET_LAYOUT: 'RESET_LAYOUT',
FORCE_CLOSE_PANEL: 'FORCE_CLOSE_PANEL',

View File

@ -120,7 +120,7 @@ function isValidArray(val) {
return !(val != '' && (val.charAt(0) != '{' || val.charAt(val.length - 1) != '}'));
}
function setEditorPosition(cellEle, editorEle) {
export function setEditorPosition(cellEle, editorEle, closestEle, topValue) {
if(!editorEle || !cellEle) {
return;
}
@ -128,12 +128,12 @@ function setEditorPosition(cellEle, editorEle) {
if(editorEle.style.left || editorEle.style.top) {
return;
}
let gridEle = cellEle.closest('.rdg');
let gridEle = cellEle.closest(closestEle);
let cellRect = cellEle.getBoundingClientRect();
let gridEleRect = gridEle.getBoundingClientRect();
let position = {
left: cellRect.left,
top: Math.max(cellRect.top - editorEle.offsetHeight + 12, 0)
top: Math.max(cellRect.top - editorEle.offsetHeight + topValue, 0)
};
if ((position.left + editorEle.offsetWidth + 10) > gridEle.offsetWidth) {
@ -204,7 +204,7 @@ export function TextEditor({row, column, onRowChange, onClose}) {
return(
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
setEditorPosition(getCellElement(column.idx), ele, '.rdg', 12);
}} className={classes.textEditor} data-label="pg-editor" onKeyDown={suppressEnterKey} >
<textarea ref={autoFocusAndSelect} className={classes.textarea} value={localVal} onChange={onChange} />
<Box display="flex" justifyContent="flex-end">
@ -374,7 +374,7 @@ export function JsonTextEditor({row, column, onRowChange, onClose}) {
return (
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
setEditorPosition(getCellElement(column.idx), ele, '.rdg', 12);
}} className={classes.jsonEditor} data-label="pg-editor" onKeyDown={suppressEnterKey} >
<JsonEditor
value={localVal}

View File

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { styled } from '@mui/styles';
import gettext from 'sources/gettext';
import { Box } from '@mui/material';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@mui/icons-material/CloseRounded';
import PropTypes from 'prop-types';
import CheckRounded from '@mui/icons-material/CheckRounded';
import { InputCheckbox } from '../../../../../../static/js/components/FormComponents';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
const StyledEditor = styled('div')(({theme})=>({
flexGrow: 1,
backgroundColor: theme.palette.background.default,
padding: '1rem',
'& .textarea': {
...theme.mixins.panelBorder?.all,
outline: 0,
resize: 'both',
maxHeight: '500px',
overflowY: 'scroll',
},
}));
const StyledFooter = styled('div')(({theme})=>({
display: 'flex',
justifyContent: 'space-between',
padding: '0.5rem',
...theme.mixins.panelBorder?.top,
'& .margin': {
marginLeft: '0.25rem',
},
}));
export default function ConfirmExecuteQueryContent({ onContinue, onClose, closeModal, text }) {
const [formData, setFormData] = useState({
save_user_choice: false
});
return (
<Box display="flex" flexDirection="column" height="100%">
<StyledEditor>
<Box>Do you want to run this query -</Box>
<CodeMirror
value={text || ''}
className={'textarea'}
readonly={true}
/>
</StyledEditor>
<StyledFooter>
<InputCheckbox controlProps={{ label: gettext('Don\'t ask again') }} value={formData['save_user_choice']}
onChange={(e) => setFormData((prev) => ({ ...prev, 'save_user_choice': e.target.checked }))} />
<Box>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
onClose?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="Continue" className={'margin'} startIcon={<CheckRounded />} onClick={() => {
let postFormData = new FormData();
postFormData.append('pref_data', JSON.stringify([{ 'name': 'underlined_query_execute_warning', 'value': !formData.save_user_choice, 'module': 'sqleditor' }]));
onContinue?.(postFormData);
closeModal();
}} autoFocus={true} >{gettext('Continue')}</PrimaryButton>
</Box>
</StyledFooter>
</Box>
);
}
ConfirmExecuteQueryContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onContinue: PropTypes.func,
onClose: PropTypes.func
};

View File

@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import { styled } from '@mui/styles';
import gettext from 'sources/gettext';
import { Box } from '@mui/material';
import { makeStyles } from '@mui/styles';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@mui/icons-material/CloseRounded';
import HTMLReactParser from 'html-react-parser';
@ -10,48 +9,40 @@ import PropTypes from 'prop-types';
import CheckRounded from '@mui/icons-material/CheckRounded';
import { InputCheckbox } from '../../../../../../static/js/components/FormComponents';
const useStyles = makeStyles(() => ({
saveChoice: {
margin: '10px 0 10px 10px',
}
const StyledFooter = styled('div')(({theme})=>({
display: 'flex',
justifyContent: 'space-between',
padding: '0.5rem',
...theme.mixins.panelBorder?.top,
'& .margin': {
marginLeft: '0.25rem',
},
}));
export default function ConfirmPromotionContent({ onContinue, onClose, closeModal, text }) {
const [formData, setFormData] = useState({
save_user_choice: false
});
const onDataChange = (e, id) => {
let val = e;
if (e?.target) {
val = e.target.value;
}
setFormData((prev) => ({ ...prev, [id]: val }));
};
const modalClasses = useModalStyles();
const classes = useStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof (text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.saveChoice}>
<StyledFooter>
<InputCheckbox controlProps={{ label: gettext('Don\'t ask again') }} value={formData['save_user_choice']}
onChange={(e) => onDataChange(e.target.checked, 'save_user_choice')} />
</Box>
<Box className={modalClasses.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
onClose?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="Continue" className={modalClasses.margin} startIcon={<CheckRounded />} onClick={() => {
let postFormData = new FormData();
postFormData.append('pref_data', JSON.stringify([{ 'name': 'view_edit_promotion_warning', 'value': !formData.save_user_choice, 'module': 'sqleditor' }]));
onContinue?.(postFormData);
closeModal();
}} autoFocus={true} >{gettext('Continue')}</PrimaryButton>
</Box>
onChange={(e) => setFormData((prev) => ({ ...prev, 'save_user_choice': e.target.checked }))} />
<Box>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
onClose?.();
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="Continue" className={'margin'} startIcon={<CheckRounded />} onClick={() => {
let postFormData = new FormData();
postFormData.append('pref_data', JSON.stringify([{ 'name': 'view_edit_promotion_warning', 'value': !formData.save_user_choice, 'module': 'sqleditor' }]));
onContinue?.(postFormData);
closeModal();
}} autoFocus={true} >{gettext('Continue')}</PrimaryButton>
</Box>
</StyledFooter>
</Box>
);
}

View File

@ -15,7 +15,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import SaveRoundedIcon from '@mui/icons-material/SaveRounded';
import StopRoundedIcon from '@mui/icons-material/StopRounded';
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
import { FilterIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import { FilterIcon, CommitIcon, RollbackIcon, ExecuteQueryIcon } from '../../../../../../static/js/components/ExternalIcon';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import AssessmentRoundedIcon from '@mui/icons-material/AssessmentRounded';
import ExplicitRoundedIcon from '@mui/icons-material/ExplicitRounded';
@ -88,7 +88,14 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const executeQuery = useCallback(()=>{
const executeCursor = useCallback(()=>{
if(!queryToolCtx.preferences.sqleditor.underline_query_cursor && queryToolCtx.preferences.sqleditor.underlined_query_execute_warning){
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTE_CURSOR_WARNING);
} else {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION,true);
}
}, [queryToolCtx.preferences.sqleditor]);
const executeScript = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}, []);
const cancelQuery = useCallback(()=>{
@ -96,7 +103,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
}, []);
const explain = useCallback((analyze=false)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, false, {
format: 'json',
analyze: analyze,
verbose: Boolean(checkedMenuItems['explain_verbose']),
@ -278,7 +285,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, true);
};
const executeMacro = (m)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION,false, null, m.sql);
};
const onLimitChange=(e)=>{
setLimit(e.target.value);
@ -389,9 +396,15 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
}
},
{
shortcut: queryToolPref.execute_query,
shortcut: queryToolPref.execute_script,
options: {
callback: ()=>{!buttonsDisabled['execute']&&executeQuery();}
callback: ()=>{!buttonsDisabled['execute']&&executeScript();}
}
},
{
shortcut: queryToolPref.execute_cursor,
options: {
callback: ()=>{!buttonsDisabled['execute']&&executeCursor();}
}
},
{
@ -521,7 +534,9 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
<PgIconButton title={gettext('Cancel query')} icon={<StopRoundedIcon style={{height: 'unset'}} />}
onClick={cancelQuery} disabled={buttonsDisabled['cancel']} shortcut={queryToolPref.btn_cancel_query} />
<PgIconButton title={gettext('Execute script')} icon={<PlayArrowRoundedIcon style={{height: 'unset'}} />}
onClick={executeQuery} disabled={buttonsDisabled['execute']} shortcut={queryToolPref.execute_query}/>
onClick={executeScript} disabled={buttonsDisabled['execute']} shortcut={queryToolPref.execute_script}/>
<PgIconButton title={gettext('Execute query')} icon={<ExecuteQueryIcon style={{padding: '2px 5px'}} />}
onClick={executeCursor} disabled={buttonsDisabled['execute'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.execute_cursor}/>
<PgIconButton title={gettext('Execute options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-autocommit" ref={autoCommitMenuRef} shortcut={queryToolPref.btn_execute_options}
onClick={toggleMenu} disabled={buttonsDisabled['execute-options']}/>

View File

@ -21,6 +21,7 @@ import { checkTrojanSource, isShortcutValue, toCodeMirrorKey } from '../../../..
import { parseApiError } from '../../../../../../static/js/api_instance';
import { usePgAdmin } from '../../../../../../static/js/BrowserComponent';
import ConfirmPromotionContent from '../dialogs/ConfirmPromotionContent';
import ConfirmExecuteQueryContent from '../dialogs/ConfirmExecuteQueryContent';
import usePreferences from '../../../../../../preferences/static/js/store';
import { getTitle } from '../../sqleditor_title';
import PropTypes from 'prop-types';
@ -74,7 +75,7 @@ export default function Query({onTextSelect}) {
const queryToolPref = queryToolCtx.preferences.sqleditor;
const highlightError = (cmObj, {errormsg: result, data})=>{
const highlightError = (cmObj, {errormsg: result, data}, executeCursor)=>{
let errorLineNo = 0,
startMarker = 0,
endMarker = 0,
@ -84,9 +85,9 @@ export default function Query({onTextSelect}) {
cmObj.removeErrorMark();
// In case of selection we need to find the actual line no
if (cmObj.getSelection().length > 0) {
if (cmObj.getSelection().length > 0 || executeCursor) {
selectedLineNo = cmObj.getCurrentLineNo();
origQueryLen = cmObj.line(selectedLineNo).length;
origQueryLen = cmObj.getLine(selectedLineNo).length;
}
// Fetch the LINE string using regex from the result
@ -144,7 +145,7 @@ export default function Query({onTextSelect}) {
}
};
const triggerExecution = (explainObject, macroSQL)=>{
const triggerExecution = (executeCursor=false, explainObject, macroSQL)=>{
if(queryToolCtx.params.is_query_tool) {
let external = null;
let query = editor.current?.getSelection();
@ -152,12 +153,15 @@ export default function Query({onTextSelect}) {
const regex = /\$SELECTION\$/gi;
query = macroSQL.replace(regex, query);
external = true;
} else{
} else if(executeCursor) {
/* Execute query at cursor position */
query = query || editor.current?.getQueryAt(editor.current?.state.selection.head).value || '';
} else {
/* Normal execution */
query = query || editor.current?.getValue() || '';
}
if(query) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, external, null);
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, external, null, executeCursor);
}
} else {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null);
@ -170,10 +174,11 @@ export default function Query({onTextSelect}) {
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, triggerExecution);
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTE_CURSOR_WARNING, checkUnderlineQueryCursorWarning);
eventBus.registerListener(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, (result)=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, (result, executeCursor)=>{
if(result) {
highlightError(editor.current, result);
highlightError(editor.current, result, executeCursor);
} else {
editor.current.removeErrorMark();
}
@ -385,6 +390,11 @@ export default function Query({onTextSelect}) {
}, [queryToolCtx.params.trans_id]);
const cursorActivity = useCallback(_.debounce((cursor)=>{
if (queryToolCtx.preferences.sqleditor.underline_query_cursor){
let {from, to}=editor.current.getQueryAt(editor.current?.state.selection.head);
editor.current.setQueryHighlightMark(from,to);
}
lastCursorPos.current = cursor;
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [lastCursorPos.current.line, lastCursorPos.current.ch+1]);
}, 100), []);
@ -434,6 +444,28 @@ export default function Query({onTextSelect}) {
});
};
const checkUnderlineQueryCursorWarning = () => {
let query = editor.current?.getSelection();
query = query || editor.current?.getQueryAt(editor.current?.state.selection.head).value || '';
query && queryToolCtx.modal.showModal(gettext('Execute query'), (closeModal) =>{
return (<ConfirmExecuteQueryContent
closeModal={closeModal}
text={query}
onContinue={(formData)=>{
preferencesStore.setPreference(formData);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION,true);
}}
onClose={()=>{
closeModal?.();
}}
/>);
}, {
onClose:(closeModal)=>{
closeModal?.();
}
});
};
const promoteToQueryTool = () => {
if(!queryToolCtx.params.is_query_tool){
queryToolCtx.toggleQueryTool();

View File

@ -181,7 +181,7 @@ export class ResultSetUtils {
}
async startExecution(query, explainObject, onIncorrectSQL, flags={
isQueryTool: true, external: false, reconnect: false
isQueryTool: true, external: false, reconnect: false, executeCursor: false
}) {
let startTime = new Date();
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
@ -232,7 +232,7 @@ export class ResultSetUtils {
is_pgadmin_query: false,
});
if(!flags.external) {
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, httpMessageData.data.result);
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, httpMessageData.data.result, flags.executeCursor);
}
}
} catch(e) {
@ -241,7 +241,7 @@ export class ResultSetUtils {
e,
{
connectionLostCallback: ()=>{
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, flags.external, true);
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, flags.external, true, flags.executeCursor);
},
checkTransaction: true,
}
@ -282,7 +282,7 @@ export class ResultSetUtils {
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.MESSAGES);
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, error.response.data.data?.transaction_status);
if (!flags.external) {
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, parseApiError(error, true));
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, parseApiError(error, true), flags.executeCursor);
}
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, {
status: false,
@ -296,7 +296,7 @@ export class ResultSetUtils {
});
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
connectionLostCallback: ()=>{
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, flags.external, true);
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, flags.external, true, flags.executeCursor);
},
checkTransaction: true,
});
@ -751,6 +751,7 @@ export function ResultSet() {
const queryToolCtx = useContext(QueryToolContext);
const layoutDocker = useContext(LayoutDockerContext);
const [loaderText, setLoaderText] = useState('');
const [dataOutputQuery,setDataOutputQuery] = useState('');
const [queryData, setQueryData] = useState(null);
const [rows, setRows] = useState([]);
const [columns, setColumns] = useState([]);
@ -791,7 +792,7 @@ export function ResultSet() {
eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length);
};
const executionStartCallback = async (query, explainObject, external=false, reconnect=false)=>{
const executionStartCallback = async (query, explainObject, external=false, reconnect=false, executeCursor=false)=>{
const yesCallback = async ()=>{
/* Reset */
eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null);
@ -800,13 +801,14 @@ export function ResultSet() {
setSelectedColumns(new Set());
rsu.current.resetClientPKIndex();
setLoaderText(gettext('Waiting for the query to complete...'));
setDataOutputQuery(query);
return await rsu.current.startExecution(
query, explainObject,
()=>{
setColumns([]);
setRows([]);
},
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect}
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor}
);
};
@ -835,7 +837,7 @@ export function ResultSet() {
setRows([]);
},
explainObject,
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect}
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor}
);
};
@ -1386,7 +1388,7 @@ export function ResultSet() {
<EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/>
}
{queryData && <>
<ResultSetToolbar containerRef={containerRef} canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}/>
<ResultSetToolbar containerRef={containerRef} query={dataOutputQuery} canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}/>
<Box flexGrow="1" minHeight="0">
<QueryToolDataGrid
columns={columns}

View File

@ -7,15 +7,15 @@
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@mui/styles';
import { Box } from '@mui/material';
import { styled } from '@mui/styles';
import { Portal } from '@mui/material';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import PlaylistAddRoundedIcon from '@mui/icons-material/PlaylistAddRounded';
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded';
import { PasteIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
import {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
@ -24,23 +24,37 @@ import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import CopyData from '../QueryToolDataGrid/CopyData';
import PropTypes from 'prop-types';
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
import { setEditorPosition } from '../QueryToolDataGrid/Editors';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
},
const StyledDiv = styled('div')(({theme})=>({
padding: '2px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
}));
export function ResultSetToolbar({canEdit, totalRowCount}) {
const classes = useStyles();
const StyledEditor = styled('div')(({theme})=>({
position: 'absolute',
backgroundColor: theme.palette.background.default,
fontSize: '12px',
...theme.mixins.panelBorder.all,
maxWidth:'50%',
overflow:'auto',
maxHeight:'35%',
'& .textarea': {
border: 0,
outline: 0,
resize: 'both',
}
}));
export function ResultSetToolbar({query,canEdit, totalRowCount}) {
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false);
const [buttonsDisabled, setButtonsDisabled] = useState({
'save-data': true,
'delete-rows': true,
@ -168,9 +182,31 @@ export function ResultSetToolbar({canEdit, totalRowCount}) {
},
], queryToolCtx.mainContainerRef);
function suppressEnterKey(e) {
if(e.keyCode == 13) {
e.stopPropagation();
}
}
const ShowDataOutputQueryPopup =()=> {
return (
<Portal container={document.body}>
<StyledEditor ref={(ele)=>{
setEditorPosition(document.getElementById('sql-query'), ele, '.MuiBox-root', 29);
}} onKeyDown={suppressEnterKey}>
<CodeMirror
value={query || ''}
className={'textarea'}
readonly={true}
/>
</StyledEditor>
</Portal>
);
};
return (
<>
<Box className={classes.root}>
<StyledDiv>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} />
@ -198,7 +234,16 @@ export function ResultSetToolbar({canEdit, totalRowCount}) {
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
</PgButtonGroup>
</Box>
{query &&
<>
<PgButtonGroup size="small">
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />}
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/>
</PgButtonGroup>
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup />}
</>
}
</StyledDiv>
<PgMenu
anchorRef={copyMenuRef}
open={menuOpenId=='menu-copyheader'}
@ -220,6 +265,7 @@ export function ResultSetToolbar({canEdit, totalRowCount}) {
}
ResultSetToolbar.propTypes = {
query: PropTypes.string,
canEdit: PropTypes.bool,
totalRowCount: PropTypes.number,
};

View File

@ -129,6 +129,40 @@ def register_query_tool_preferences(self):
)
)
self.view_edit_promotion_warning = self.preference.register(
'Options', 'view_edit_promotion_warning',
gettext("Show View/Edit Data Promotion Warning?"),
'boolean', True,
category_label=PREF_LABEL_OPTIONS,
help_str=gettext(
'If set to True, View/Edit Data tool will show promote to '
'Query tool confirm dialog on query edit.'
)
)
self.underline_query_cursor = self.preference.register(
'Options', 'underline_query_cursor',
gettext("Underline query at cursor?"),
'boolean', True,
category_label=PREF_LABEL_OPTIONS,
help_str=gettext(
'If set to True, query tool will parse and underline '
'the query at the cursor position.'
)
)
self.underlined_query_execute_warning = self.preference.register(
'Options', 'underlined_query_execute_warning',
gettext("Underlined query execute warning?"),
'boolean', True,
category_label=PREF_LABEL_OPTIONS,
help_str=gettext(
'If set to True, query tool will warn upon clicking the '
'Execute Query button in the query tool. The warning will '
'appear only if Underline query at cursor? is set to False.'
)
)
self.sql_font_size = self.preference.register(
'Editor', 'plain_editor_mode',
gettext("Plain text mode?"), 'boolean', False,
@ -180,17 +214,6 @@ def register_query_tool_preferences(self):
)
)
self.view_edit_promotion_warning = self.preference.register(
'Editor', 'view_edit_promotion_warning',
gettext("Show View/Edit Data Promotion Warning?"),
'boolean', True,
category_label=PREF_LABEL_OPTIONS,
help_str=gettext(
'If set to True, View/Edit Data tool will show promote to '
'Query tool confirm dialog on query edit.'
)
)
self.csv_quoting = self.preference.register(
'CSV_output', 'csv_quoting',
gettext("CSV quoting"), 'options', 'strings',
@ -366,13 +389,31 @@ def register_query_tool_preferences(self):
self.preference.register(
'keyboard_shortcuts',
'execute_query',
'execute_script',
gettext('Execute script'),
'keyboardshortcut',
{
'alt': False,
'shift': False,
'control': False,
'key': {
'key_code': 116,
'char': 'F5'
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(
'keyboard_shortcuts',
'execute_cursor',
gettext('Execute query'),
'keyboardshortcut',
{
'alt': True,
'shift': False,
'control': False,
'ctrl_is_meta': False,
'key': {
'key_code': 116,
@ -380,6 +421,7 @@ def register_query_tool_preferences(self):
}
},
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
fields=shortcut_fields
)
self.preference.register(

View File

@ -0,0 +1,119 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { withTheme } from '../fake_theme';
import CodeMirror from 'sources/components/ReactCodeMirror';
import { render } from '@testing-library/react';
describe('CodeMirrorCustomEditorView', ()=>{
const ThemedCM = withTheme(CodeMirror);
let cmInstance, editor;
const cmRerender = (props)=>{
cmInstance.rerender(
<ThemedCM
value={'Init text'}
className="testClass"
currEditor={(obj) => {
editor = obj;
}}
{...props}
/>
);
};
beforeEach(()=>{
cmInstance = render(
<ThemedCM
value={'Init text'}
className="testClass"
currEditor={(obj) => {
editor = obj;
}}
/>);
});
it('single query with no cursor position',()=>{
cmRerender({value:'select * from public.actor;'});
expect(editor.getQueryAt()).toEqual({'value': 'select * from public.actor;', 'from': 0, 'to': 27});
});
it('cursor within a query in multiple queries',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(20)).toEqual({'value': 'select * from public.actor;', 'from': 0, 'to': 27});
});
it('cursor outside the semicolon of a query in multiple queries',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(29)).toEqual({'value': 'select * from public.actor;', 'from': 0, 'to': 27});
});
it('cursor at the starting of a comment block',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(31)).toEqual({'value': '--rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 27, 'to': 107});
});
it('cursor inside a comment block',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(72)).toEqual({'value': '--rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 27, 'to': 107});
});
it('cursor inside a comment block`s 2nd line',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(107)).toEqual({'value': '--rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 27, 'to': 107});
});
it('cursor at the starting of a query in multiple queries',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(109)).toEqual({'value': 'select * from public.address where address_id=5;', 'from': 109, 'to': 157});
});
it('cursor at the next line where query ends with semicolon',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(158)).toEqual({'value': 'select * from public.address where address_id=5;', 'from': 109, 'to': 157});
});
it('cursor at an empty line where query is present one empty line above',()=>{
cmRerender({value: 'select * from public.actor; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5;\n\nselect * from public.city;\n\nselect 1;\n\n\n'});
expect(editor.getQueryAt(198)).toEqual({'value': '', 'from': 198, 'to': 199});
});
it('cursor at 2nd line and query is in 2 lines',()=>{
cmRerender({value: 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address \n\twhere address_id=5;\n\nselect * from public.city;\n\nselect 1;'});
expect(editor.getQueryAt(141)).toEqual({'value':'select * from public.address \n\twhere address_id=5;', 'from': 108, 'to': 158});
});
it('cursor at the start of query and multiple queries without semicolon',()=>{
cmRerender({value: 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5\n\nselect * from public.city\n\nselect 1'});
expect(editor.getQueryAt(0)).toEqual({'value': 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 0, 'to': 106});
});
it('cursor at the end of query and multiple queries without semicolon',()=>{
cmRerender({value: 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5\n\nselect * from public.city\n\nselect 1'});
expect(editor.getQueryAt(26)).toEqual({'value': 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 0, 'to': 106});
});
it('cursor in between of a query and multiple queries without semicolon',()=>{
cmRerender({value: 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5\n\nselect * from public.city\n\nselect 1'});
expect(editor.getQueryAt(17)).toEqual({'value': 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff', 'from': 0, 'to': 106});
});
it('cursor is at a new empty line and just above it a query without semicolon',()=>{
cmRerender({value: 'select * from public.actor --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5\n\nselect * from public.city\n\nselect 1'});
expect(editor.getQueryAt(156)).toEqual({'value': 'select * from public.address where address_id=5', 'from': 108, 'to': 156});
});
it('cursor at a empty query with semicolon',()=>{
cmRerender({value: 'select * from public.actor; ; --rhhryyr select * from public.film\n--skskks\n--sksksksksks\n--sksksksksksdff\n\t\nselect * from public.address where address_id=5\n\nselect * from public.city\n\nselect 1'});
expect(editor.getQueryAt(29)).toEqual({'value': 'select * from public.actor;', 'from': 0, 'to': 27});
});
});