Replace infinite scrolling with pagination in query tool data output for better UX and performance. #1780
parent
f8fb78be11
commit
6322674d98
Binary file not shown.
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 176 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 151 KiB |
|
@ -230,7 +230,7 @@ Use the fields on the *Options* panel to manage ERD preferences.
|
|||
|
||||
|
||||
* Use *Cardinality Notation* to change the cardinality notation format
|
||||
used to present relationship links.
|
||||
used to present relationship links.
|
||||
|
||||
* When the *SQL With DROP Table* switch is set to *True*, the SQL
|
||||
generated by the ERD Tool will add DROP table DDL before each CREATE
|
||||
|
@ -398,7 +398,7 @@ 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 *Highlight selection matches?* switch is set to *True*, the editor will
|
||||
* When the *Highlight selection matches?* switch is set to *True*, the editor will
|
||||
highlight matched selected text.
|
||||
|
||||
.. image:: images/preferences_sql_explain.png
|
||||
|
@ -476,11 +476,11 @@ Use the fields on the *Options* panel to manage editor preferences.
|
|||
* 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
|
||||
* 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
|
||||
* 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
|
||||
|
@ -497,9 +497,8 @@ preferences for copied data.
|
|||
* Specify the maximum width of the column in pixels when 'Columns sized by' is
|
||||
set to *Column data*. If 'Columns sized by' is set to *Column name* then this
|
||||
setting won't have any effect.
|
||||
* Specify the number of records to fetch in one batch in query tool when
|
||||
query result set is large. Changing this value will override
|
||||
ON_DEMAND_ROW_COUNT setting from config file.
|
||||
* Specify the number of records to fetch in one batch. Changing this value will
|
||||
override DATA_RESULT_ROWS_PER_PAGE setting from config file.
|
||||
* Use the *Result copy field separator* drop-down listbox to select the field
|
||||
separator for copied data.
|
||||
* Use the *Result copy quote character* drop-down listbox to select the quote
|
||||
|
@ -523,7 +522,7 @@ reformatting of SQL.
|
|||
|
||||
* Use the *Data type case* option to specify whether to change data types
|
||||
into upper, lower, or preserve case.
|
||||
* Use the *Expression width* option to specify maximum number of characters
|
||||
* Use the *Expression width* option to specify maximum number of characters
|
||||
in parenthesized expressions to be kept on single line.
|
||||
* Use the *Function case* option to specify whether to change function
|
||||
names into upper, lower, or preserve case.
|
||||
|
@ -531,7 +530,7 @@ reformatting of SQL.
|
|||
(object names) into upper, lower, or capitalized case.
|
||||
* Use the *Keyword case* option to specify whether to change keywords into
|
||||
upper, lower, or preserve case.
|
||||
* Use *Lines between queries* to specify how many empty lines to leave
|
||||
* Use *Lines between queries* to specify how many empty lines to leave
|
||||
between SQL statements. If set to zero it puts no new line.
|
||||
* Use *Logical operator new line* to specify newline placement before or
|
||||
after logical operators (AND, OR, XOR).
|
||||
|
|
|
@ -74,8 +74,8 @@ 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
|
||||
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_script.png
|
||||
|
@ -177,6 +177,7 @@ You can:
|
|||
* 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.
|
||||
* Move between pages of data result.
|
||||
|
||||
.. _updatable-result-set:
|
||||
|
||||
|
|
|
@ -214,6 +214,48 @@ Data Editing Options
|
|||
| SQL | Use the SQL button to check the current query that gave the data. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
|
||||
|
||||
Pagination Options
|
||||
********************
|
||||
|
||||
.. image:: images/query_data_pagination.png
|
||||
:alt: Query tool data pagination options
|
||||
:align: center
|
||||
|
||||
.. table::
|
||||
:class: longtable
|
||||
:widths: 1 4 1
|
||||
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| Icon | Behavior | Shortcut |
|
||||
+======================+===================================================================================================+================+
|
||||
| *Rows Range* | Show the current row numbers visible in the data grid. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Edit Range* | Click to open the from and to rows range inputs to allow setting them. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Page No* | Enter the page no you want to jump to out of total shown next to this input | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *First Page* | Click to go to the first page. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Previous Page* | Click to go to the previous page. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Next Page* | Click to go to the next page. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
| *Last Page* | Click to go to the last page. | |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
|
||||
|
||||
|
||||
.. image:: images/query_data_pagination_edit.png
|
||||
:alt: Query tool data pagination options
|
||||
:align: center
|
||||
|
||||
One can click the edit range button to open rows range editor:
|
||||
|
||||
* From and to range should be between 1 and total rows.
|
||||
* The range can be applied by clicking the *Apply* button or by pressing enter in the range inputs.
|
||||
* Once the range is applied, pgAdmin will recalculate the rows per page. The pagination will then behave based on the new rows per page.
|
||||
* It may be possible that on pressing next page button, the new rows range is not next to manually enterred range.
|
||||
|
||||
Status Bar
|
||||
**********
|
||||
|
||||
|
|
|
@ -513,10 +513,10 @@ THREADED_MODE = True
|
|||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
##########################################################################
|
||||
# Number of records to fetch in one batch in query tool when query result
|
||||
# set is large.
|
||||
# Number of records to fetch in one page in query tool when query result
|
||||
# set is large and is divided in multiple pages
|
||||
##########################################################################
|
||||
ON_DEMAND_RECORD_COUNT = 1000
|
||||
DATA_RESULT_ROWS_PER_PAGE = 1000
|
||||
|
||||
##########################################################################
|
||||
# Allow users to display Gravatar image for their username in Server mode
|
||||
|
|
|
@ -15,6 +15,7 @@ import secrets
|
|||
from urllib.parse import unquote
|
||||
from threading import Lock
|
||||
import threading
|
||||
import math
|
||||
|
||||
import json
|
||||
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE
|
||||
|
@ -106,8 +107,7 @@ class SqlEditorModule(PgAdminModule):
|
|||
'sqleditor.view_data_start',
|
||||
'sqleditor.query_tool_start',
|
||||
'sqleditor.poll',
|
||||
'sqleditor.fetch',
|
||||
'sqleditor.fetch_all',
|
||||
'sqleditor.fetch_window',
|
||||
'sqleditor.fetch_all_from_start',
|
||||
'sqleditor.save',
|
||||
'sqleditor.inclusive_filter',
|
||||
|
@ -470,7 +470,8 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
|
|||
"prompt_password": True,
|
||||
"allow_save_password": True
|
||||
if ALLOW_SAVE_PASSWORD and
|
||||
session['allow_save_password'] else False,
|
||||
session.get('allow_save_password', None)
|
||||
else False,
|
||||
}
|
||||
), '', ''
|
||||
else:
|
||||
|
@ -925,7 +926,6 @@ def poll(trans_id):
|
|||
rows_affected = 0
|
||||
rows_fetched_from = 0
|
||||
rows_fetched_to = 0
|
||||
has_more_rows = False
|
||||
columns = dict()
|
||||
columns_info = None
|
||||
primary_keys = None
|
||||
|
@ -936,8 +936,8 @@ def poll(trans_id):
|
|||
additional_messages = None
|
||||
notifies = None
|
||||
data_obj = {}
|
||||
on_demand_record_count = Preferences.module(MODULE_NAME).\
|
||||
preference('on_demand_record_count').get()
|
||||
data_result_rows_per_page = Preferences.module(MODULE_NAME).\
|
||||
preference('data_result_rows_per_page').get()
|
||||
# Check the transaction and connection status
|
||||
status, error_msg, conn, trans_obj, session_obj = \
|
||||
check_transaction_status(trans_id)
|
||||
|
@ -1004,7 +1004,8 @@ def poll(trans_id):
|
|||
status = 'Success'
|
||||
rows_affected = conn.rows_affected()
|
||||
|
||||
st, result = conn.async_fetchmany_2darray(on_demand_record_count)
|
||||
st, result = \
|
||||
conn.async_fetchmany_2darray(data_result_rows_per_page)
|
||||
|
||||
# There may be additional messages even if result is present
|
||||
# eg: Function can provide result as well as RAISE messages
|
||||
|
@ -1081,8 +1082,6 @@ def poll(trans_id):
|
|||
# means nothing to fetch
|
||||
if result and rows_affected > -1:
|
||||
res_len = len(result)
|
||||
if res_len == on_demand_record_count:
|
||||
has_more_rows = True
|
||||
|
||||
if res_len > 0:
|
||||
rows_fetched_from = trans_obj.get_fetched_row_cnt()
|
||||
|
@ -1126,6 +1125,15 @@ def poll(trans_id):
|
|||
data_obj['db_id'] = trans_obj.did \
|
||||
if trans_obj is not None and hasattr(trans_obj, 'did') else 0
|
||||
|
||||
page_size = rows_fetched_to - rows_fetched_from + 1
|
||||
pagination = {
|
||||
'page_size': page_size,
|
||||
'page_count': math.ceil(conn.total_rows / page_size),
|
||||
'page_no': math.floor(rows_fetched_from / page_size) + 1,
|
||||
'rows_from': rows_fetched_from,
|
||||
'rows_to': rows_fetched_to
|
||||
}
|
||||
|
||||
return make_json_response(
|
||||
data={
|
||||
'status': status, 'result': result,
|
||||
|
@ -1134,7 +1142,6 @@ def poll(trans_id):
|
|||
'rows_fetched_to': rows_fetched_to,
|
||||
'additional_messages': additional_messages,
|
||||
'notifies': notifies,
|
||||
'has_more_rows': has_more_rows,
|
||||
'colinfo': columns_info,
|
||||
'primary_keys': primary_keys,
|
||||
'types': types,
|
||||
|
@ -1143,26 +1150,20 @@ def poll(trans_id):
|
|||
'oids': oids,
|
||||
'transaction_status': transaction_status,
|
||||
'data_obj': data_obj,
|
||||
'pagination': pagination,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@blueprint.route(
|
||||
'/fetch/<int:trans_id>', methods=["GET"], endpoint='fetch'
|
||||
)
|
||||
@blueprint.route(
|
||||
'/fetch/<int:trans_id>/<int:fetch_all>', methods=["GET"],
|
||||
endpoint='fetch_all'
|
||||
'/fetch_window/<int:trans_id>/<int:from_rownum>/<int:to_rownum>',
|
||||
methods=["GET"], endpoint='fetch_window'
|
||||
)
|
||||
@pga_login_required
|
||||
def fetch(trans_id, fetch_all=None):
|
||||
def fetch_window(trans_id, from_rownum=0, to_rownum=0):
|
||||
result = None
|
||||
has_more_rows = False
|
||||
rows_fetched_from = 0
|
||||
rows_fetched_to = 0
|
||||
on_demand_record_count = Preferences.module(MODULE_NAME).preference(
|
||||
'on_demand_record_count').get()
|
||||
fetch_row_cnt = -1 if fetch_all == 1 else on_demand_record_count
|
||||
|
||||
# Check the transaction and connection status
|
||||
status, error_msg, conn, trans_obj, session_obj = \
|
||||
|
@ -1174,33 +1175,39 @@ def fetch(trans_id, fetch_all=None):
|
|||
status=404)
|
||||
|
||||
if status and conn is not None and session_obj is not None:
|
||||
status, result = conn.async_fetchmany_2darray(fetch_row_cnt)
|
||||
# rownums start from 0 but UI will ask from 1
|
||||
status, result = conn.async_fetchmany_2darray(
|
||||
records=None, from_rownum=from_rownum - 1, to_rownum=to_rownum - 1)
|
||||
if not status:
|
||||
status = 'Error'
|
||||
else:
|
||||
status = 'Success'
|
||||
res_len = len(result) if result else 0
|
||||
if fetch_row_cnt != -1 and res_len == on_demand_record_count:
|
||||
has_more_rows = True
|
||||
|
||||
if res_len:
|
||||
rows_fetched_from = trans_obj.get_fetched_row_cnt()
|
||||
trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len)
|
||||
rows_fetched_from += 1
|
||||
rows_fetched_to = trans_obj.get_fetched_row_cnt()
|
||||
rows_fetched_from = from_rownum
|
||||
rows_fetched_to = rows_fetched_from + res_len - 1
|
||||
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
|
||||
update_session_grid_transaction(trans_id, session_obj)
|
||||
else:
|
||||
status = 'NotConnected'
|
||||
result = error_msg
|
||||
|
||||
page_size = to_rownum - from_rownum + 1
|
||||
pagination = {
|
||||
'page_size': page_size,
|
||||
'page_count': math.ceil(conn.total_rows / page_size),
|
||||
'page_no': math.floor(rows_fetched_from / page_size) + 1,
|
||||
'rows_from': rows_fetched_from,
|
||||
'rows_to': rows_fetched_to
|
||||
}
|
||||
|
||||
return make_json_response(
|
||||
data={
|
||||
'status': status,
|
||||
'result': result,
|
||||
'has_more_rows': has_more_rows,
|
||||
'rows_fetched_from': rows_fetched_from,
|
||||
'rows_fetched_to': rows_fetched_to
|
||||
'pagination': pagination,
|
||||
'row_count': conn.row_count,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export const QUERY_TOOL_EVENTS = {
|
|||
STOP_QUERY: 'STOP_QUERY',
|
||||
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
|
||||
SET_MESSAGE: 'SET_MESSAGE',
|
||||
ROWS_FETCHED: 'ROWS_FETCHED',
|
||||
TOTAL_ROWS_COUNT: 'TOTAL_ROWS_COUNT',
|
||||
SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED',
|
||||
DATAGRID_CHANGED: 'DATAGRID_CHANGED',
|
||||
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
|
||||
|
@ -56,8 +56,12 @@ export const QUERY_TOOL_EVENTS = {
|
|||
PUSH_HISTORY: 'PUSH_HISTORY',
|
||||
HANDLE_API_ERROR: 'HANDLE_API_ERROR',
|
||||
SET_FILTER_INFO: 'SET_FILTER_INFO',
|
||||
FETCH_MORE_ROWS: 'FETCH_MORE_ROWS',
|
||||
REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION',
|
||||
FETCH_WINDOW: 'FETCH_WINDOW',
|
||||
ALL_PAGE_ROWS_SELECTED: 'ALL_PAGE_ROWS_SELECTED',
|
||||
ALL_ROWS_SELECTED: 'ALL_ROWS_SELECTED',
|
||||
CLEAR_ROWS_SELECTED: 'CLEAR_ROWS_SELECTED',
|
||||
ALL_ROWS_SELECTED_STATUS: 'ALL_ROWS_SELECTED_STATUS',
|
||||
|
||||
EDITOR_LAST_FOCUS: 'EDITOR_LAST_FOCUS',
|
||||
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
|
||||
|
@ -109,4 +113,4 @@ export const PANELS = {
|
|||
|
||||
export const MAX_QUERY_LENGTH = 1000000;
|
||||
|
||||
export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf';
|
||||
export const OS_EOL = navigator.platform === 'win32' ? 'crlf' : 'lf';
|
||||
|
|
|
@ -139,9 +139,8 @@ function SelectAllHeaderRenderer({isCellSelected}) {
|
|||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const dataGridExtras = useContext(DataGridExtrasContext);
|
||||
const onClick = ()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
|
||||
onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected });
|
||||
});
|
||||
onRowSelectionChange({ type: 'HEADER', checked: !isRowSelected });
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, !isRowSelected);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -149,6 +148,15 @@ function SelectAllHeaderRenderer({isCellSelected}) {
|
|||
cellRef.current?.focus({ preventScroll: true });
|
||||
}, [isCellSelected]);
|
||||
|
||||
useEffect(()=>{
|
||||
const unregClear = eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{
|
||||
onRowSelectionChange({ type: 'HEADER', checked: false });
|
||||
});
|
||||
return ()=>{
|
||||
unregClear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
|
||||
tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>;
|
||||
}
|
||||
|
@ -167,15 +175,13 @@ function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsCha
|
|||
}
|
||||
|
||||
const onClick = ()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
|
||||
const newSelectedCols = new Set(selectedColumns);
|
||||
if (newSelectedCols.has(column.idx)) {
|
||||
newSelectedCols.delete(column.idx);
|
||||
} else {
|
||||
newSelectedCols.add(column.idx);
|
||||
}
|
||||
onSelectedColumnsChange(newSelectedCols);
|
||||
});
|
||||
const newSelectedCols = new Set(selectedColumns);
|
||||
if (newSelectedCols.has(column.idx)) {
|
||||
newSelectedCols.delete(column.idx);
|
||||
} else {
|
||||
newSelectedCols.add(column.idx);
|
||||
}
|
||||
onSelectedColumnsChange(newSelectedCols);
|
||||
};
|
||||
|
||||
const isSelected = selectedColumns.has(column.idx);
|
||||
|
@ -296,9 +302,10 @@ function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
|
|||
}
|
||||
function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) {
|
||||
const [isRowSelected, onRowSelectionChange] = useRowSelection();
|
||||
const {startRowNum} = useContext(DataGridExtrasContext);
|
||||
|
||||
let rowKey = rowKeyGetter(row);
|
||||
let rownum = rowIdx+1;
|
||||
let rownum = rowIdx+(startRowNum??1);
|
||||
if(rowKey in (dataChangeStore?.added || {})) {
|
||||
rownum = rownum+'+';
|
||||
} else if(rowKey in (dataChangeStore?.deleted || {})) {
|
||||
|
@ -375,7 +382,7 @@ function getTextWidth(column, rows, canvas, columnWidthBy) {
|
|||
}
|
||||
|
||||
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
|
||||
onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) {
|
||||
onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, startRowNum, ...props}) {
|
||||
const [readyColumns, setReadyColumns] = useState([]);
|
||||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const onSelectedColumnsChangeWrapped = (arg)=>{
|
||||
|
@ -392,7 +399,7 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
|
|||
}, []);
|
||||
|
||||
const dataGridExtras = useMemo(()=>({
|
||||
onSelectedCellChange, handleCopy
|
||||
onSelectedCellChange, handleCopy, startRowNum
|
||||
}), [onSelectedCellChange]);
|
||||
|
||||
useEffect(()=>{
|
||||
|
|
|
@ -276,10 +276,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
|
|||
}, [queryToolConnCtx.connectionStatus]);
|
||||
|
||||
const onCommitClick=()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, '', true);
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', {external: true});
|
||||
};
|
||||
const onRollbackClick=()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, '', true);
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', {external: true});
|
||||
};
|
||||
const executeMacro = (m)=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql);
|
||||
|
|
|
@ -151,10 +151,10 @@ export default function Query({onTextSelect, handleEndOfLineChange}) {
|
|||
query = query || editor.current?.getValue() || '';
|
||||
}
|
||||
if(query) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, macroSQL, external, null, executeCursor);
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, macroSQL, external, executeCursor});
|
||||
}
|
||||
} else {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null, '');
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, {});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -232,15 +232,20 @@ export class ResultSetUtils {
|
|||
};
|
||||
|
||||
async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={
|
||||
isQueryTool: true, external: false, reconnect: false, executeCursor: false
|
||||
isQueryTool: true, external: false, reconnect: false, executeCursor: false, refreshData: false,
|
||||
}) {
|
||||
let startTime = new Date();
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START, gettext('Waiting for the query to complete...'), startTime);
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TASK_START,
|
||||
flags.refreshData ? gettext('Refetching latest results...') : gettext('Waiting for the query to complete...'),
|
||||
startTime
|
||||
);
|
||||
this.setStartTime(startTime);
|
||||
this.query = query;
|
||||
this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA;
|
||||
if(explainObject) {
|
||||
if(flags.refreshData) {
|
||||
this.historyQuerySource = null;
|
||||
} else if(explainObject) {
|
||||
if(explainObject.analyze) {
|
||||
this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE;
|
||||
} else {
|
||||
|
@ -301,7 +306,7 @@ export class ResultSetUtils {
|
|||
e,
|
||||
{
|
||||
connectionLostCallback: ()=>{
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, '', flags.external, true, flags.executeCursor);
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor});
|
||||
},
|
||||
checkTransaction: true,
|
||||
}
|
||||
|
@ -357,7 +362,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, flags.executeCursor);
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, {explainObject, external: flags.external, reconnect: true, executeCursor: flags.executeCursor});
|
||||
},
|
||||
checkTransaction: true,
|
||||
});
|
||||
|
@ -396,7 +401,7 @@ export class ResultSetUtils {
|
|||
if(this.qtPref?.query_success_notification) {
|
||||
pgAdmin.Browser.notifier.success(msg);
|
||||
}
|
||||
if(!ResultSetUtils.isQueryStillRunning(httpMessage)) {
|
||||
if(!ResultSetUtils.isQueryStillRunning(httpMessage) && this.historyQuerySource) {
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, {
|
||||
status: true,
|
||||
start_time: this.startTime,
|
||||
|
@ -414,16 +419,12 @@ export class ResultSetUtils {
|
|||
}
|
||||
}
|
||||
|
||||
getMoreRows(all=false) {
|
||||
let url = url_for('sqleditor.fetch', {
|
||||
getWindowRows(fromRownum, toRownum) {
|
||||
let url = url_for('sqleditor.fetch_window', {
|
||||
'trans_id': this.transId,
|
||||
'from_rownum': fromRownum,
|
||||
'to_rownum': toRownum,
|
||||
});
|
||||
if(all) {
|
||||
url = url_for('sqleditor.fetch_all', {
|
||||
'trans_id': this.transId,
|
||||
'fetch_all': 1,
|
||||
});
|
||||
}
|
||||
return this.api.get(url);
|
||||
}
|
||||
|
||||
|
@ -791,6 +792,11 @@ function dataChangeReducer(state, action) {
|
|||
...dataChange.deleted,
|
||||
...action.add,
|
||||
};
|
||||
if(action.all) {
|
||||
dataChange.delete_all = true;
|
||||
} else {
|
||||
dataChange.delete_all = false;
|
||||
}
|
||||
break;
|
||||
case 'reset':
|
||||
dataChange = {
|
||||
|
@ -798,6 +804,7 @@ function dataChangeReducer(state, action) {
|
|||
added: {},
|
||||
added_index: {},
|
||||
deleted: {},
|
||||
delete_all: false,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
|
@ -818,12 +825,14 @@ export function ResultSet() {
|
|||
const [queryData, setQueryData] = useState(null);
|
||||
const [rows, setRows] = useState([]);
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const api = getApiInstance();
|
||||
const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool));
|
||||
const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {});
|
||||
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||
const [selectedColumns, setSelectedColumns] = useState(new Set());
|
||||
// NONE - no select, PAGE - show select all, ALL - select all.
|
||||
const [allRowsSelect, setAllRowsSelect] = useState('NONE');
|
||||
|
||||
const selectedCell = useRef([]);
|
||||
const selectedRange = useRef(null);
|
||||
const setSelectedCell = (val)=>{
|
||||
|
@ -855,7 +864,9 @@ 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, macroSQL, external=false, reconnect=false, executeCursor=false)=>{
|
||||
const executionStartCallback = async (query, {
|
||||
explainObject, macroSQL, external=false, reconnect=false, executeCursor=false, refreshData=false
|
||||
})=>{
|
||||
const yesCallback = async ()=>{
|
||||
/* Reset */
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null);
|
||||
|
@ -871,7 +882,7 @@ export function ResultSet() {
|
|||
setColumns([]);
|
||||
setRows([]);
|
||||
},
|
||||
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor}
|
||||
{isQueryTool: queryToolCtx.params.is_query_tool, external: external, reconnect: reconnect, executeCursor: executeCursor, refreshData: refreshData}
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -916,7 +927,7 @@ export function ResultSet() {
|
|||
});
|
||||
};
|
||||
|
||||
if(isDataChanged()) {
|
||||
if(isDataChanged() && !refreshData) {
|
||||
queryToolCtx.modal.confirm(
|
||||
gettext('Unsaved changes'),
|
||||
gettext('The data has been modified, but not saved. Are you sure you wish to discard the changes?'),
|
||||
|
@ -1010,19 +1021,41 @@ export function ResultSet() {
|
|||
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, triggerFilter);
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL, triggerResetScroll);
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_PAGE_ROWS_SELECTED, (selectAll)=>{
|
||||
if(selectAll) {
|
||||
setAllRowsSelect('PAGE');
|
||||
} else {
|
||||
setAllRowsSelect('NONE');
|
||||
}
|
||||
});
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED, ()=>{
|
||||
setAllRowsSelect('ALL');
|
||||
});
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED, ()=>{
|
||||
setSelectedRows(new Set());
|
||||
setAllRowsSelect('NONE');
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
||||
const deregExec = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
||||
return ()=>{
|
||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
||||
deregExec();
|
||||
};
|
||||
}, [dataChangeStore]);
|
||||
}, [dataChangeStore, dataOutputQuery]);
|
||||
|
||||
useEffect(()=>{
|
||||
fireRowsColsCellChanged();
|
||||
setAllRowsSelect('NONE');
|
||||
}, [selectedRows.size, selectedColumns.size]);
|
||||
|
||||
useEffect(()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, allRowsSelect);
|
||||
}, [allRowsSelect]);
|
||||
|
||||
useEffect(()=>{
|
||||
rsu.current.transId = queryToolCtx.params.trans_id;
|
||||
}, [queryToolCtx.params.trans_id]);
|
||||
|
@ -1031,45 +1064,44 @@ export function ResultSet() {
|
|||
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns);
|
||||
}, [columns]);
|
||||
|
||||
const fetchMoreRows = async (all=false, callback=undefined)=>{
|
||||
if(queryData.has_more_rows) {
|
||||
let res = [];
|
||||
setIsLoadingMore(true);
|
||||
try {
|
||||
res = await rsu.current.getMoreRows(all);
|
||||
const newRows = rsu.current.processRows(res.data.data.result, columns);
|
||||
setRows((prevRows)=>[...prevRows, ...newRows]);
|
||||
setQueryData((prev)=>({
|
||||
...prev,
|
||||
has_more_rows: res.data.data.has_more_rows,
|
||||
rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to,
|
||||
}));
|
||||
} catch (e) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR,
|
||||
e,
|
||||
{
|
||||
connectionLostCallback: ()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, '', false, true);
|
||||
},
|
||||
checkTransaction: true,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
const fetchWindow = async (fromRownum, toRownum, callback)=>{
|
||||
let res = [];
|
||||
setLoaderText(gettext('Fetching rows...'));
|
||||
try {
|
||||
res = await rsu.current.getWindowRows(fromRownum, toRownum);
|
||||
const newRows = rsu.current.processRows(res.data.data.result, columns);
|
||||
setRows([...newRows]);
|
||||
setQueryData((prev)=>({
|
||||
...prev,
|
||||
pagination: res.data.data.pagination,
|
||||
rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to,
|
||||
}));
|
||||
} catch (e) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR,
|
||||
e,
|
||||
{
|
||||
connectionLostCallback: ()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {external: false, reconnect: true});
|
||||
},
|
||||
checkTransaction: true,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
setLoaderText('');
|
||||
}
|
||||
callback?.();
|
||||
};
|
||||
useEffect(()=>{
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
|
||||
return ()=>{
|
||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
|
||||
};
|
||||
}, [queryData?.has_more_rows, columns]);
|
||||
|
||||
useEffect(()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.ROWS_FETCHED, queryData?.rows_fetched_to, queryData?.rows_affected);
|
||||
}, [queryData?.rows_fetched_to, queryData?.rows_affected]);
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow);
|
||||
return ()=>{
|
||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow);
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
useEffect(()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, queryData?.rows_affected);
|
||||
}, [queryData?.rows_affected]);
|
||||
|
||||
const warnSaveDataClose = ()=>{
|
||||
// No changes.
|
||||
|
@ -1116,6 +1148,7 @@ export function ResultSet() {
|
|||
let {data: respData} = await rsu.current.saveData({
|
||||
updated: dataChangeStore.updated,
|
||||
deleted: dataChangeStore.deleted,
|
||||
delete_all: dataChangeStore.delete_all,
|
||||
added_index: dataChangeStore.added_index,
|
||||
added: added,
|
||||
columns: columns,
|
||||
|
@ -1152,37 +1185,6 @@ export function ResultSet() {
|
|||
}
|
||||
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true);
|
||||
if(_.size(dataChangeStore.added)) {
|
||||
// Update the rows in a grid after addition
|
||||
respData.data.query_results.forEach((qr)=>{
|
||||
if(!_.isNull(qr.row_added)) {
|
||||
let rowClientPK = Object.keys(qr.row_added)[0];
|
||||
setRows((prevRows)=>{
|
||||
let rowIdx = prevRows.findIndex((r)=>rowKeyGetter(r)==rowClientPK);
|
||||
return [
|
||||
...prevRows.slice(0, rowIdx),
|
||||
{
|
||||
...prevRows[rowIdx],
|
||||
...qr.row_added[rowClientPK],
|
||||
},
|
||||
...prevRows.slice(rowIdx+1),
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
let deletedKeys = Object.keys(dataChangeStore.deleted);
|
||||
if(deletedKeys.length == rows.length) {
|
||||
setRows([]);
|
||||
}
|
||||
else if(deletedKeys.length > 0) {
|
||||
setRows((prevRows)=>{
|
||||
return prevRows.filter((row)=>{
|
||||
return deletedKeys.indexOf(row[rsu.current.clientPK]) == -1;
|
||||
});
|
||||
});
|
||||
setColumns((prev)=>prev);
|
||||
}
|
||||
dispatchDataChange({type: 'reset'});
|
||||
setSelectedRows(new Set());
|
||||
setSelectedColumns(new Set());
|
||||
|
@ -1192,6 +1194,8 @@ export function ResultSet() {
|
|||
if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) {
|
||||
pgAdmin.Browser.notifier.info(gettext('Auto-commit is off. You still need to commit changes to the database.'));
|
||||
}
|
||||
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {refreshData: true});
|
||||
} catch (error) {
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false);
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
||||
|
@ -1292,6 +1296,7 @@ export function ResultSet() {
|
|||
type: 'deleted',
|
||||
add: add,
|
||||
remove: remove,
|
||||
all: remove.length > 0 ? false : allRowsSelect == 'ALL',
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1307,7 +1312,7 @@ export function ResultSet() {
|
|||
return ()=>{
|
||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows);
|
||||
};
|
||||
}, [selectedRows, queryData, dataChangeStore, rows]);
|
||||
}, [selectedRows, queryData, dataChangeStore, rows, allRowsSelect]);
|
||||
|
||||
useEffect(()=>{
|
||||
const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{
|
||||
|
@ -1317,7 +1322,7 @@ export function ResultSet() {
|
|||
selectedRowsSorted.sort();
|
||||
insPosn = _.findIndex(rows, (r)=>rowKeyGetter(r)==selectedRowsSorted[selectedRowsSorted.length-1])+1;
|
||||
}
|
||||
let byteaCellSelection = columns.filter(o=>o.type=='bytea');
|
||||
let byteaCellSelection = columns.filter(o=>o.type=='bytea');
|
||||
if (byteaCellSelection.length>0) {
|
||||
_rows = _rows.map(x=>{
|
||||
byteaCellSelection.forEach(r=>{
|
||||
|
@ -1373,20 +1378,6 @@ export function ResultSet() {
|
|||
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
|
||||
}, [rows, columns, selectedRows.size, selectedColumns.size]);
|
||||
|
||||
const handleScroll = (e) => {
|
||||
// Set scroll current position of RestSet.
|
||||
if (!_.isNull(e.currentTarget) && isResettingScroll.current) {
|
||||
lastScrollRef.current = {
|
||||
ref: { ...e },
|
||||
top: e.currentTarget.scrollTop,
|
||||
left: e.currentTarget.scrollLeft
|
||||
};
|
||||
}
|
||||
|
||||
if (isLoadingMore || !rsu.current.isAtBottom(e)) return;
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS);
|
||||
};
|
||||
|
||||
const triggerResetScroll = () => {
|
||||
// Reset the scroll position to previously saved location.
|
||||
if (lastScrollRef.current) {
|
||||
|
@ -1463,17 +1454,20 @@ export function ResultSet() {
|
|||
return (
|
||||
<StyledBox ref={containerRef} tabIndex="0">
|
||||
<Loader message={loaderText} />
|
||||
<Loader data-label="loader-more-rows" message={isLoadingMore ? gettext('Loading more rows...') : null} style={{top: 'unset', right: 'unset', padding: '0.5rem 1rem'}}/>
|
||||
{!queryData &&
|
||||
<EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/>
|
||||
}
|
||||
{queryData && <>
|
||||
<ResultSetToolbar containerRef={containerRef} query={dataOutputQuery} canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}/>
|
||||
<ResultSetToolbar containerRef={containerRef} query={dataOutputQuery}
|
||||
canEdit={queryData.can_edit} totalRowCount={queryData?.rows_affected}
|
||||
pagination={queryData?.pagination ?? {}} allRowsSelect={allRowsSelect}
|
||||
/>
|
||||
<Box flexGrow="1" minHeight="0">
|
||||
<QueryToolDataGrid
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
totalRowCount={queryData?.rows_affected}
|
||||
startRowNum={queryData?.pagination?.rows_from}
|
||||
columnWidthBy={
|
||||
queryToolCtx.preferences?.sqleditor?.column_data_auto_resize == 'by_data' ?
|
||||
queryToolCtx.preferences.sqleditor.column_data_max_width :
|
||||
|
@ -1481,7 +1475,6 @@ export function ResultSet() {
|
|||
}
|
||||
key={rowsResetKey}
|
||||
rowKeyGetter={rowKeyGetter}
|
||||
onScroll={handleScroll}
|
||||
onRowsChange={onRowsChange}
|
||||
dataChangeStore={dataChangeStore}
|
||||
selectedRows={selectedRows}
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
//////////////////////////////////////////////////////////////
|
||||
import React, {useContext, useCallback, useEffect, useState} from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Portal } from '@mui/material';
|
||||
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
|
||||
import { Box, Portal } from '@mui/material';
|
||||
import { DefaultButton, 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';
|
||||
|
@ -17,6 +17,14 @@ import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
|||
import TimelineRoundedIcon from '@mui/icons-material/TimelineRounded';
|
||||
import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
|
||||
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
|
||||
import FastForwardRoundedIcon from '@mui/icons-material/FastForwardRounded';
|
||||
import FastRewindRoundedIcon from '@mui/icons-material/FastRewindRounded';
|
||||
import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded';
|
||||
import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import EditOffRoundedIcon from '@mui/icons-material/EditOffRounded';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
|
||||
import {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
|
||||
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
|
||||
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
|
||||
|
@ -26,14 +34,28 @@ import CopyData from '../QueryToolDataGrid/CopyData';
|
|||
import PropTypes from 'prop-types';
|
||||
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
|
||||
import { setEditorPosition } from '../QueryToolDataGrid/Editors';
|
||||
import { InputText } from '../../../../../../static/js/components/FormComponents';
|
||||
import { minMaxValidator } from '../../../../../../static/js/validators';
|
||||
|
||||
const StyledDiv = styled('div')(({theme})=>({
|
||||
padding: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
flexWrap: 'wrap',
|
||||
rowGap: '4px',
|
||||
backgroundColor: theme.otherVars.editorToolbarBg,
|
||||
justifyContent: 'space-between',
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
|
||||
'& .PaginationInputs': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
|
||||
'& .PaginationInputs-divider': {
|
||||
...theme.mixins.panelBorder.right,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const StyledEditor = styled('div')(({theme})=>({
|
||||
|
@ -76,7 +98,134 @@ ShowDataOutputQueryPopup.propTypes = {
|
|||
query: PropTypes.string,
|
||||
};
|
||||
|
||||
export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
||||
|
||||
function PaginationInputs({pagination, totalRowCount, clearSelection}) {
|
||||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const [editPageRange, setEditPageRange] = useState(false);
|
||||
const [errorInputs, setErrorInputs] = useState({
|
||||
'from': false,
|
||||
'to': false,
|
||||
'pageNo': false
|
||||
});
|
||||
const [inputs, setInputs] = useState({
|
||||
from: pagination.rows_from ?? 0,
|
||||
to: pagination.rows_to ?? 0,
|
||||
pageNo: pagination.page_no ?? 0,
|
||||
});
|
||||
|
||||
const goToPage = (pageNo)=>{
|
||||
const from = (pageNo-1) * pagination.page_size + 1;
|
||||
const to = from + pagination.page_size - 1;
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, from, to);
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const onInputChange = (key, value)=>{
|
||||
setInputs((prev)=>({...prev, [key]: value}));
|
||||
};
|
||||
|
||||
const onInputKeydown = (e)=>{
|
||||
if(e.code === 'Enter' && !errorInputs.from && !errorInputs.to) {
|
||||
e.preventDefault();
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to);
|
||||
}
|
||||
};
|
||||
|
||||
const onInputKeydownPageNo = (e)=>{
|
||||
if(e.code === 'Enter' && !errorInputs.pageNo) {
|
||||
e.preventDefault();
|
||||
goToPage(inputs.pageNo);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
// validate
|
||||
setErrorInputs((prev)=>{
|
||||
let errors = {...prev};
|
||||
|
||||
if(minMaxValidator('', inputs.pageNo, 1, pagination.page_count)) {
|
||||
errors.pageNo = true;
|
||||
} else {
|
||||
errors.pageNo = false;
|
||||
}
|
||||
if(minMaxValidator('', inputs.from, 1, inputs.to)) {
|
||||
errors.from = true;
|
||||
} else {
|
||||
errors.from = false;
|
||||
}
|
||||
if(minMaxValidator('', inputs.to, 1, totalRowCount)) {
|
||||
errors.to = true;
|
||||
} else {
|
||||
errors.to = false;
|
||||
}
|
||||
|
||||
return errors;
|
||||
});
|
||||
}, [inputs, pagination]);
|
||||
|
||||
return (
|
||||
<Box className='PaginationInputs'>
|
||||
{editPageRange ?
|
||||
<Box display="flex" gap="2px" alignItems="center">
|
||||
<div>{gettext('Showing rows:')}</div>
|
||||
<InputText size="small"
|
||||
controlProps={{maxLength: 7}}
|
||||
style={{
|
||||
maxWidth: '10ch'
|
||||
}}
|
||||
value={inputs.from}
|
||||
onChange={(value)=>onInputChange('from', value)}
|
||||
onKeyDown={onInputKeydown}
|
||||
error={errorInputs['from']}
|
||||
/>
|
||||
<div>{gettext('to')}</div>
|
||||
<InputText size="small"
|
||||
controlProps={{maxLength: 7}}
|
||||
style={{
|
||||
maxWidth: '10ch'
|
||||
}}
|
||||
value={inputs.to}
|
||||
onChange={(value)=>onInputChange('to', value)}
|
||||
onKeyDown={onInputKeydown}
|
||||
error={errorInputs['to']}
|
||||
/>
|
||||
</Box> : <span>{gettext('Showing rows: %s to %s', inputs.from, inputs.to)}</span>}
|
||||
<PgButtonGroup>
|
||||
{editPageRange && <PgIconButton size="xs"
|
||||
title={editPageRange ? gettext('Apply (or press Enter on input)') : gettext('Edit range')}
|
||||
onClick={()=>eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_WINDOW, inputs.from, inputs.to)}
|
||||
icon={<CheckRoundedIcon />}
|
||||
/>}
|
||||
<PgIconButton size="xs"
|
||||
title={editPageRange ? gettext('Cancel edit') : gettext('Edit range')}
|
||||
onClick={()=>setEditPageRange((prev)=>!prev)}
|
||||
icon={editPageRange ? <EditOffRoundedIcon /> : <EditRoundedIcon />}
|
||||
/>
|
||||
</PgButtonGroup>
|
||||
<div className='PaginationInputs-divider'> </div>
|
||||
<span>{gettext('Page No:')}</span>
|
||||
<InputText size="small"
|
||||
controlProps={{maxLength: 7}}
|
||||
style={{
|
||||
maxWidth: '10ch'
|
||||
}}
|
||||
value={inputs.pageNo}
|
||||
onChange={(value)=>onInputChange('pageNo', value)}
|
||||
onKeyDown={onInputKeydownPageNo}
|
||||
error={errorInputs['pageNo']}
|
||||
/>
|
||||
<span> {gettext('of')} {pagination.page_count}</span>
|
||||
<div className='PaginationInputs-divider'> </div>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('First Page')} disabled={pagination.page_no == 1} onClick={()=>goToPage(1)} icon={<SkipPreviousRoundedIcon />}/>
|
||||
<PgIconButton title={gettext('Previous Page')} disabled={pagination.page_no == 1} onClick={()=>goToPage(pagination.page_no-1)} icon={<FastRewindRoundedIcon />}/>
|
||||
<PgIconButton title={gettext('Next Page')} disabled={pagination.page_no == pagination.page_count} onClick={()=>goToPage(pagination.page_no+1)} icon={<FastForwardRoundedIcon />}/>
|
||||
<PgIconButton title={gettext('Last Page')} disabled={pagination.page_no == pagination.page_count} onClick={()=>goToPage(pagination.page_count)} icon={<SkipNextRoundedIcon />} />
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
export function ResultSetToolbar({query, canEdit, totalRowCount, pagination, allRowsSelect}) {
|
||||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const queryToolCtx = useContext(QueryToolContext);
|
||||
const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false);
|
||||
|
@ -208,45 +357,74 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
|||
},
|
||||
], queryToolCtx.mainContainerRef);
|
||||
|
||||
const clearSelection = ()=>{
|
||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDiv>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
|
||||
shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} />
|
||||
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
|
||||
shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']} onClick={copyData} />
|
||||
<PgIconButton title={gettext('Copy options')} icon={<KeyboardArrowDownIcon />} splitButton
|
||||
name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} />
|
||||
<PgIconButton title={gettext('Paste')} icon={<PasteIcon />}
|
||||
shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} />
|
||||
<PgIconButton title={gettext('Paste options')} icon={<KeyboardArrowDownIcon />} splitButton
|
||||
name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} />
|
||||
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
|
||||
shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} />
|
||||
</PgButtonGroup>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Save Data Changes')} icon={<SaveDataIcon />}
|
||||
shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/>
|
||||
</PgButtonGroup>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Save results to file')} icon={<GetAppRoundedIcon />}
|
||||
onClick={downloadResult} shortcut={queryToolPref.download_results}
|
||||
disabled={buttonsDisabled['save-result']} />
|
||||
</PgButtonGroup>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
|
||||
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
|
||||
</PgButtonGroup>
|
||||
{query &&
|
||||
<>
|
||||
<Box display="flex" alignItems="center" gap="4px">
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />}
|
||||
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/>
|
||||
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
|
||||
shortcut={queryToolPref.btn_add_row} disabled={!canEdit} onClick={addRow} />
|
||||
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
|
||||
shortcut={FIXED_PREF.copy} disabled={buttonsDisabled['copy-rows']||allRowsSelect=='ALL'} onClick={copyData} />
|
||||
<PgIconButton title={gettext('Copy options')} icon={<KeyboardArrowDownIcon />} splitButton
|
||||
name="menu-copyheader" ref={copyMenuRef} onClick={openMenu} />
|
||||
<PgIconButton title={gettext('Paste')} icon={<PasteIcon />}
|
||||
shortcut={queryToolPref.btn_paste_row} disabled={!canEdit} onClick={pasteRows} />
|
||||
<PgIconButton title={gettext('Paste options')} icon={<KeyboardArrowDownIcon />} splitButton
|
||||
name="menu-pasteoptions" ref={pasetMenuRef} onClick={openMenu} />
|
||||
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
|
||||
shortcut={queryToolPref.btn_delete_row} disabled={buttonsDisabled['delete-rows'] || !canEdit} onClick={deleteRows} />
|
||||
</PgButtonGroup>
|
||||
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup query={query} />}
|
||||
</>
|
||||
}
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Save Data Changes')} icon={<SaveDataIcon />}
|
||||
shortcut={queryToolPref.save_data} disabled={buttonsDisabled['save-data'] || !canEdit} onClick={saveData}/>
|
||||
</PgButtonGroup>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Save results to file')} icon={<GetAppRoundedIcon />}
|
||||
onClick={downloadResult} shortcut={queryToolPref.download_results}
|
||||
disabled={buttonsDisabled['save-result']} />
|
||||
</PgButtonGroup>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
|
||||
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
|
||||
</PgButtonGroup>
|
||||
{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 query={query} />}
|
||||
</>
|
||||
}
|
||||
{
|
||||
allRowsSelect == 'PAGE' && (
|
||||
<div>
|
||||
<span>{gettext('All rows on this page are selected.')}</span>
|
||||
<PgButtonGroup size="small">
|
||||
<DefaultButton onClick={()=>eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED)}>Select All {totalRowCount} Rows</DefaultButton>
|
||||
</PgButtonGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
allRowsSelect == 'ALL' && (
|
||||
<div>
|
||||
<span>{gettext('All %s rows are selected.', totalRowCount)}</span>
|
||||
<PgButtonGroup size="small">
|
||||
<DefaultButton onClick={clearSelection}>{gettext('Clear Selection')}</DefaultButton>
|
||||
</PgButtonGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
<Box>
|
||||
<PaginationInputs key={JSON.stringify(pagination)} pagination={pagination} totalRowCount={totalRowCount} clearSelection={clearSelection} />
|
||||
</Box>
|
||||
</StyledDiv>
|
||||
<PgMenu
|
||||
anchorRef={copyMenuRef}
|
||||
|
|
|
@ -41,7 +41,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
|||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const [position, setPosition] = useState([1, 1]);
|
||||
const [lastTaskText, setLastTaskText] = useState(null);
|
||||
const [rowsCount, setRowsCount] = useState([0, 0]);
|
||||
const [rowsCount, setRowsCount] = useState(0);
|
||||
const [selectedRowsCount, setSelectedRowsCount] = useState(0);
|
||||
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
|
||||
isDirty: false,
|
||||
|
@ -52,6 +52,8 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
|||
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
|
||||
const eolMenuRef = React.useRef(null);
|
||||
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
|
||||
// NONE - no select, PAGE - show select all, ALL - select all.
|
||||
const [allRowsSelect, setAllRowsSelect] = useState('NONE');
|
||||
|
||||
useEffect(()=>{
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
|
||||
|
@ -70,20 +72,30 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
|||
pauseTimer(endTime);
|
||||
setLastTaskText(taskText);
|
||||
});
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{
|
||||
setRowsCount([fetched||0, total||0]);
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, (total)=>{
|
||||
setRowsCount(total);
|
||||
});
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, (v)=>{
|
||||
setAllRowsSelect(v);
|
||||
});
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, (rows)=>{
|
||||
setSelectedRowsCount(rows);
|
||||
});
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
const unregDataChange = eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (_isDirty, dataChangeStore)=>{
|
||||
setDataRowChangeCounts({
|
||||
added: Object.keys(dataChangeStore.added||{}).length,
|
||||
updated: Object.keys(dataChangeStore.updated||{}).length,
|
||||
deleted: Object.keys(dataChangeStore.deleted||{}).length,
|
||||
deleted: dataChangeStore.delete_all ? rowsCount : Object.keys(dataChangeStore.deleted||{}).length,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return ()=>{
|
||||
unregDataChange();
|
||||
};
|
||||
}, [rowsCount]);
|
||||
|
||||
let stagedText = '';
|
||||
if(dataRowChangeCounts.added > 0) {
|
||||
|
@ -98,7 +110,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
|||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Box className='StatusBar-padding StatusBar-divider'>{gettext('Total rows: %s of %s', rowsCount[0], rowsCount[1])}</Box>
|
||||
{rowsCount && <Box className='StatusBar-padding StatusBar-divider'>{gettext('Total rows: %s', rowsCount)}</Box>}
|
||||
{lastTaskText &&
|
||||
<Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
|
||||
}
|
||||
|
@ -106,13 +118,13 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
|||
<Box className='StatusBar-padding StatusBar-divider'>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
|
||||
}
|
||||
{Boolean(selectedRowsCount) &&
|
||||
<Box className='StatusBar-padding StatusBar-divider'>{gettext('Rows selected: %s',selectedRowsCount)}</Box>}
|
||||
<Box className='StatusBar-padding StatusBar-divider'>{gettext('Rows selected: %s', allRowsSelect == 'ALL' ? rowsCount : selectedRowsCount)}</Box>}
|
||||
{stagedText &&
|
||||
<Box className='StatusBar-padding StatusBar-divider'>
|
||||
<span>{gettext('Changes staged: %s', stagedText)}</span>
|
||||
</Box>
|
||||
}
|
||||
|
||||
|
||||
<Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}>
|
||||
<Box className="StatusBar-padding StatusBar-divider">
|
||||
<Tooltip title="Select EOL Sequence" disableInteractive enterDelay={2500}>
|
||||
|
|
|
@ -6,8 +6,8 @@ DELETE FROM {{ conn|qtIdent(nsp_name, object_name) }}
|
|||
{% elif no_of_keys > 1 %}
|
||||
WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN
|
||||
{% endif %}
|
||||
{### Rows to delete ###}
|
||||
{% if no_of_keys >= 1 %}{### Rows to delete ###}
|
||||
({% for obj in data %}{% if no_of_keys == 1 %}{{ obj[primary_key_labels[0]]|qtLiteral(conn) }}{% elif no_of_keys > 1 %}
|
||||
{### Here we need to make tuple for each row ###}
|
||||
({% for each_label in primary_key_labels %}{{ obj[each_label]|qtLiteral(conn) }}{% if not loop.last %}, {% endif %}{% endfor %}){% endif %}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %});
|
||||
{% endfor %}){% endif %};
|
||||
|
|
|
@ -14,7 +14,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\
|
|||
PREF_LABEL_EDITOR, PREF_LABEL_CSV_TXT, PREF_LABEL_RESULTS_GRID,\
|
||||
PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER
|
||||
from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields
|
||||
from config import ON_DEMAND_RECORD_COUNT
|
||||
from config import DATA_RESULT_ROWS_PER_PAGE
|
||||
|
||||
UPPER_CASE_STR = gettext('Upper case')
|
||||
LOWER_CASE_STR = gettext('Lower case')
|
||||
|
@ -346,15 +346,15 @@ def register_query_tool_preferences(self):
|
|||
),
|
||||
)
|
||||
|
||||
self.on_demand_record_count = self.preference.register(
|
||||
'Results_grid', 'on_demand_record_count',
|
||||
gettext("On demand record count"), 'integer', ON_DEMAND_RECORD_COUNT,
|
||||
min_val=1,
|
||||
self.data_result_rows_per_page = self.preference.register(
|
||||
'Results_grid', 'data_result_rows_per_page',
|
||||
gettext("Data result rows per page"), 'integer',
|
||||
DATA_RESULT_ROWS_PER_PAGE, min_val=10,
|
||||
category_label=PREF_LABEL_RESULTS_GRID,
|
||||
help_str=gettext('Specify the number of records to fetch in one batch '
|
||||
'in query tool when query result set is large. '
|
||||
'Changing this value will override '
|
||||
'ON_DEMAND_RECORD_COUNT setting from config file.')
|
||||
help_str=gettext('Specify the number of records to fetch in one batch.'
|
||||
' Changing this value will override'
|
||||
' DATA_RESULT_ROWS_PER_PAGE setting from config '
|
||||
' file.')
|
||||
)
|
||||
|
||||
self.sql_font_size = self.preference.register(
|
||||
|
|
|
@ -188,35 +188,37 @@ def save_changed_data(changed_data, columns_info, conn, command_obj,
|
|||
|
||||
# For deleted rows
|
||||
elif of_type == 'deleted':
|
||||
delete_all = changed_data.get('delete_all', False)
|
||||
list_of_sql[of_type] = []
|
||||
is_first = True
|
||||
rows_to_delete = []
|
||||
keys = None
|
||||
no_of_keys = None
|
||||
for each_row in changed_data[of_type]:
|
||||
rows_to_delete.append(changed_data[of_type][each_row])
|
||||
# Fetch the keys for SQL generation
|
||||
if is_first:
|
||||
# We need to covert dict_keys to normal list in
|
||||
# Python3
|
||||
# In Python2, it's already a list & We will also
|
||||
# fetch column names using index
|
||||
keys = list(
|
||||
changed_data[of_type][each_row].keys()
|
||||
)
|
||||
no_of_keys = len(keys)
|
||||
is_first = False
|
||||
# Map index with column name for each row
|
||||
for row in rows_to_delete:
|
||||
for k, v in row.items():
|
||||
# Set primary key with label & delete index based
|
||||
# mapped key
|
||||
try:
|
||||
row[changed_data['columns']
|
||||
[int(k)]['name']] = v
|
||||
except ValueError:
|
||||
continue
|
||||
del row[k]
|
||||
keys = []
|
||||
no_of_keys = 0
|
||||
if not delete_all:
|
||||
for each_row in changed_data[of_type]:
|
||||
rows_to_delete.append(changed_data[of_type][each_row])
|
||||
# Fetch the keys for SQL generation
|
||||
if is_first:
|
||||
# We need to covert dict_keys to normal list in
|
||||
# Python3
|
||||
# In Python2, it's already a list & We will also
|
||||
# fetch column names using index
|
||||
keys = list(
|
||||
changed_data[of_type][each_row].keys()
|
||||
)
|
||||
no_of_keys = len(keys)
|
||||
is_first = False
|
||||
# Map index with column name for each row
|
||||
for row in rows_to_delete:
|
||||
for k, v in row.items():
|
||||
# Set primary key with label & delete index based
|
||||
# mapped key
|
||||
try:
|
||||
row[changed_data['columns']
|
||||
[int(k)]['name']] = v
|
||||
except ValueError:
|
||||
continue
|
||||
del row[k]
|
||||
|
||||
sql = render_template(
|
||||
"/".join([command_obj.sql_path, 'delete.sql']),
|
||||
|
|
|
@ -1308,6 +1308,7 @@ WHERE db.datname = current_database()""")
|
|||
return True, {'columns': columns, 'rows': rows}
|
||||
|
||||
def async_fetchmany_2darray(self, records=2000,
|
||||
from_rownum=0, to_rownum=0,
|
||||
formatted_exception_msg=False):
|
||||
"""
|
||||
User should poll and check if status is ASYNC_OK before calling this
|
||||
|
@ -1342,6 +1343,10 @@ WHERE db.datname = current_database()""")
|
|||
try:
|
||||
if records == -1:
|
||||
result = cur.fetchall(_tupples=True)
|
||||
elif records is None:
|
||||
result = cur.fetchwindow(from_rownum=from_rownum,
|
||||
to_rownum=to_rownum,
|
||||
_tupples=True)
|
||||
else:
|
||||
result = cur.fetchmany(records, _tupples=True)
|
||||
except psycopg.ProgrammingError:
|
||||
|
@ -1538,6 +1543,12 @@ Failed to reset the connection to the server due to following error:
|
|||
|
||||
return self.row_count
|
||||
|
||||
@property
|
||||
def total_rows(self):
|
||||
if self.__async_cursor is None:
|
||||
return 0
|
||||
return self.__async_cursor.rowcount
|
||||
|
||||
def get_column_info(self):
|
||||
"""
|
||||
This function will returns list of columns for last async sql command
|
||||
|
|
|
@ -364,6 +364,20 @@ class AsyncDictCursor(_async_cursor):
|
|||
self.row_factory = dict_row
|
||||
return res
|
||||
|
||||
def fetchwindow(self, from_rownum=0, to_rownum=0, _tupples=False):
|
||||
"""
|
||||
Fetch many tuples as ordered dictionary list.
|
||||
"""
|
||||
self._odt_desc = None
|
||||
self.row_factory = tuple_row
|
||||
asyncio.run(self._scrollcur(from_rownum, "absolute"))
|
||||
res = asyncio.run(self._fetchmany(to_rownum - from_rownum + 1))
|
||||
if not _tupples and res is not None:
|
||||
res = [self._dict_tuple(t) for t in res]
|
||||
|
||||
self.row_factory = dict_row
|
||||
return res
|
||||
|
||||
async def _scrollcur(self, position, mode):
|
||||
"""
|
||||
Fetch all tuples as ordered dictionary list.
|
||||
|
|
|
@ -47,10 +47,10 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
|||
|
||||
def runTest(self):
|
||||
self._reset_options()
|
||||
# on demand result set on scrolling.
|
||||
print("\nOn demand query result... ",
|
||||
# pagination result on page change.
|
||||
print("\nPagination query result... ",
|
||||
file=sys.stderr, end="")
|
||||
self._on_demand_result()
|
||||
self._pagination_result()
|
||||
self.page.clear_query_tool()
|
||||
|
||||
# explain query with verbose and cost
|
||||
|
@ -129,18 +129,12 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
|||
# close menu
|
||||
query_op.click()
|
||||
|
||||
def _on_demand_result(self):
|
||||
ON_DEMAND_CHUNKS = 2
|
||||
row_id_to_find = config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS
|
||||
|
||||
query = """-- On demand query result on scroll
|
||||
-- Grid select all
|
||||
-- Column select all
|
||||
def _pagination_result(self):
|
||||
query = """-- Pagination result
|
||||
SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
|
||||
config.ON_DEMAND_RECORD_COUNT * ON_DEMAND_CHUNKS)
|
||||
config.DATA_RESULT_ROWS_PER_PAGE * 2.5)
|
||||
|
||||
print("\nOn demand result set on scrolling... ",
|
||||
file=sys.stderr, end="")
|
||||
print("\nPagination result... ", file=sys.stderr, end="")
|
||||
self.page.execute_query(query)
|
||||
|
||||
# wait for header of the table to be visible
|
||||
|
@ -151,94 +145,33 @@ SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
|
|||
(By.CSS_SELECTOR,
|
||||
QueryToolLocators.query_output_cells)))
|
||||
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.query_output_canvas_css)
|
||||
for i, page in enumerate([
|
||||
{'page_info': '1 to 1000', 'cell_rownum': '1'},
|
||||
{'page_info': '1001 to 2000', 'cell_rownum': '1001'},
|
||||
{'page_info': '2001 to 2500', 'cell_rownum': '2001'}
|
||||
]):
|
||||
page_info = self.page.find_by_css_selector(
|
||||
QueryToolLocators.pagination_inputs +
|
||||
f' span:nth-of-type(1)')
|
||||
|
||||
self._check_ondemand_result(row_id_to_find)
|
||||
print("OK.", file=sys.stderr)
|
||||
self.assertEqual(page_info.text, f"Showing: {page['page_info']}")
|
||||
|
||||
print("On demand result set on grid select all... ",
|
||||
file=sys.stderr, end="")
|
||||
self.page.click_execute_query_button()
|
||||
page_info = self.page.find_by_css_selector(
|
||||
QueryToolLocators.pagination_inputs + ' span:nth-of-type(3)')
|
||||
|
||||
# wait for header of the table to be visible
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.query_output_canvas_css)
|
||||
self.assertEqual(page_info.text, "of 3")
|
||||
|
||||
# wait for the rows in the table to be displayed
|
||||
self.wait.until(EC.presence_of_element_located(
|
||||
(By.CSS_SELECTOR,
|
||||
QueryToolLocators.query_output_cells))
|
||||
)
|
||||
cell_rownum = self.page.find_by_css_selector(
|
||||
QueryToolLocators.query_output_cells + ':nth-of-type(1)')
|
||||
|
||||
# Select all rows in a table
|
||||
multiple_check = True
|
||||
while multiple_check:
|
||||
try:
|
||||
select_all = self.wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, QueryToolLocators.select_all_column)))
|
||||
select_all.click()
|
||||
multiple_check = False
|
||||
except (StaleElementReferenceException,
|
||||
ElementClickInterceptedException):
|
||||
pass
|
||||
self.assertEqual(cell_rownum.text, page['cell_rownum'])
|
||||
|
||||
self._check_ondemand_result(row_id_to_find)
|
||||
print("OK.", file=sys.stderr)
|
||||
|
||||
print("On demand result set on column select all... ",
|
||||
file=sys.stderr, end="")
|
||||
self.page.click_execute_query_button()
|
||||
|
||||
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||
|
||||
# wait for header of the table to be visible
|
||||
self.wait.until(EC.visibility_of_element_located(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
|
||||
|
||||
# wait for the rows in the table to be displayed
|
||||
self.wait.until(EC.presence_of_element_located(
|
||||
(By.CSS_SELECTOR,
|
||||
QueryToolLocators.query_output_cells))
|
||||
)
|
||||
|
||||
self.wait.until(EC.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
|
||||
|
||||
self._check_ondemand_result(row_id_to_find)
|
||||
print("OK.", file=sys.stderr)
|
||||
|
||||
def _check_ondemand_result(self, row_id_to_find):
|
||||
# scroll to bottom to bring last row of next chunk in viewport.
|
||||
scroll = 10
|
||||
status = False
|
||||
while scroll:
|
||||
# click on first data column to select all column.
|
||||
column_1 = \
|
||||
if i < 2:
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.output_column_header_css.format('id1'))
|
||||
column_1.click()
|
||||
grid = self.page.find_by_css_selector('.rdg')
|
||||
scrolling_height = grid.size['height']
|
||||
self.driver.execute_script(
|
||||
"document.querySelector('.rdg').scrollTop="
|
||||
"document.querySelector('.rdg').scrollHeight"
|
||||
)
|
||||
# Table height takes some time to update, for which their is no
|
||||
# particular way
|
||||
time.sleep(2)
|
||||
if grid.size['height'] == scrolling_height and \
|
||||
self.page.check_if_element_exist_by_xpath(
|
||||
QueryToolLocators.output_column_data_xpath.format(
|
||||
row_id_to_find)):
|
||||
status = True
|
||||
break
|
||||
else:
|
||||
scroll -= 1
|
||||
QueryToolLocators.pagination_inputs +
|
||||
' button[aria-label="Next Page"]').click()
|
||||
|
||||
self.assertTrue(
|
||||
status, "Element is not loaded to the rows id: "
|
||||
"{}".format(row_id_to_find))
|
||||
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||
|
||||
def _query_tool_explain_with_verbose_and_cost(self):
|
||||
query = """-- Explain query with verbose and cost
|
||||
|
|
|
@ -172,8 +172,6 @@ CREATE TABLE public.nonintpkey
|
|||
self._copy_paste_row(config_data_local)
|
||||
|
||||
self._update_row(config_data_local)
|
||||
self.page.click_tab("Messages")
|
||||
self._verify_messsages("")
|
||||
self.page.click_tab("Data Output")
|
||||
updated_row_data = {
|
||||
i: config_data_local['update'][i] if i in config_data_local[
|
||||
|
|
|
@ -232,6 +232,8 @@ class QueryToolLocators:
|
|||
|
||||
query_output_canvas_css = "#id-dataoutput .rdg"
|
||||
|
||||
pagination_inputs = "#id-dataoutput .PaginationInputs"
|
||||
|
||||
query_output_cells = ".rdg-cell[role='gridcell']"
|
||||
|
||||
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"
|
||||
|
|
Loading…
Reference in New Issue