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
|
* 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
|
* 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
|
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.
|
changed to text/plain. Keyword highlighting and code folding will be disabled.
|
||||||
This will improve editor performance with large files.
|
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.
|
highlight matched selected text.
|
||||||
|
|
||||||
.. image:: images/preferences_sql_explain.png
|
.. 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*
|
* 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.
|
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.
|
parse and underline the query at the cursor position.
|
||||||
|
|
||||||
* When the *Underlined query execute warning?* switch is set to *True*, query tool
|
* 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 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*.
|
will appear only if *Underline query at cursor?* is set to *False*.
|
||||||
|
|
||||||
.. image:: images/preferences_sql_results_grid.png
|
.. 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
|
* 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
|
set to *Column data*. If 'Columns sized by' is set to *Column name* then this
|
||||||
setting won't have any effect.
|
setting won't have any effect.
|
||||||
* Specify the number of records to fetch in one batch in query tool when
|
* Specify the number of records to fetch in one batch. Changing this value will
|
||||||
query result set is large. Changing this value will override
|
override DATA_RESULT_ROWS_PER_PAGE setting from config file.
|
||||||
ON_DEMAND_ROW_COUNT setting from config file.
|
|
||||||
* Use the *Result copy field separator* drop-down listbox to select the field
|
* Use the *Result copy field separator* drop-down listbox to select the field
|
||||||
separator for copied data.
|
separator for copied data.
|
||||||
* Use the *Result copy quote character* drop-down listbox to select the quote
|
* 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
|
* Use the *Data type case* option to specify whether to change data types
|
||||||
into upper, lower, or preserve case.
|
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.
|
in parenthesized expressions to be kept on single line.
|
||||||
* Use the *Function case* option to specify whether to change function
|
* Use the *Function case* option to specify whether to change function
|
||||||
names into upper, lower, or preserve case.
|
names into upper, lower, or preserve case.
|
||||||
|
@ -531,7 +530,7 @@ reformatting of SQL.
|
||||||
(object names) into upper, lower, or capitalized case.
|
(object names) into upper, lower, or capitalized case.
|
||||||
* Use the *Keyword case* option to specify whether to change keywords into
|
* Use the *Keyword case* option to specify whether to change keywords into
|
||||||
upper, lower, or preserve case.
|
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.
|
between SQL statements. If set to zero it puts no new line.
|
||||||
* Use *Logical operator new line* to specify newline placement before or
|
* Use *Logical operator new line* to specify newline placement before or
|
||||||
after logical operators (AND, OR, XOR).
|
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
|
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
|
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
|
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
|
SQL editor, highlight the text that you want the server to execute, and click the
|
||||||
*Execute script* icon.
|
*Execute script* icon.
|
||||||
|
|
||||||
.. image:: images/query_execute_script.png
|
.. 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*
|
* Use the *Save results to file* icon to save the content of the *Data Output*
|
||||||
tab as a comma-delimited file.
|
tab as a comma-delimited file.
|
||||||
* Edit the data in the result set of a SELECT query if it is updatable.
|
* Edit the data in the result set of a SELECT query if it is updatable.
|
||||||
|
* Move between pages of data result.
|
||||||
|
|
||||||
.. _updatable-result-set:
|
.. _updatable-result-set:
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,48 @@ Data Editing Options
|
||||||
| SQL | Use the SQL button to check the current query that gave the data. | |
|
| 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
|
Status Bar
|
||||||
**********
|
**********
|
||||||
|
|
||||||
|
|
|
@ -513,10 +513,10 @@ THREADED_MODE = True
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# Number of records to fetch in one batch in query tool when query result
|
# Number of records to fetch in one page in query tool when query result
|
||||||
# set is large.
|
# 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
|
# Allow users to display Gravatar image for their username in Server mode
|
||||||
|
|
|
@ -15,6 +15,7 @@ import secrets
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
import threading
|
import threading
|
||||||
|
import math
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE
|
from config import PG_DEFAULT_DRIVER, ALLOW_SAVE_PASSWORD, SHARED_STORAGE
|
||||||
|
@ -106,8 +107,7 @@ class SqlEditorModule(PgAdminModule):
|
||||||
'sqleditor.view_data_start',
|
'sqleditor.view_data_start',
|
||||||
'sqleditor.query_tool_start',
|
'sqleditor.query_tool_start',
|
||||||
'sqleditor.poll',
|
'sqleditor.poll',
|
||||||
'sqleditor.fetch',
|
'sqleditor.fetch_window',
|
||||||
'sqleditor.fetch_all',
|
|
||||||
'sqleditor.fetch_all_from_start',
|
'sqleditor.fetch_all_from_start',
|
||||||
'sqleditor.save',
|
'sqleditor.save',
|
||||||
'sqleditor.inclusive_filter',
|
'sqleditor.inclusive_filter',
|
||||||
|
@ -470,7 +470,8 @@ def _init_sqleditor(trans_id, connect, sgid, sid, did, dbname=None, **kwargs):
|
||||||
"prompt_password": True,
|
"prompt_password": True,
|
||||||
"allow_save_password": True
|
"allow_save_password": True
|
||||||
if ALLOW_SAVE_PASSWORD and
|
if ALLOW_SAVE_PASSWORD and
|
||||||
session['allow_save_password'] else False,
|
session.get('allow_save_password', None)
|
||||||
|
else False,
|
||||||
}
|
}
|
||||||
), '', ''
|
), '', ''
|
||||||
else:
|
else:
|
||||||
|
@ -925,7 +926,6 @@ def poll(trans_id):
|
||||||
rows_affected = 0
|
rows_affected = 0
|
||||||
rows_fetched_from = 0
|
rows_fetched_from = 0
|
||||||
rows_fetched_to = 0
|
rows_fetched_to = 0
|
||||||
has_more_rows = False
|
|
||||||
columns = dict()
|
columns = dict()
|
||||||
columns_info = None
|
columns_info = None
|
||||||
primary_keys = None
|
primary_keys = None
|
||||||
|
@ -936,8 +936,8 @@ def poll(trans_id):
|
||||||
additional_messages = None
|
additional_messages = None
|
||||||
notifies = None
|
notifies = None
|
||||||
data_obj = {}
|
data_obj = {}
|
||||||
on_demand_record_count = Preferences.module(MODULE_NAME).\
|
data_result_rows_per_page = Preferences.module(MODULE_NAME).\
|
||||||
preference('on_demand_record_count').get()
|
preference('data_result_rows_per_page').get()
|
||||||
# Check the transaction and connection status
|
# Check the transaction and connection status
|
||||||
status, error_msg, conn, trans_obj, session_obj = \
|
status, error_msg, conn, trans_obj, session_obj = \
|
||||||
check_transaction_status(trans_id)
|
check_transaction_status(trans_id)
|
||||||
|
@ -1004,7 +1004,8 @@ def poll(trans_id):
|
||||||
status = 'Success'
|
status = 'Success'
|
||||||
rows_affected = conn.rows_affected()
|
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
|
# There may be additional messages even if result is present
|
||||||
# eg: Function can provide result as well as RAISE messages
|
# eg: Function can provide result as well as RAISE messages
|
||||||
|
@ -1081,8 +1082,6 @@ def poll(trans_id):
|
||||||
# means nothing to fetch
|
# means nothing to fetch
|
||||||
if result and rows_affected > -1:
|
if result and rows_affected > -1:
|
||||||
res_len = len(result)
|
res_len = len(result)
|
||||||
if res_len == on_demand_record_count:
|
|
||||||
has_more_rows = True
|
|
||||||
|
|
||||||
if res_len > 0:
|
if res_len > 0:
|
||||||
rows_fetched_from = trans_obj.get_fetched_row_cnt()
|
rows_fetched_from = trans_obj.get_fetched_row_cnt()
|
||||||
|
@ -1126,6 +1125,15 @@ def poll(trans_id):
|
||||||
data_obj['db_id'] = trans_obj.did \
|
data_obj['db_id'] = trans_obj.did \
|
||||||
if trans_obj is not None and hasattr(trans_obj, 'did') else 0
|
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(
|
return make_json_response(
|
||||||
data={
|
data={
|
||||||
'status': status, 'result': result,
|
'status': status, 'result': result,
|
||||||
|
@ -1134,7 +1142,6 @@ def poll(trans_id):
|
||||||
'rows_fetched_to': rows_fetched_to,
|
'rows_fetched_to': rows_fetched_to,
|
||||||
'additional_messages': additional_messages,
|
'additional_messages': additional_messages,
|
||||||
'notifies': notifies,
|
'notifies': notifies,
|
||||||
'has_more_rows': has_more_rows,
|
|
||||||
'colinfo': columns_info,
|
'colinfo': columns_info,
|
||||||
'primary_keys': primary_keys,
|
'primary_keys': primary_keys,
|
||||||
'types': types,
|
'types': types,
|
||||||
|
@ -1143,26 +1150,20 @@ def poll(trans_id):
|
||||||
'oids': oids,
|
'oids': oids,
|
||||||
'transaction_status': transaction_status,
|
'transaction_status': transaction_status,
|
||||||
'data_obj': data_obj,
|
'data_obj': data_obj,
|
||||||
|
'pagination': pagination,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route(
|
@blueprint.route(
|
||||||
'/fetch/<int:trans_id>', methods=["GET"], endpoint='fetch'
|
'/fetch_window/<int:trans_id>/<int:from_rownum>/<int:to_rownum>',
|
||||||
)
|
methods=["GET"], endpoint='fetch_window'
|
||||||
@blueprint.route(
|
|
||||||
'/fetch/<int:trans_id>/<int:fetch_all>', methods=["GET"],
|
|
||||||
endpoint='fetch_all'
|
|
||||||
)
|
)
|
||||||
@pga_login_required
|
@pga_login_required
|
||||||
def fetch(trans_id, fetch_all=None):
|
def fetch_window(trans_id, from_rownum=0, to_rownum=0):
|
||||||
result = None
|
result = None
|
||||||
has_more_rows = False
|
|
||||||
rows_fetched_from = 0
|
rows_fetched_from = 0
|
||||||
rows_fetched_to = 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
|
# Check the transaction and connection status
|
||||||
status, error_msg, conn, trans_obj, session_obj = \
|
status, error_msg, conn, trans_obj, session_obj = \
|
||||||
|
@ -1174,33 +1175,39 @@ def fetch(trans_id, fetch_all=None):
|
||||||
status=404)
|
status=404)
|
||||||
|
|
||||||
if status and conn is not None and session_obj is not None:
|
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:
|
if not status:
|
||||||
status = 'Error'
|
status = 'Error'
|
||||||
else:
|
else:
|
||||||
status = 'Success'
|
status = 'Success'
|
||||||
res_len = len(result) if result else 0
|
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:
|
if res_len:
|
||||||
rows_fetched_from = trans_obj.get_fetched_row_cnt()
|
rows_fetched_from = from_rownum
|
||||||
trans_obj.update_fetched_row_cnt(rows_fetched_from + res_len)
|
rows_fetched_to = rows_fetched_from + res_len - 1
|
||||||
rows_fetched_from += 1
|
|
||||||
rows_fetched_to = trans_obj.get_fetched_row_cnt()
|
|
||||||
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
|
session_obj['command_obj'] = pickle.dumps(trans_obj, -1)
|
||||||
update_session_grid_transaction(trans_id, session_obj)
|
update_session_grid_transaction(trans_id, session_obj)
|
||||||
else:
|
else:
|
||||||
status = 'NotConnected'
|
status = 'NotConnected'
|
||||||
result = error_msg
|
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(
|
return make_json_response(
|
||||||
data={
|
data={
|
||||||
'status': status,
|
'status': status,
|
||||||
'result': result,
|
'result': result,
|
||||||
'has_more_rows': has_more_rows,
|
'pagination': pagination,
|
||||||
'rows_fetched_from': rows_fetched_from,
|
'row_count': conn.row_count,
|
||||||
'rows_fetched_to': rows_fetched_to
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ export const QUERY_TOOL_EVENTS = {
|
||||||
STOP_QUERY: 'STOP_QUERY',
|
STOP_QUERY: 'STOP_QUERY',
|
||||||
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
|
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
|
||||||
SET_MESSAGE: 'SET_MESSAGE',
|
SET_MESSAGE: 'SET_MESSAGE',
|
||||||
ROWS_FETCHED: 'ROWS_FETCHED',
|
TOTAL_ROWS_COUNT: 'TOTAL_ROWS_COUNT',
|
||||||
SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED',
|
SELECTED_ROWS_COLS_CELL_CHANGED: 'SELECTED_ROWS_COLS_CELL_CHANGED',
|
||||||
DATAGRID_CHANGED: 'DATAGRID_CHANGED',
|
DATAGRID_CHANGED: 'DATAGRID_CHANGED',
|
||||||
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
|
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
|
||||||
|
@ -56,8 +56,12 @@ export const QUERY_TOOL_EVENTS = {
|
||||||
PUSH_HISTORY: 'PUSH_HISTORY',
|
PUSH_HISTORY: 'PUSH_HISTORY',
|
||||||
HANDLE_API_ERROR: 'HANDLE_API_ERROR',
|
HANDLE_API_ERROR: 'HANDLE_API_ERROR',
|
||||||
SET_FILTER_INFO: 'SET_FILTER_INFO',
|
SET_FILTER_INFO: 'SET_FILTER_INFO',
|
||||||
FETCH_MORE_ROWS: 'FETCH_MORE_ROWS',
|
|
||||||
REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION',
|
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_LAST_FOCUS: 'EDITOR_LAST_FOCUS',
|
||||||
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
|
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
|
||||||
|
@ -109,4 +113,4 @@ export const PANELS = {
|
||||||
|
|
||||||
export const MAX_QUERY_LENGTH = 1000000;
|
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 eventBus = useContext(QueryToolEventsContext);
|
||||||
const dataGridExtras = useContext(DataGridExtrasContext);
|
const dataGridExtras = useContext(DataGridExtrasContext);
|
||||||
const onClick = ()=>{
|
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(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -149,6 +148,15 @@ function SelectAllHeaderRenderer({isCellSelected}) {
|
||||||
cellRef.current?.focus({ preventScroll: true });
|
cellRef.current?.focus({ preventScroll: true });
|
||||||
}, [isCellSelected]);
|
}, [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}
|
return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
|
||||||
tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>;
|
tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>;
|
||||||
}
|
}
|
||||||
|
@ -167,15 +175,13 @@ function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsCha
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick = ()=>{
|
const onClick = ()=>{
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
|
const newSelectedCols = new Set(selectedColumns);
|
||||||
const newSelectedCols = new Set(selectedColumns);
|
if (newSelectedCols.has(column.idx)) {
|
||||||
if (newSelectedCols.has(column.idx)) {
|
newSelectedCols.delete(column.idx);
|
||||||
newSelectedCols.delete(column.idx);
|
} else {
|
||||||
} else {
|
newSelectedCols.add(column.idx);
|
||||||
newSelectedCols.add(column.idx);
|
}
|
||||||
}
|
onSelectedColumnsChange(newSelectedCols);
|
||||||
onSelectedColumnsChange(newSelectedCols);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = selectedColumns.has(column.idx);
|
const isSelected = selectedColumns.has(column.idx);
|
||||||
|
@ -296,9 +302,10 @@ function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
|
||||||
}
|
}
|
||||||
function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) {
|
function RowNumColFormatter({row, rowKeyGetter, rowIdx, dataChangeStore, onSelectedColumnsChange}) {
|
||||||
const [isRowSelected, onRowSelectionChange] = useRowSelection();
|
const [isRowSelected, onRowSelectionChange] = useRowSelection();
|
||||||
|
const {startRowNum} = useContext(DataGridExtrasContext);
|
||||||
|
|
||||||
let rowKey = rowKeyGetter(row);
|
let rowKey = rowKeyGetter(row);
|
||||||
let rownum = rowIdx+1;
|
let rownum = rowIdx+(startRowNum??1);
|
||||||
if(rowKey in (dataChangeStore?.added || {})) {
|
if(rowKey in (dataChangeStore?.added || {})) {
|
||||||
rownum = rownum+'+';
|
rownum = rownum+'+';
|
||||||
} else if(rowKey in (dataChangeStore?.deleted || {})) {
|
} else if(rowKey in (dataChangeStore?.deleted || {})) {
|
||||||
|
@ -375,7 +382,7 @@ function getTextWidth(column, rows, canvas, columnWidthBy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
|
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
|
||||||
onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) {
|
onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, startRowNum, ...props}) {
|
||||||
const [readyColumns, setReadyColumns] = useState([]);
|
const [readyColumns, setReadyColumns] = useState([]);
|
||||||
const eventBus = useContext(QueryToolEventsContext);
|
const eventBus = useContext(QueryToolEventsContext);
|
||||||
const onSelectedColumnsChangeWrapped = (arg)=>{
|
const onSelectedColumnsChangeWrapped = (arg)=>{
|
||||||
|
@ -392,7 +399,7 @@ export default function QueryToolDataGrid({columns, rows, totalRowCount, dataCha
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dataGridExtras = useMemo(()=>({
|
const dataGridExtras = useMemo(()=>({
|
||||||
onSelectedCellChange, handleCopy
|
onSelectedCellChange, handleCopy, startRowNum
|
||||||
}), [onSelectedCellChange]);
|
}), [onSelectedCellChange]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
|
|
|
@ -276,10 +276,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT
|
||||||
}, [queryToolConnCtx.connectionStatus]);
|
}, [queryToolConnCtx.connectionStatus]);
|
||||||
|
|
||||||
const onCommitClick=()=>{
|
const onCommitClick=()=>{
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, '', true);
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', {external: true});
|
||||||
};
|
};
|
||||||
const onRollbackClick=()=>{
|
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)=>{
|
const executeMacro = (m)=>{
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql);
|
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() || '';
|
query = query || editor.current?.getValue() || '';
|
||||||
}
|
}
|
||||||
if(query) {
|
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 {
|
} 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={
|
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();
|
let startTime = new Date();
|
||||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, '');
|
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.setStartTime(startTime);
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA;
|
this.historyQuerySource = flags.isQueryTool ? QuerySources.EXECUTE : QuerySources.VIEW_DATA;
|
||||||
if(explainObject) {
|
if(flags.refreshData) {
|
||||||
|
this.historyQuerySource = null;
|
||||||
|
} else if(explainObject) {
|
||||||
if(explainObject.analyze) {
|
if(explainObject.analyze) {
|
||||||
this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE;
|
this.historyQuerySource = QuerySources.EXPLAIN_ANALYZE;
|
||||||
} else {
|
} else {
|
||||||
|
@ -301,7 +306,7 @@ export class ResultSetUtils {
|
||||||
e,
|
e,
|
||||||
{
|
{
|
||||||
connectionLostCallback: ()=>{
|
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,
|
checkTransaction: true,
|
||||||
}
|
}
|
||||||
|
@ -357,7 +362,7 @@ export class ResultSetUtils {
|
||||||
});
|
});
|
||||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
||||||
connectionLostCallback: ()=>{
|
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,
|
checkTransaction: true,
|
||||||
});
|
});
|
||||||
|
@ -396,7 +401,7 @@ export class ResultSetUtils {
|
||||||
if(this.qtPref?.query_success_notification) {
|
if(this.qtPref?.query_success_notification) {
|
||||||
pgAdmin.Browser.notifier.success(msg);
|
pgAdmin.Browser.notifier.success(msg);
|
||||||
}
|
}
|
||||||
if(!ResultSetUtils.isQueryStillRunning(httpMessage)) {
|
if(!ResultSetUtils.isQueryStillRunning(httpMessage) && this.historyQuerySource) {
|
||||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, {
|
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.PUSH_HISTORY, {
|
||||||
status: true,
|
status: true,
|
||||||
start_time: this.startTime,
|
start_time: this.startTime,
|
||||||
|
@ -414,16 +419,12 @@ export class ResultSetUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMoreRows(all=false) {
|
getWindowRows(fromRownum, toRownum) {
|
||||||
let url = url_for('sqleditor.fetch', {
|
let url = url_for('sqleditor.fetch_window', {
|
||||||
'trans_id': this.transId,
|
'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);
|
return this.api.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -791,6 +792,11 @@ function dataChangeReducer(state, action) {
|
||||||
...dataChange.deleted,
|
...dataChange.deleted,
|
||||||
...action.add,
|
...action.add,
|
||||||
};
|
};
|
||||||
|
if(action.all) {
|
||||||
|
dataChange.delete_all = true;
|
||||||
|
} else {
|
||||||
|
dataChange.delete_all = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'reset':
|
case 'reset':
|
||||||
dataChange = {
|
dataChange = {
|
||||||
|
@ -798,6 +804,7 @@ function dataChangeReducer(state, action) {
|
||||||
added: {},
|
added: {},
|
||||||
added_index: {},
|
added_index: {},
|
||||||
deleted: {},
|
deleted: {},
|
||||||
|
delete_all: false,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -818,12 +825,14 @@ export function ResultSet() {
|
||||||
const [queryData, setQueryData] = useState(null);
|
const [queryData, setQueryData] = useState(null);
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
||||||
const api = getApiInstance();
|
const api = getApiInstance();
|
||||||
const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool));
|
const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool));
|
||||||
const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {});
|
const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {});
|
||||||
const [selectedRows, setSelectedRows] = useState(new Set());
|
const [selectedRows, setSelectedRows] = useState(new Set());
|
||||||
const [selectedColumns, setSelectedColumns] = 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 selectedCell = useRef([]);
|
||||||
const selectedRange = useRef(null);
|
const selectedRange = useRef(null);
|
||||||
const setSelectedCell = (val)=>{
|
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);
|
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 ()=>{
|
const yesCallback = async ()=>{
|
||||||
/* Reset */
|
/* Reset */
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null);
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null);
|
||||||
|
@ -871,7 +882,7 @@ export function ResultSet() {
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
setRows([]);
|
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(
|
queryToolCtx.modal.confirm(
|
||||||
gettext('Unsaved changes'),
|
gettext('Unsaved changes'),
|
||||||
gettext('The data has been modified, but not saved. Are you sure you wish to discard the 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.TRIGGER_INCLUDE_EXCLUDE_FILTER, triggerFilter);
|
||||||
|
|
||||||
eventBus.registerListener(QUERY_TOOL_EVENTS.GOTO_LAST_SCROLL, triggerResetScroll);
|
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(()=>{
|
useEffect(()=>{
|
||||||
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
const deregExec = eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
||||||
return ()=>{
|
return ()=>{
|
||||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.EXECUTION_START, executionStartCallback);
|
deregExec();
|
||||||
};
|
};
|
||||||
}, [dataChangeStore]);
|
}, [dataChangeStore, dataOutputQuery]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
fireRowsColsCellChanged();
|
fireRowsColsCellChanged();
|
||||||
|
setAllRowsSelect('NONE');
|
||||||
}, [selectedRows.size, selectedColumns.size]);
|
}, [selectedRows.size, selectedColumns.size]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.ALL_ROWS_SELECTED_STATUS, allRowsSelect);
|
||||||
|
}, [allRowsSelect]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
rsu.current.transId = queryToolCtx.params.trans_id;
|
rsu.current.transId = queryToolCtx.params.trans_id;
|
||||||
}, [queryToolCtx.params.trans_id]);
|
}, [queryToolCtx.params.trans_id]);
|
||||||
|
@ -1031,45 +1064,44 @@ export function ResultSet() {
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns);
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.RESET_GRAPH_VISUALISER, columns);
|
||||||
}, [columns]);
|
}, [columns]);
|
||||||
|
|
||||||
const fetchMoreRows = async (all=false, callback=undefined)=>{
|
const fetchWindow = async (fromRownum, toRownum, callback)=>{
|
||||||
if(queryData.has_more_rows) {
|
let res = [];
|
||||||
let res = [];
|
setLoaderText(gettext('Fetching rows...'));
|
||||||
setIsLoadingMore(true);
|
try {
|
||||||
try {
|
res = await rsu.current.getWindowRows(fromRownum, toRownum);
|
||||||
res = await rsu.current.getMoreRows(all);
|
const newRows = rsu.current.processRows(res.data.data.result, columns);
|
||||||
const newRows = rsu.current.processRows(res.data.data.result, columns);
|
setRows([...newRows]);
|
||||||
setRows((prevRows)=>[...prevRows, ...newRows]);
|
setQueryData((prev)=>({
|
||||||
setQueryData((prev)=>({
|
...prev,
|
||||||
...prev,
|
pagination: res.data.data.pagination,
|
||||||
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,
|
||||||
rows_fetched_to: res.data.data.rows_fetched_to!=0 ? res.data.data.rows_fetched_to : prev.rows_fetched_to,
|
}));
|
||||||
}));
|
} catch (e) {
|
||||||
} catch (e) {
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR,
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR,
|
e,
|
||||||
e,
|
{
|
||||||
{
|
connectionLostCallback: ()=>{
|
||||||
connectionLostCallback: ()=>{
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, {external: false, reconnect: true});
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, '', false, true);
|
},
|
||||||
},
|
checkTransaction: true,
|
||||||
checkTransaction: true,
|
}
|
||||||
}
|
);
|
||||||
);
|
} finally {
|
||||||
} finally {
|
setLoaderText('');
|
||||||
setIsLoadingMore(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
callback?.();
|
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(()=>{
|
useEffect(()=>{
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.ROWS_FETCHED, queryData?.rows_fetched_to, queryData?.rows_affected);
|
eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_WINDOW, fetchWindow);
|
||||||
}, [queryData?.rows_fetched_to, queryData?.rows_affected]);
|
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 = ()=>{
|
const warnSaveDataClose = ()=>{
|
||||||
// No changes.
|
// No changes.
|
||||||
|
@ -1116,6 +1148,7 @@ export function ResultSet() {
|
||||||
let {data: respData} = await rsu.current.saveData({
|
let {data: respData} = await rsu.current.saveData({
|
||||||
updated: dataChangeStore.updated,
|
updated: dataChangeStore.updated,
|
||||||
deleted: dataChangeStore.deleted,
|
deleted: dataChangeStore.deleted,
|
||||||
|
delete_all: dataChangeStore.delete_all,
|
||||||
added_index: dataChangeStore.added_index,
|
added_index: dataChangeStore.added_index,
|
||||||
added: added,
|
added: added,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
|
@ -1152,37 +1185,6 @@ export function ResultSet() {
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true);
|
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'});
|
dispatchDataChange({type: 'reset'});
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
setSelectedColumns(new Set());
|
setSelectedColumns(new Set());
|
||||||
|
@ -1192,6 +1194,8 @@ export function ResultSet() {
|
||||||
if(respData.data.transaction_status > CONNECTION_STATUS.TRANSACTION_STATUS_IDLE) {
|
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.'));
|
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) {
|
} catch (error) {
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false);
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, false);
|
||||||
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
|
||||||
|
@ -1292,6 +1296,7 @@ export function ResultSet() {
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
add: add,
|
add: add,
|
||||||
remove: remove,
|
remove: remove,
|
||||||
|
all: remove.length > 0 ? false : allRowsSelect == 'ALL',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1307,7 +1312,7 @@ export function ResultSet() {
|
||||||
return ()=>{
|
return ()=>{
|
||||||
eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows);
|
eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS, triggerDeleteRows);
|
||||||
};
|
};
|
||||||
}, [selectedRows, queryData, dataChangeStore, rows]);
|
}, [selectedRows, queryData, dataChangeStore, rows, allRowsSelect]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{
|
const triggerAddRows = (_rows, fromClipboard, pasteSerials)=>{
|
||||||
|
@ -1317,7 +1322,7 @@ export function ResultSet() {
|
||||||
selectedRowsSorted.sort();
|
selectedRowsSorted.sort();
|
||||||
insPosn = _.findIndex(rows, (r)=>rowKeyGetter(r)==selectedRowsSorted[selectedRowsSorted.length-1])+1;
|
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) {
|
if (byteaCellSelection.length>0) {
|
||||||
_rows = _rows.map(x=>{
|
_rows = _rows.map(x=>{
|
||||||
byteaCellSelection.forEach(r=>{
|
byteaCellSelection.forEach(r=>{
|
||||||
|
@ -1373,20 +1378,6 @@ export function ResultSet() {
|
||||||
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
|
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, renderGeometries);
|
||||||
}, [rows, columns, selectedRows.size, selectedColumns.size]);
|
}, [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 = () => {
|
const triggerResetScroll = () => {
|
||||||
// Reset the scroll position to previously saved location.
|
// Reset the scroll position to previously saved location.
|
||||||
if (lastScrollRef.current) {
|
if (lastScrollRef.current) {
|
||||||
|
@ -1463,17 +1454,20 @@ export function ResultSet() {
|
||||||
return (
|
return (
|
||||||
<StyledBox ref={containerRef} tabIndex="0">
|
<StyledBox ref={containerRef} tabIndex="0">
|
||||||
<Loader message={loaderText} />
|
<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 &&
|
{!queryData &&
|
||||||
<EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/>
|
<EmptyPanelMessage text={gettext('No data output. Execute a query to get output.')}/>
|
||||||
}
|
}
|
||||||
{queryData && <>
|
{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">
|
<Box flexGrow="1" minHeight="0">
|
||||||
<QueryToolDataGrid
|
<QueryToolDataGrid
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
totalRowCount={queryData?.rows_affected}
|
totalRowCount={queryData?.rows_affected}
|
||||||
|
startRowNum={queryData?.pagination?.rows_from}
|
||||||
columnWidthBy={
|
columnWidthBy={
|
||||||
queryToolCtx.preferences?.sqleditor?.column_data_auto_resize == 'by_data' ?
|
queryToolCtx.preferences?.sqleditor?.column_data_auto_resize == 'by_data' ?
|
||||||
queryToolCtx.preferences.sqleditor.column_data_max_width :
|
queryToolCtx.preferences.sqleditor.column_data_max_width :
|
||||||
|
@ -1481,7 +1475,6 @@ export function ResultSet() {
|
||||||
}
|
}
|
||||||
key={rowsResetKey}
|
key={rowsResetKey}
|
||||||
rowKeyGetter={rowKeyGetter}
|
rowKeyGetter={rowKeyGetter}
|
||||||
onScroll={handleScroll}
|
|
||||||
onRowsChange={onRowsChange}
|
onRowsChange={onRowsChange}
|
||||||
dataChangeStore={dataChangeStore}
|
dataChangeStore={dataChangeStore}
|
||||||
selectedRows={selectedRows}
|
selectedRows={selectedRows}
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
import React, {useContext, useCallback, useEffect, useState} from 'react';
|
import React, {useContext, useCallback, useEffect, useState} from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { Portal } from '@mui/material';
|
import { Box, Portal } from '@mui/material';
|
||||||
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
|
import { DefaultButton, PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
import PlaylistAddRoundedIcon from '@mui/icons-material/PlaylistAddRounded';
|
import PlaylistAddRoundedIcon from '@mui/icons-material/PlaylistAddRounded';
|
||||||
import FileCopyRoundedIcon from '@mui/icons-material/FileCopyRounded';
|
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 TimelineRoundedIcon from '@mui/icons-material/TimelineRounded';
|
||||||
import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
|
import { PasteIcon, SQLQueryIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
|
||||||
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
|
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 {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
|
||||||
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
|
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
|
||||||
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
|
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
|
||||||
|
@ -26,14 +34,28 @@ import CopyData from '../QueryToolDataGrid/CopyData';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
|
import CodeMirror from '../../../../../../static/js/components/ReactCodeMirror';
|
||||||
import { setEditorPosition } from '../QueryToolDataGrid/Editors';
|
import { setEditorPosition } from '../QueryToolDataGrid/Editors';
|
||||||
|
import { InputText } from '../../../../../../static/js/components/FormComponents';
|
||||||
|
import { minMaxValidator } from '../../../../../../static/js/validators';
|
||||||
|
|
||||||
const StyledDiv = styled('div')(({theme})=>({
|
const StyledDiv = styled('div')(({theme})=>({
|
||||||
padding: '2px',
|
padding: '2px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '4px',
|
flexWrap: 'wrap',
|
||||||
|
rowGap: '4px',
|
||||||
backgroundColor: theme.otherVars.editorToolbarBg,
|
backgroundColor: theme.otherVars.editorToolbarBg,
|
||||||
|
justifyContent: 'space-between',
|
||||||
...theme.mixins.panelBorder.bottom,
|
...theme.mixins.panelBorder.bottom,
|
||||||
|
|
||||||
|
'& .PaginationInputs': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
|
||||||
|
'& .PaginationInputs-divider': {
|
||||||
|
...theme.mixins.panelBorder.right,
|
||||||
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledEditor = styled('div')(({theme})=>({
|
const StyledEditor = styled('div')(({theme})=>({
|
||||||
|
@ -76,7 +98,134 @@ ShowDataOutputQueryPopup.propTypes = {
|
||||||
query: PropTypes.string,
|
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 eventBus = useContext(QueryToolEventsContext);
|
||||||
const queryToolCtx = useContext(QueryToolContext);
|
const queryToolCtx = useContext(QueryToolContext);
|
||||||
const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false);
|
const [dataOutputQueryBtn,setDataOutputQueryBtn] = useState(false);
|
||||||
|
@ -208,45 +357,74 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
||||||
},
|
},
|
||||||
], queryToolCtx.mainContainerRef);
|
], queryToolCtx.mainContainerRef);
|
||||||
|
|
||||||
|
const clearSelection = ()=>{
|
||||||
|
eventBus.fireEvent(QUERY_TOOL_EVENTS.CLEAR_ROWS_SELECTED);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<PgButtonGroup size="small">
|
<Box display="flex" alignItems="center" gap="4px">
|
||||||
<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 &&
|
|
||||||
<>
|
|
||||||
<PgButtonGroup size="small">
|
<PgButtonGroup size="small">
|
||||||
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />}
|
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
|
||||||
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/>
|
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>
|
</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>
|
</StyledDiv>
|
||||||
<PgMenu
|
<PgMenu
|
||||||
anchorRef={copyMenuRef}
|
anchorRef={copyMenuRef}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
||||||
const eventBus = useContext(QueryToolEventsContext);
|
const eventBus = useContext(QueryToolEventsContext);
|
||||||
const [position, setPosition] = useState([1, 1]);
|
const [position, setPosition] = useState([1, 1]);
|
||||||
const [lastTaskText, setLastTaskText] = useState(null);
|
const [lastTaskText, setLastTaskText] = useState(null);
|
||||||
const [rowsCount, setRowsCount] = useState([0, 0]);
|
const [rowsCount, setRowsCount] = useState(0);
|
||||||
const [selectedRowsCount, setSelectedRowsCount] = useState(0);
|
const [selectedRowsCount, setSelectedRowsCount] = useState(0);
|
||||||
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
|
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
|
@ -52,6 +52,8 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
||||||
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
|
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
|
||||||
const eolMenuRef = React.useRef(null);
|
const eolMenuRef = React.useRef(null);
|
||||||
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
|
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
|
||||||
|
// NONE - no select, PAGE - show select all, ALL - select all.
|
||||||
|
const [allRowsSelect, setAllRowsSelect] = useState('NONE');
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
|
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
|
||||||
|
@ -70,20 +72,30 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
||||||
pauseTimer(endTime);
|
pauseTimer(endTime);
|
||||||
setLastTaskText(taskText);
|
setLastTaskText(taskText);
|
||||||
});
|
});
|
||||||
eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{
|
eventBus.registerListener(QUERY_TOOL_EVENTS.TOTAL_ROWS_COUNT, (total)=>{
|
||||||
setRowsCount([fetched||0, total||0]);
|
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)=>{
|
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, (rows)=>{
|
||||||
setSelectedRowsCount(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({
|
setDataRowChangeCounts({
|
||||||
added: Object.keys(dataChangeStore.added||{}).length,
|
added: Object.keys(dataChangeStore.added||{}).length,
|
||||||
updated: Object.keys(dataChangeStore.updated||{}).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 = '';
|
let stagedText = '';
|
||||||
if(dataRowChangeCounts.added > 0) {
|
if(dataRowChangeCounts.added > 0) {
|
||||||
|
@ -98,7 +110,7 @@ export function StatusBar({eol, handleEndOfLineChange}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<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 &&
|
{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>
|
<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>
|
<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) &&
|
{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 &&
|
{stagedText &&
|
||||||
<Box className='StatusBar-padding StatusBar-divider'>
|
<Box className='StatusBar-padding StatusBar-divider'>
|
||||||
<span>{gettext('Changes staged: %s', stagedText)}</span>
|
<span>{gettext('Changes staged: %s', stagedText)}</span>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}>
|
<Box className='StatusBar-padding StatusBar-mlAuto' style={{display:'flex'}}>
|
||||||
<Box className="StatusBar-padding StatusBar-divider">
|
<Box className="StatusBar-padding StatusBar-divider">
|
||||||
<Tooltip title="Select EOL Sequence" disableInteractive enterDelay={2500}>
|
<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 %}
|
{% elif no_of_keys > 1 %}
|
||||||
WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN
|
WHERE ({% for each_label in primary_key_labels %}{{ conn|qtIdent(each_label) }}{% if not loop.last %}, {% endif %}{% endfor %}) IN
|
||||||
{% endif %}
|
{% 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 %}
|
({% 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 ###}
|
{### 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 %}
|
({% 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_EDITOR, PREF_LABEL_CSV_TXT, PREF_LABEL_RESULTS_GRID,\
|
||||||
PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER
|
PREF_LABEL_SQL_FORMATTING, PREF_LABEL_GRAPH_VISUALISER
|
||||||
from pgadmin.utils import SHORTCUT_FIELDS as shortcut_fields
|
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')
|
UPPER_CASE_STR = gettext('Upper case')
|
||||||
LOWER_CASE_STR = gettext('Lower 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(
|
self.data_result_rows_per_page = self.preference.register(
|
||||||
'Results_grid', 'on_demand_record_count',
|
'Results_grid', 'data_result_rows_per_page',
|
||||||
gettext("On demand record count"), 'integer', ON_DEMAND_RECORD_COUNT,
|
gettext("Data result rows per page"), 'integer',
|
||||||
min_val=1,
|
DATA_RESULT_ROWS_PER_PAGE, min_val=10,
|
||||||
category_label=PREF_LABEL_RESULTS_GRID,
|
category_label=PREF_LABEL_RESULTS_GRID,
|
||||||
help_str=gettext('Specify the number of records to fetch in one batch '
|
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'
|
||||||
'Changing this value will override '
|
' DATA_RESULT_ROWS_PER_PAGE setting from config '
|
||||||
'ON_DEMAND_RECORD_COUNT setting from config file.')
|
' file.')
|
||||||
)
|
)
|
||||||
|
|
||||||
self.sql_font_size = self.preference.register(
|
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
|
# For deleted rows
|
||||||
elif of_type == 'deleted':
|
elif of_type == 'deleted':
|
||||||
|
delete_all = changed_data.get('delete_all', False)
|
||||||
list_of_sql[of_type] = []
|
list_of_sql[of_type] = []
|
||||||
is_first = True
|
is_first = True
|
||||||
rows_to_delete = []
|
rows_to_delete = []
|
||||||
keys = None
|
keys = []
|
||||||
no_of_keys = None
|
no_of_keys = 0
|
||||||
for each_row in changed_data[of_type]:
|
if not delete_all:
|
||||||
rows_to_delete.append(changed_data[of_type][each_row])
|
for each_row in changed_data[of_type]:
|
||||||
# Fetch the keys for SQL generation
|
rows_to_delete.append(changed_data[of_type][each_row])
|
||||||
if is_first:
|
# Fetch the keys for SQL generation
|
||||||
# We need to covert dict_keys to normal list in
|
if is_first:
|
||||||
# Python3
|
# We need to covert dict_keys to normal list in
|
||||||
# In Python2, it's already a list & We will also
|
# Python3
|
||||||
# fetch column names using index
|
# In Python2, it's already a list & We will also
|
||||||
keys = list(
|
# fetch column names using index
|
||||||
changed_data[of_type][each_row].keys()
|
keys = list(
|
||||||
)
|
changed_data[of_type][each_row].keys()
|
||||||
no_of_keys = len(keys)
|
)
|
||||||
is_first = False
|
no_of_keys = len(keys)
|
||||||
# Map index with column name for each row
|
is_first = False
|
||||||
for row in rows_to_delete:
|
# Map index with column name for each row
|
||||||
for k, v in row.items():
|
for row in rows_to_delete:
|
||||||
# Set primary key with label & delete index based
|
for k, v in row.items():
|
||||||
# mapped key
|
# Set primary key with label & delete index based
|
||||||
try:
|
# mapped key
|
||||||
row[changed_data['columns']
|
try:
|
||||||
[int(k)]['name']] = v
|
row[changed_data['columns']
|
||||||
except ValueError:
|
[int(k)]['name']] = v
|
||||||
continue
|
except ValueError:
|
||||||
del row[k]
|
continue
|
||||||
|
del row[k]
|
||||||
|
|
||||||
sql = render_template(
|
sql = render_template(
|
||||||
"/".join([command_obj.sql_path, 'delete.sql']),
|
"/".join([command_obj.sql_path, 'delete.sql']),
|
||||||
|
|
|
@ -1308,6 +1308,7 @@ WHERE db.datname = current_database()""")
|
||||||
return True, {'columns': columns, 'rows': rows}
|
return True, {'columns': columns, 'rows': rows}
|
||||||
|
|
||||||
def async_fetchmany_2darray(self, records=2000,
|
def async_fetchmany_2darray(self, records=2000,
|
||||||
|
from_rownum=0, to_rownum=0,
|
||||||
formatted_exception_msg=False):
|
formatted_exception_msg=False):
|
||||||
"""
|
"""
|
||||||
User should poll and check if status is ASYNC_OK before calling this
|
User should poll and check if status is ASYNC_OK before calling this
|
||||||
|
@ -1342,6 +1343,10 @@ WHERE db.datname = current_database()""")
|
||||||
try:
|
try:
|
||||||
if records == -1:
|
if records == -1:
|
||||||
result = cur.fetchall(_tupples=True)
|
result = cur.fetchall(_tupples=True)
|
||||||
|
elif records is None:
|
||||||
|
result = cur.fetchwindow(from_rownum=from_rownum,
|
||||||
|
to_rownum=to_rownum,
|
||||||
|
_tupples=True)
|
||||||
else:
|
else:
|
||||||
result = cur.fetchmany(records, _tupples=True)
|
result = cur.fetchmany(records, _tupples=True)
|
||||||
except psycopg.ProgrammingError:
|
except psycopg.ProgrammingError:
|
||||||
|
@ -1538,6 +1543,12 @@ Failed to reset the connection to the server due to following error:
|
||||||
|
|
||||||
return self.row_count
|
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):
|
def get_column_info(self):
|
||||||
"""
|
"""
|
||||||
This function will returns list of columns for last async sql command
|
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
|
self.row_factory = dict_row
|
||||||
return res
|
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):
|
async def _scrollcur(self, position, mode):
|
||||||
"""
|
"""
|
||||||
Fetch all tuples as ordered dictionary list.
|
Fetch all tuples as ordered dictionary list.
|
||||||
|
|
|
@ -47,10 +47,10 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
|
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
self._reset_options()
|
self._reset_options()
|
||||||
# on demand result set on scrolling.
|
# pagination result on page change.
|
||||||
print("\nOn demand query result... ",
|
print("\nPagination query result... ",
|
||||||
file=sys.stderr, end="")
|
file=sys.stderr, end="")
|
||||||
self._on_demand_result()
|
self._pagination_result()
|
||||||
self.page.clear_query_tool()
|
self.page.clear_query_tool()
|
||||||
|
|
||||||
# explain query with verbose and cost
|
# explain query with verbose and cost
|
||||||
|
@ -129,18 +129,12 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||||
# close menu
|
# close menu
|
||||||
query_op.click()
|
query_op.click()
|
||||||
|
|
||||||
def _on_demand_result(self):
|
def _pagination_result(self):
|
||||||
ON_DEMAND_CHUNKS = 2
|
query = """-- Pagination result
|
||||||
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
|
|
||||||
SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
|
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... ",
|
print("\nPagination result... ", file=sys.stderr, end="")
|
||||||
file=sys.stderr, end="")
|
|
||||||
self.page.execute_query(query)
|
self.page.execute_query(query)
|
||||||
|
|
||||||
# wait for header of the table to be visible
|
# 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,
|
(By.CSS_SELECTOR,
|
||||||
QueryToolLocators.query_output_cells)))
|
QueryToolLocators.query_output_cells)))
|
||||||
|
|
||||||
self.page.find_by_css_selector(
|
for i, page in enumerate([
|
||||||
QueryToolLocators.query_output_canvas_css)
|
{'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)
|
self.assertEqual(page_info.text, f"Showing: {page['page_info']}")
|
||||||
print("OK.", file=sys.stderr)
|
|
||||||
|
|
||||||
print("On demand result set on grid select all... ",
|
page_info = self.page.find_by_css_selector(
|
||||||
file=sys.stderr, end="")
|
QueryToolLocators.pagination_inputs + ' span:nth-of-type(3)')
|
||||||
self.page.click_execute_query_button()
|
|
||||||
|
|
||||||
# wait for header of the table to be visible
|
self.assertEqual(page_info.text, "of 3")
|
||||||
self.page.find_by_css_selector(
|
|
||||||
QueryToolLocators.query_output_canvas_css)
|
|
||||||
|
|
||||||
# wait for the rows in the table to be displayed
|
cell_rownum = self.page.find_by_css_selector(
|
||||||
self.wait.until(EC.presence_of_element_located(
|
QueryToolLocators.query_output_cells + ':nth-of-type(1)')
|
||||||
(By.CSS_SELECTOR,
|
|
||||||
QueryToolLocators.query_output_cells))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Select all rows in a table
|
self.assertEqual(cell_rownum.text, page['cell_rownum'])
|
||||||
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._check_ondemand_result(row_id_to_find)
|
if i < 2:
|
||||||
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 = \
|
|
||||||
self.page.find_by_css_selector(
|
self.page.find_by_css_selector(
|
||||||
QueryToolLocators.output_column_header_css.format('id1'))
|
QueryToolLocators.pagination_inputs +
|
||||||
column_1.click()
|
' button[aria-label="Next Page"]').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
|
|
||||||
|
|
||||||
self.assertTrue(
|
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||||
status, "Element is not loaded to the rows id: "
|
|
||||||
"{}".format(row_id_to_find))
|
|
||||||
|
|
||||||
def _query_tool_explain_with_verbose_and_cost(self):
|
def _query_tool_explain_with_verbose_and_cost(self):
|
||||||
query = """-- Explain query with verbose and cost
|
query = """-- Explain query with verbose and cost
|
||||||
|
|
|
@ -172,8 +172,6 @@ CREATE TABLE public.nonintpkey
|
||||||
self._copy_paste_row(config_data_local)
|
self._copy_paste_row(config_data_local)
|
||||||
|
|
||||||
self._update_row(config_data_local)
|
self._update_row(config_data_local)
|
||||||
self.page.click_tab("Messages")
|
|
||||||
self._verify_messsages("")
|
|
||||||
self.page.click_tab("Data Output")
|
self.page.click_tab("Data Output")
|
||||||
updated_row_data = {
|
updated_row_data = {
|
||||||
i: config_data_local['update'][i] if i in config_data_local[
|
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"
|
query_output_canvas_css = "#id-dataoutput .rdg"
|
||||||
|
|
||||||
|
pagination_inputs = "#id-dataoutput .PaginationInputs"
|
||||||
|
|
||||||
query_output_cells = ".rdg-cell[role='gridcell']"
|
query_output_cells = ".rdg-cell[role='gridcell']"
|
||||||
|
|
||||||
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"
|
sql_editor_message = "//div[@id='id-messages'][contains(string(), '{}')]"
|
||||||
|
|
Loading…
Reference in New Issue