1) Port query tool to React. Fixes #6131

2) Added status bar to the Query Tool. Fixes #3253
3) Ensure that row numbers should be visible in view when scrolling horizontally. Fixes #3989
4) Allow removing a single query history. Refs #4113
5) Partially fixed Macros usability issues. Ref #6969
6) Fixed an issue where the Query tool opens on minimum size if the user opens multiple query tool Window quickly. Fixes #6725
7) Relocate GIS Viewer Button to the Left Side of the Results Table. Fixes #6830
8) Fixed an issue where the connection bar is not visible. Fixes #7188
9) Fixed an issue where an Empty message popup after running a query. Fixes #7260
10) Ensure that Autocomplete should work after changing the connection. Fixes #7262
11) Fixed an issue where the copy and paste row does not work if the first column contains no data. Fixes #7294
This commit is contained in:
Aditya Toshniwal
2022-04-07 17:36:56 +05:30
committed by Akshay Joshi
parent bf8e569bde
commit b5b9ee46a1
213 changed files with 11134 additions and 18830 deletions

View File

@@ -1,401 +0,0 @@
#main-editor_panel {
position: absolute;
left: 0;
right: 0;
top : 0;
bottom: 0;
}
.sql-editor {
position: absolute;
left: 0;
right: 0;
top : 0;
bottom: 0;
}
.filter-container .CodeMirror-scroll {
min-height: 120px;
}
.filter-container .sql-textarea{
box-shadow: 0.1px 0.1px 3px #000;
margin-bottom: 5px;
}
#filter .btn-group {
margin-right: 2px;
float: right;
}
#filter .btn-group > button {
padding: 3px;
}
#filter .btn-group .btn-primary {
margin: auto !important;
}
.has-select-all table thead tr th:nth-child(1),
.has-select-all table tbody tr td:nth-child(1) {
width: 35px !important;
max-width: 35px !important;
min-width: 35px !important;
}
.sql-status-cell {
max-width: 30px;
}
.btn-circle {
width: 16px;
height: 16px;
text-align: center;
padding: 0;
font-size: 10px;
line-height: 1.428571429;
border-radius: 10px;
cursor: auto;
}
.visibility-hidden {
visibility: hidden;
}
.sql-editor-mark {
border-bottom: 2px dotted red;
}
.CodeMirror {
min-height: 100%;
height: 100%;
}
#output-panel {
height: 100% !important;
}
.sql-editor-explain {
height: 100%;
width: 100%;
overflow: auto;
}
.sqleditor-hint {
padding-left: 20px;
}
.CodeMirror-hint .fa::before {
padding-right: 7px;
}
h2 {
font-size: 10pt;
border-bottom: 1px dotted gray;
}
ul {
margin-left: 0;
padding: 0;
cursor: default;
}
li {
padding: 0 0 0 0px;
list-style: none;
margin: 0;
}
#datagrid {
background: white;
outline: 0;
font-size: 9pt;
}
#datagrid .slick-header-column.ui-state-default {
height: 32px !important;
}
#datagrid .grid-header label {
display: inline-block;
font-weight: bold;
margin: auto auto auto 6px;
}
.grid-header .ui-icon {
margin: 4px 4px auto 6px;
background-color: transparent;
border-color: transparent;
}
.slick-row .cell-actions {
text-align: left;
}
/* Slick.Editors.Text, Slick.Editors.Date */
#datagrid .slick-header > input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
#datagrid .slick-header > input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
.slick-row.selected .cell-selection {
background-color: transparent; /* show default selected row background */
}
#datagrid .slick-header .ui-state-default,
#datagrid .slick-header .ui-widget-content.ui-state-default,
#datagrid .slick-header .ui-widget-header .ui-state-default {
background: none;
}
#datagrid .slick-header .slick-header-column .column-name {
font-weight: bold;
display: block;
}
.column-description {
display: table-cell;
}
.long_text_editor {
margin-left: 5px;
font-size: 12px !important;
padding: 1px 7px;
}
/* Slick.Editors.Text, Slick.Editors.Date */
input.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Text, Slick.Editors.Date */
textarea.editor-text {
width: 100%;
height: 100%;
border: 0;
margin: 0;
background: transparent;
outline: 0;
padding: 0;
}
/* Slick.Editors.Checkbox */
input.editor-checkbox {
margin: 0;
height: 100%;
padding: 0;
border: 0;
}
/* remove outlined border on focus */
input.editor-checkbox:focus {
outline: none;
}
.slick-cell span[data-cell-type="row-header-selector"] {
display: block;
text-align: center;
}
/*
SlickGrid, To fix the issue of width misalignment between Column Header &
actual Column in Mozilla Firefox browser
Ref: https://github.com/mleibman/SlickGrid/issues/742
*/
.slickgrid, .slickgrid *, .slick-header-column {
box-sizing: content-box;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
-ms-box-sizing: content-box;
}
.select-all-icon {
margin-left: 9px;
margin-right: 4px;
vertical-align: bottom;
position: absolute;
bottom: 4px;
right: 0;
}
/* Style for text editor */
.pg_buttons {
text-align:right;
}
#datagrid .slick-row .slick-cell {
white-space: pre;
}
.connection_status {
font-size: 1rem;
width: 40px;
}
.ajs-body .warn-header {
font-size: 13px;
font-weight: bold;
line-height: 3em;
}
.ajs-body .warn-body {
font-size: 13px;
}
.ajs-body .warn-footer {
font-size: 13px;
line-height: 3em;
}
/* For Filter status bar */
.data_sorting_dialog .pg-prop-status-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 5;
}
.data_sorting_dialog .CodeMirror-gutter-wrapper {
left: -30px !important;
}
.data_sorting_dialog .CodeMirror-gutters {
left: 0px !important;
}
.data_sorting_dialog .custom_height_css_class {
height: 100px;
}
.data_sorting_dialog .data_sorting {
padding: 10px 0px;
}
.connection-status-hide {
display: none !important;
}
/* For geometry data viewer panel */
.sql-editor-geometry-viewer{
width: 100%;
height: 100%;
}
.geometry-viewer-container {
width: 100%;
height: 100%;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAANElEQVQoU2O8e/fuf2VlZUYGLAAkB5bApggmBteJrAiZjWI0SAJkIrKVxCvAawVeRxLyJgB+Ajc1cwux9wAAAABJRU5ErkJggg==);
/* Let's keep the background as fff irrespective of theme
* make geometry viewer look clean
*/
background-color: #fff;
}
/* For geometry column button */
.div-view-geometry-column, .editable-column-header-icon {
float: right;
height: 100%;
display: flex;
display: -webkit-flex;
align-items: center;
padding-right: 6px;
}
/* For leaflet popup */
.leaflet-popup-content-wrapper {
border-radius: 2px;
}
.leaflet-popup-content {
margin: 5px;
padding: 10px 10px 0;
overflow-y: scroll;
overflow-x: hidden;
}
/* For geometry viewer property table */
.view-geometry-property-table {
table-layout: fixed;
white-space: nowrap;
padding: 0;
}
.view-geometry-property-table th {
overflow: hidden;
text-overflow: ellipsis;
}
.view-geometry-property-table td {
overflow: hidden;
text-overflow: ellipsis;
}
/* For geometry viewer info control */
.geometry-viewer-info-control {
padding: 5px;
background: white;
border: 2px solid rgba(0, 0, 0, 0.2);
background-clip: padding-box;
border-radius: 2px;
}
.geometry-viewer-info-control i{
margin: 0 0 0 4px;
}
.hide-vertical-scrollbar {
overflow-y: hidden;
}
/* Macros */
.macro-tab {
top: 0px !important;
}
.macro-tab .tab-pane {
padding: 0px !important;
}
.macro_dialog .CodeMirror {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .sql-cell > div {
overflow-y: auto;
resize: vertical;
}
.macro_dialog .CodeMirror-cursor {
width: 1px !important;
height: 18px !important;
}
.macro_dialog .pg-prop-status-bar {
z-index: 1;
}
.new-connection-dialog-style {
width: 100% !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,424 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import * as showViewData from './show_view_data';
import * as showQueryTool from './show_query_tool';
import * as toolBar from 'pgadmin.browser.toolbar';
import * as panelTitleFunc from './sqleditor_title';
import * as commonUtils from 'sources/utils';
import $ from 'jquery';
import url_for from 'sources/url_for';
import _ from 'lodash';
import alertify from 'pgadmin.alertifyjs';
var wcDocker = window.wcDocker;
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser';
import 'pgadmin.file_manager';
import 'pgadmin.tools.user_management';
import gettext from 'sources/gettext';
import React from 'react';
import ReactDOM from 'react-dom';
import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
export function setPanelTitle(queryToolPanel, panelTitle) {
queryToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
}
export default class SQLEditor {
static instance;
static getInstance(...args) {
if(!SQLEditor.instance) {
SQLEditor.instance = new SQLEditor(...args);
}
return SQLEditor.instance;
}
SUPPORTED_NODES = [
'table', 'view', 'mview',
'foreign_table', 'catalog_object', 'partition',
];
/* Enable/disable View data menu in tools based
* on node selected. if selected node is present
* in supportedNodes, menu will be enabled
* otherwise disabled.
*/
viewMenuEnabled(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj))
return (_.indexOf(this.SUPPORTED_NODES, obj._type) !== -1 ? true : false);
else
return false;
})();
toolBar.enable(gettext('View Data'), isEnabled);
toolBar.enable(gettext('Filtered Rows'), isEnabled);
return isEnabled;
}
/* Enable/disable Query tool menu in tools based
* on node selected. if selected node is present
* in unsupported_nodes, menu will be disabled
* otherwise enabled.
*/
queryToolMenuEnabled(obj) {
var isEnabled = (() => {
if (!_.isUndefined(obj) && !_.isNull(obj)) {
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
toolBar.enable(gettext('Query Tool'), isEnabled);
return isEnabled;
}
init() {
if(this.initialized)
return;
this.initialized = true;
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('sqleditor', function() {
self.preferences = pgBrowser.get_preferences_for_module('sqleditor');
});
// Define the nodes on which the menus to be appear
var menus = [{
name: 'query_tool',
module: this,
applies: ['tools'],
callback: 'showQueryTool',
enable: self.queryToolMenuEnabled,
priority: 1,
label: gettext('Query Tool'),
icon: 'pg-font-icon icon-query_tool',
data:{
applies: 'tools',
data_disabled: gettext('Please select a database from the browser tree to access Query Tool.'),
},
}];
// Create context menu
for (const supportedNode of self.SUPPORTED_NODES) {
menus.push({
name: 'view_all_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 3,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 101,
label: gettext('All Rows'),
}, {
name: 'view_first_100_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 1,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 102,
label: gettext('First 100 Rows'),
}, {
name: 'view_last_100_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 2,
},
applies: ['context', 'object'],
callback: 'showViewData',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 103,
label: gettext('Last 100 Rows'),
}, {
name: 'view_filtered_rows_context_' + supportedNode,
node: supportedNode,
module: this,
data: {
mnuid: 4,
},
applies: ['context', 'object'],
callback: 'showFilteredRow',
enable: self.viewMenuEnabled,
category: 'view_data',
priority: 104,
label: gettext('Filtered Rows...'),
});
}
pgBrowser.add_menu_category('view_data', gettext('View/Edit Data'), 100, '');
pgBrowser.add_menus(menus);
// Creating a new pgAdmin.Browser frame to show the data.
var frame = new pgAdmin.Browser.Frame({
name: 'frm_sqleditor',
showTitle: true,
isCloseable: true,
isRenamable: true,
isPrivate: true,
url: 'about:blank',
});
// Load the newly created frame
frame.load(pgBrowser.docker);
}
// This is a callback function to show data when user click on menu item.
showViewData(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showViewData.showViewData(this, pgBrowser, alertify, data, i, transId);
}
// This is a callback function to show filtered data when user click on menu item.
showFilteredRow(data, i) {
const transId = commonUtils.getRandomInt(1, 9999999);
showViewData.showViewData(this, pgBrowser, alertify, data, i, transId, true, this.preferences);
}
// This is a callback function to show query tool when user click on menu item.
showQueryTool(url, treeIdentifier) {
const transId = commonUtils.getRandomInt(1, 9999999);
var t = pgBrowser.tree,
i = treeIdentifier || t.selected(),
d = i ? t.itemData(i) : undefined;
//Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
var preference = pgBrowser.get_preference('sqleditor', 'copy_sql_to_query_tool');
if(preference.value && !d._type.includes('coll-') && (url === '' || url['applies'] === 'tools')){
var stype = d._type.toLowerCase();
var data = {
'script': stype,
data_disabled: gettext('The selected tree node does not support this option.'),
};
pgBrowser.Node.callbacks.show_script(data);
} else {
if(d._type.includes('coll-')){
url = '';
}
showQueryTool.showQueryTool(this, pgBrowser, url, treeIdentifier, transId);
}
}
onPanelRename(queryToolPanel, panelData, is_query_tool) {
var temp_title = panelData.$titleText[0].textContent;
var is_dirty_editor = queryToolPanel.is_dirty_editor ? queryToolPanel.is_dirty_editor : false;
var title = queryToolPanel.is_dirty_editor ? panelData.$titleText[0].textContent.replace(/.$/, '') : temp_title;
alertify.prompt('', title,
// We will execute this function when user clicks on the OK button
function(evt, value) {
// Remove the leading and trailing white spaces.
value = value.trim();
if(value) {
var is_file = false;
if(panelData.$titleText[0].innerHTML.includes('File - ')) {
is_file = true;
}
var selected_item = pgBrowser.tree.selected();
var panel_titles = '';
if(is_query_tool) {
panel_titles = panelTitleFunc.getPanelTitle(pgBrowser, selected_item, value);
} else {
panel_titles = showViewData.generateDatagridTitle(pgBrowser, selected_item, value);
}
// Set title to the selected tab.
if (is_dirty_editor) {
panel_titles = panel_titles + ' *';
}
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_titles), is_file);
}
},
// We will execute this function when user clicks on the Cancel
// button. Do nothing just close it.
function(evt) { evt.cancel = false; }
).set({'title': gettext('Rename Panel')});
}
openQueryToolPanel(trans_id, is_query_tool, panel_title, closeUrl, queryToolForm) {
let self = this;
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var propertiesPanel = pgBrowser.docker.findPanels('properties');
var queryToolPanel = pgBrowser.docker.addPanel('frm_sqleditor', wcDocker.DOCK.STACKED, propertiesPanel[0]);
queryToolPanel.trans_id = trans_id;
showQueryTool._set_dynamic_tab(pgBrowser, browser_preferences['dynamic_tabs']);
// Set panel title and icon
panelTitleFunc.setQueryToolDockerTitle(queryToolPanel, is_query_tool, _.unescape(panel_title));
queryToolPanel.focus();
// Listen on the panel closed event.
if (queryToolPanel.isVisible()) {
queryToolPanel.on(wcDocker.EVENT.CLOSED, function() {
$.ajax({
url: closeUrl,
method: 'DELETE',
});
});
}
queryToolPanel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function() {
queryToolPanel.trigger(wcDocker.EVENT.RESIZED);
});
commonUtils.registerDetachEvent(queryToolPanel);
// Listen on the panelRename event.
queryToolPanel.on(wcDocker.EVENT.RENAME, function(panelData) {
self.onPanelRename(queryToolPanel, panelData, is_query_tool);
});
var openQueryToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).appendTo($(j).data('embeddedFrame').$container);
let init_poller_id = setInterval(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
clearInterval(init_poller_id);
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.onLoaded(()=>{
$spinner_el.remove();
});
frame.openHTML(queryToolForm);
}
}
}, 100);
};
openQueryToolURL(queryToolPanel);
}
launch(trans_id, panel_url, is_query_tool, panel_title, sURL=null, sql_filter=null) {
const self = this;
let closeUrl = url_for('sqleditor.close', {'trans_id': trans_id});
let queryToolForm = `
<form id="queryToolForm" action="${panel_url}" method="post">
<input id="title" name="title" hidden />
<input id="conn_title" name="conn_title" hidden />
<input name="close_url" value="${closeUrl}" hidden />`;
if(sURL){
queryToolForm +=`<input name="query_url" value="${sURL}" hidden />`;
}
if(sql_filter) {
queryToolForm +=`<textarea name="sql_filter" hidden>${sql_filter}</textarea>`;
}
/* Escape backslashes as it is stripped by back end */
queryToolForm +=`
</form>
<script>
document.getElementById("title").value = "${_.escape(panel_title.replace('\\', '\\\\'))}";
document.getElementById("queryToolForm").submit();
</script>
`;
var browser_preferences = pgBrowser.get_preferences_for_module('browser');
var open_new_tab = browser_preferences.new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('qt')) {
var newWin = window.open('', '_blank');
if(newWin) {
newWin.document.write(queryToolForm);
newWin.document.title = panel_title;
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
return false;
}
} else {
/* On successfully initialization find the dashboard panel,
* create new panel and add it to the dashboard panel.
*/
self.openQueryToolPanel(trans_id, is_query_tool, panel_title, closeUrl, queryToolForm);
}
return true;
}
setupPreferencesWorker() {
if (window.location == window.parent?.location) {
/* Sync the local preferences with the main window if in new tab */
setInterval(()=>{
if(pgWindow?.pgAdmin) {
if(pgAdmin.Browser.preference_version() < pgWindow.pgAdmin.Browser.preference_version()){
pgAdmin.Browser.preferences_cache = pgWindow.pgAdmin.Browser.preferences_cache;
pgAdmin.Browser.preference_version(pgWindow.pgAdmin.Browser.preference_version());
pgAdmin.Browser.triggerPreferencesChange('browser');
pgAdmin.Browser.triggerPreferencesChange('sqleditor');
}
}
}, 1000);
}
}
loadComponent(container, params) {
let panel = null;
let selectedNodeInfo = pgWindow.pgAdmin.Browser.tree.getTreeNodeHierarchy(
pgWindow.pgAdmin.Browser.tree.selected()
);
_.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_sqleditor'), function(p) {
if (p.trans_id == params.trans_id) {
panel = p;
}
});
this.setupPreferencesWorker();
ReactDOM.render(
<ModalProvider>
<QueryToolComponent params={params} pgWindow={pgWindow} pgAdmin={pgAdmin} panel={panel} selectedNodeInfo={selectedNodeInfo}/>
</ModalProvider>,
container
);
}
}

View File

@@ -0,0 +1,646 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useCallback, useRef, useMemo, useState, useEffect} from 'react';
import _ from 'lodash';
import Layout, { LayoutHelper } from '../../../../../static/js/helpers/Layout';
import EventBus from '../../../../../static/js/helpers/EventBus';
import Query from './sections/Query';
import { ConnectionBar } from './sections/ConnectionBar';
import { ResultSet } from './sections/ResultSet';
import { StatusBar } from './sections/StatusBar';
import { MainToolBar } from './sections/MainToolBar';
import { Messages } from './sections/Messages';
import Theme from 'sources/Theme';
import getApiInstance, {parseApiError} from '../../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import { PANELS, QUERY_TOOL_EVENTS, CONNECTION_STATUS } from './QueryToolConstants';
import { useInterval } from '../../../../../static/js/custom_hooks';
import { Box } from '@material-ui/core';
import { getDatabaseLabel, getTitle, setQueryToolDockerTitle } from '../sqleditor_title';
import gettext from 'sources/gettext';
import NewConnectionDialog from './dialogs/NewConnectionDialog';
import { evalFunc } from '../../../../../static/js/utils';
import { Notifications } from './sections/Notifications';
import MacrosDialog from './dialogs/MacrosDialog';
import Notifier from '../../../../../static/js/helpers/Notifier';
import FilterDialog from './dialogs/FilterDialog';
import { QueryHistory } from './sections/QueryHistory';
import * as showQueryTool from '../show_query_tool';
import * as commonUtils from 'sources/utils';
import * as Kerberos from 'pgadmin.authenticate.kerberos';
import PropTypes from 'prop-types';
import { retrieveNodeName } from '../show_view_data';
import 'wcdocker';
import { useModal } from '../../../../../static/js/helpers/ModalProvider';
export const QueryToolContext = React.createContext();
export const QueryToolConnectionContext = React.createContext();
export const QueryToolEventsContext = React.createContext();
function fetchConnectionStatus(api, transId) {
return api.get(url_for('sqleditor.connection_status', {trans_id: transId}));
}
function initConnection(api, params, passdata) {
return api.post(url_for('NODE-server.connect_id', params), passdata);
}
function setPanelTitle(panel, title, qtState, dirty=false) {
if(title) {
title =title.split('\\').pop().split('/').pop();
} else if(qtState.current_file) {
title = qtState.current_file.split('\\').pop().split('/').pop();
} else {
title = qtState.params.title || 'Untitled';
}
title = title + (dirty ? '*': '');
if (qtState.is_new_tab) {
window.document.title = title;
} else {
setQueryToolDockerTitle(panel, true, title, qtState.current_file ? true : false);
}
}
export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedNodeInfo, panel, eventBusObj}) {
const containerRef = React.useRef(null);
const forceClose = React.useRef(false);
const [qtState, _setQtState] = useState({
preferences: {
browser: {}, sqleditor: {},
},
is_new_tab: window.location == window.parent?.location,
current_file: null,
obtaining_conn: true,
connected: false,
connection_status: null,
connection_status_msg: '',
params: {
...params,
is_query_tool: params.is_query_tool == 'true' ? true : false,
node_name: retrieveNodeName(selectedNodeInfo),
},
connection_list: [{
sgid: params.sgid,
sid: params.sid,
did: params.did,
user: params.username,
role: null,
title: _.unescape(params.title),
conn_title: getTitle(
pgAdmin, null, selectedNodeInfo, true, params.server_name, params.database_name || getDatabaseLabel(selectedNodeInfo),
params.username, params.is_query_tool == 'true' ? true : false),
server_name: params.server_name,
database_name: params.database_name || getDatabaseLabel(selectedNodeInfo),
is_selected: true,
}],
});
const setQtState = (state)=>{
_setQtState((prev)=>({...prev,...evalFunc(null, state, prev)}));
};
const eventBus = useRef(eventBusObj || (new EventBus()));
const docker = useRef(null);
const api = useMemo(()=>getApiInstance(), []);
const modal = useModal();
/* Connection status poller */
let pollTime = qtState.preferences.sqleditor.connection_status_fetch_time > 0 ?
qtState.preferences.sqleditor.connection_status_fetch_time*1000 : -1;
/* No need to poll when the query is executing. Query poller will the txn status */
if(qtState.connection_status === CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE && qtState.connected) {
pollTime = -1;
}
useInterval(async ()=>{
try {
let {data: respData} = await fetchConnectionStatus(api, qtState.params.trans_id);
if(respData.data) {
setQtState({
connected: true,
connection_status: respData.data.status,
});
} else {
setQtState({
connected: false,
connection_status: null,
connection_status_msg: gettext('An unexpected error occurred - ensure you are logged into the application.')
});
}
if(respData.data.notifies) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.PUSH_NOTICE, respData.data.notifies);
}
} catch (error) {
console.error(error);
setQtState({
connected: false,
connection_status: null,
connection_status_msg: parseApiError(error),
});
}
}, pollTime);
let defaultLayout = {
dockbox: {
mode: 'vertical',
children: [
{
mode: 'horizontal',
children: [
{
tabs: [
LayoutHelper.getPanel({id: PANELS.QUERY, title: gettext('Query'), content: <Query />}),
LayoutHelper.getPanel({id: PANELS.HISTORY, title: 'Query History', content: <QueryHistory />,
cached: undefined}),
],
},
{
size: 75,
tabs: [
LayoutHelper.getPanel({
id: PANELS.SCRATCH, title: gettext('Scratch Pad'),
closable: true,
content: <textarea style={{
border: 0,
height: '100%',
width: '100%',
resize: 'none'
}}/>
}),
]
}
]
},
{
mode: 'horizontal',
children: [
{
tabs: [
LayoutHelper.getPanel({
id: PANELS.DATA_OUTPUT, title: 'Data output', content: <ResultSet />,
}),
LayoutHelper.getPanel({
id: PANELS.MESSAGES, title:'Messages', content: <Messages />,
}),
LayoutHelper.getPanel({
id: PANELS.NOTIFICATIONS, title:'Notifications', content: <Notifications />,
}),
],
}
]
},
]
},
};
const reflectPreferences = useCallback(()=>{
setQtState({preferences: {
browser: pgWindow.pgAdmin.Browser.get_preferences_for_module('browser'),
sqleditor: pgWindow.pgAdmin.Browser.get_preferences_for_module('sqleditor'),
}});
}, []);
const getSQLScript = ()=>{
// Fetch the SQL for Scripts (eg: CREATE/UPDATE/DELETE/SELECT)
// Call AJAX only if script type url is present
if(qtState.params.is_query_tool && qtState.params.query_url) {
api.get(qtState.params.query_url)
.then((res)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, res.data);
})
.catch((err)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
} else if(qtState.params.sql_id) {
let sqlValue = localStorage.getItem(qtState.params.sql_id);
localStorage.removeItem(qtState.params.sql_id);
if(sqlValue) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, sqlValue);
}
}
};
const initializeQueryTool = ()=>{
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
let baseUrl = '';
if(qtState.params.is_query_tool) {
let endpoint = 'sqleditor.initialize_sqleditor';
if(qtState.params.did) {
endpoint = 'sqleditor.initialize_sqleditor_with_did';
}
baseUrl = url_for(endpoint, {
...selectedConn,
trans_id: qtState.params.trans_id,
});
} else {
baseUrl = url_for('sqleditor.initialize_viewdata', {
...qtState.params,
});
}
api.post(baseUrl, qtState.params.is_query_tool ? null : JSON.stringify(qtState.params.sql_filter))
.then(()=>{
setQtState({
connected: true,
obtaining_conn: false,
});
if(!qtState.params.is_query_tool) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}
}).catch((err)=>{
if(err.response?.request?.responseText?.search('Ticket expired') !== -1) {
Kerberos.fetch_ticket()
.then(()=>{
initializeQueryTool();
})
.catch((kberr)=>{
setQtState({
connected: false,
obtaining_conn: false,
});
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, kberr);
});
}
setQtState({
connected: false,
obtaining_conn: false,
});
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
};
useEffect(()=>{
getSQLScript();
initializeQueryTool();
eventBus.current.registerListener(QUERY_TOOL_EVENTS.FOCUS_PANEL, (panelId)=>{
LayoutHelper.focus(docker.current, panelId);
});
eventBus.current.registerListener(QUERY_TOOL_EVENTS.SET_CONNECTION_STATUS, (status)=>{
setQtState({connection_status: status});
});
eventBus.current.registerListener(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL, ()=>{
panel.off(window.wcDocker.EVENT.CLOSING);
panel.close();
});
reflectPreferences();
pgWindow.pgAdmin.Browser.onPreferencesChange('sqleditor', function() {
reflectPreferences();
});
/* WC docker events */
panel?.on(window.wcDocker.EVENT.CLOSING, function() {
if(!forceClose.current) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.WARN_SAVE_DATA_CLOSE);
} else {
panel.close();
}
});
pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:select_file', (fileName)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName);
}, pgAdmin);
pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:create_file', (fileName)=>{
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, fileName);
}, pgAdmin);
}, []);
useEffect(()=>{
const pushHistory = (h)=>{
api.post(
url_for('sqleditor.add_query_history', {
'trans_id': qtState.params.trans_id,
}),
JSON.stringify(h),
).catch((error)=>{console.error(error);});
};
eventBus.current.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
return ()=>{eventBus.current.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);};
}, [qtState.params.trans_id]);
const handleApiError = (error, handleParams)=>{
if(error.response && pgAdmin.Browser?.UserManagement?.isPgaLoginRequired(error.response)) {
return pgAdmin.Browser.UserManagement.pgaLogin();
}
if(error.response?.status == 503 && error.response.data?.info == 'CONNECTION_LOST') {
// We will display re-connect dialog, no need to display error message again
modal.confirm(
gettext('Connection Warning'),
<p>
<span>{gettext('The application has lost the database connection:')}</span>
<br/><span>{gettext(' If the connection was idle it may have been forcibly disconnected.')}</span>
<br/><span>{gettext(' The application server or database server may have been restarted.')}</span>
<br/><span>{gettext(' The user session may have timed out.')}</span>
<br />
<span>{gettext('Do you want to continue and establish a new session')}</span>
</p>,
function() {
handleParams?.connectionLostCallback?.();
}, null,
gettext('Continue'),
gettext('Cancel')
);
} else if(handleParams?.checkTransaction && error.response?.data.info == 'DATAGRID_TRANSACTION_REQUIRED') {
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
initConnection(api, {
'gid': selectedConn.sgid,
'sid': selectedConn.sid,
'did': selectedConn.did,
'role': selectedConn.role,
}).then(()=>{
initializeQueryTool();
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
} else {
let msg = parseApiError(error);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, msg, true);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.MESSAGES);
Notifier.error(msg);
}
};
useEffect(()=>{
const fileDone = (fileName, success=true)=>{
if(success) {
setQtState({
current_file: fileName,
});
setPanelTitle(panel, fileName, {...qtState, current_file: fileName});
}
};
const events = [
[QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE, ()=>{
let fileParams = {
'supported_types': ['*', 'sql'], // file types allowed
'dialog_type': 'select_file', // open select file dialog
};
pgAdmin.FileManager.init();
pgAdmin.FileManager.show_dialog(fileParams);
}],
[QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{
if(!isSaveAs && qtState.current_file) {
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE, qtState.current_file);
} else {
let fileParams = {
'supported_types': ['*', 'sql'],
'dialog_type': 'create_file',
'dialog_title': 'Save File',
'btn_primary': 'Save',
};
pgAdmin.FileManager.init();
pgAdmin.FileManager.show_dialog(fileParams);
}
}],
[QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileDone],
[QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileDone],
[QUERY_TOOL_EVENTS.QUERY_CHANGED, (isDirty)=>{
if(qtState.params.is_query_tool) {
setPanelTitle(panel, null, qtState, isDirty);
}
}],
[QUERY_TOOL_EVENTS.HANDLE_API_ERROR, handleApiError],
];
events.forEach((e)=>{
eventBus.current.registerListener(e[0], e[1]);
});
return ()=>{
events.forEach((e)=>{
eventBus.current.deregisterListener(e[0], e[1]);
});
};
}, [qtState]);
useEffect(()=>{
/* Fire query change so that title changes to latest */
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE);
}, [qtState.params.title]);
const updateQueryToolConnection = useCallback((connectionData, isNew=false)=>{
setQtState((prev)=>{
let newConnList = [...prev.connection_list];
if(isNew) {
newConnList.push(connectionData);
}
for (const connItem of newConnList) {
if(connectionData.sid == connItem.sid
&& connectionData.did == connItem.did
&& connectionData.user == connItem.user
&& connectionData.role == connItem.role) {
connItem.is_selected = true;
} else {
connItem.is_selected = false;
}
}
return {
connection_list: newConnList,
};
});
setQtState((prev)=>{
return {
params: {
...prev.params,
sid: connectionData.sid,
did: connectionData.did,
title: connectionData.title,
},
obtaining_conn: true,
connected: false,
};
});
return api.post(url_for('sqleditor.update_sqleditor_connection', {
trans_id: qtState.params.trans_id,
sgid: connectionData.sgid,
sid: connectionData.sid,
did: connectionData.did
}), connectionData)
.then(({data: respData})=>{
setQtState((prev)=>{
return {
params: {
...prev.params,
trans_id: respData.data.trans_id,
},
connected: respData.data.trans_id ? true : false,
obtaining_conn: false,
};
});
let msg = `${connectionData['server_name']}/${connectionData['database_name']} - Database connected`;
Notifier.success(msg);
});
}, [qtState.params.trans_id]);
const onNewConnClick = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'new-conn');
LayoutHelper.openDialog(docker.current, {
id: 'new-conn',
title: gettext('Add new connection'),
content: <NewConnectionDialog onSave={(_isNew, data)=>{
let connectionData = {
sgid: 0,
sid: data.sid,
did: data.did,
user: data.user,
role: data.role && null,
title: getTitle(pgAdmin, qtState.preferences.browser, null, false, data.server_name, data.database_name, data.user, true),
conn_title: getTitle(pgAdmin, null, null, true, data.server_name, data.database_name, data.user, true),
server_name: data.server_name,
database_name: data.database_name,
is_selected: true,
};
updateQueryToolConnection(connectionData, true);
onClose();
return Promise.resolve();
}}
onClose={onClose}/>
});
}, [qtState.preferences.browser]);
const onNewQueryToolClick = ()=>{
const transId = commonUtils.getRandomInt(1, 9999999);
let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected);
let parentData = {
server_group: {
_id: selectedConn.sgid || 0,
},
server: {
_id: selectedConn.sid,
server_type: qtState.params.server_type,
},
database: {
_id: selectedConn.did,
label: selectedConn.database_name,
},
};
const gridUrl = showQueryTool.generateUrl(transId, parentData, null);
const title = getTitle(pgAdmin, qtState.preferences.browser, null, false, selectedConn.server_name, selectedConn.database_name, selectedConn.user);
showQueryTool.launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, gridUrl, title, '');
};
const onManageMacros = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'manage-macros');
LayoutHelper.openDialog(docker.current, {
id: 'manage-macros',
title: gettext('Manage Macros'),
content: <MacrosDialog onSave={(newMacros)=>{
setQtState((prev)=>{
return {
params: {
...prev.params,
macros: newMacros,
},
};
});
}}
onClose={onClose}/>
}, 850, 500);
}, [qtState.preferences.browser]);
const onFilterClick = useCallback(()=>{
const onClose = ()=>LayoutHelper.close(docker.current, 'filter-dialog');
LayoutHelper.openDialog(docker.current, {
id: 'filter-dialog',
title: gettext('Sort/Filter options'),
content: <FilterDialog onSave={()=>{
onClose();
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}}
onClose={onClose}/>
}, 700, 400);
}, [qtState.preferences.browser]);
const onResetLayout = useCallback(()=>{
docker.current?.resetLayout();
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
}, []);
const queryToolContextValue = React.useMemo(()=>({
docker: docker.current,
api: api,
modal: modal,
params: qtState.params,
preferences: qtState.preferences,
}), [qtState.params, qtState.preferences]);
const queryToolConnContextValue = React.useMemo(()=>({
connected: qtState.connected,
obtainingConn: qtState.obtaining_conn,
connectionStatus: qtState.connection_status,
}), [qtState]);
/* Push only those things in context which do not change frequently */
return (
<QueryToolContext.Provider value={queryToolContextValue}>
<QueryToolConnectionContext.Provider value={queryToolConnContextValue}>
<QueryToolEventsContext.Provider value={eventBus.current}>
<Theme>
<Box width="100%" height="100%" display="flex" flexDirection="column" flexGrow="1" tabIndex="0" ref={containerRef}>
<ConnectionBar
connected={qtState.connected}
connecting={qtState.obtaining_conn}
connectionStatus={qtState.connection_status}
connectionStatusMsg={qtState.connection_status_msg}
connectionList={qtState.connection_list}
onConnectionChange={(connectionData)=>updateQueryToolConnection(connectionData)}
onNewConnClick={onNewConnClick}
onNewQueryToolClick={onNewQueryToolClick}
onResetLayout={onResetLayout}
docker={docker.current}
/>
<MainToolBar
containerRef={containerRef}
onManageMacros={onManageMacros}
onFilterClick={onFilterClick}
/>
<Layout
getLayoutInstance={(obj)=>docker.current=obj}
defaultLayout={defaultLayout}
layoutId="SQLEditor/Layout"
savedLayout={params.layout}
/>
<StatusBar />
</Box>
</Theme>
</QueryToolEventsContext.Provider>
</QueryToolConnectionContext.Provider>
</QueryToolContext.Provider>
);
}
QueryToolComponent.propTypes = {
params:PropTypes.shape({
trans_id: PropTypes.number.isRequired,
sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
sid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
did: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
server_type: PropTypes.string,
title: PropTypes.string.isRequired,
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
is_query_tool: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
username: PropTypes.string,
server_name: PropTypes.string,
database_name: PropTypes.string,
layout: PropTypes.string,
}),
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,
selectedNodeInfo: PropTypes.object,
panel: PropTypes.object,
eventBusObj: PropTypes.objectOf(EventBus),
};

View File

@@ -0,0 +1,97 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
export const QUERY_TOOL_EVENTS = {
TRIGGER_STOP_EXECUTION: 'TRIGGER_STOP_EXECUTION',
TRIGGER_EXECUTION: 'TRIGGER_EXECUTION',
TRIGGER_LOAD_FILE: 'TRIGGER_LOAD_FILE',
TRIGGER_SAVE_FILE: 'TRIGGER_SAVE_FILE',
TRIGGER_SAVE_DATA: 'TRIGGER_SAVE_DATA',
TRIGGER_DELETE_ROWS: 'TRIGGER_DELETE_ROWS',
TRIGGER_COPY_DATA: 'TRIGGER_COPY_DATA',
TRIGGER_ADD_ROWS: 'TRIGGER_ADD_ROWS',
TRIGGER_RENDER_GEOMETRIES: 'TRIGGER_RENDER_GEOMETRIES',
TRIGGER_SAVE_RESULTS: 'TRIGGER_SAVE_RESULTS',
TRIGGER_SAVE_RESULTS_END: 'TRIGGER_SAVE_RESULTS_END',
TRIGGER_PASTE_ROWS: 'TRIGGER_PASTE_ROWS',
TRIGGER_QUERY_CHANGE: 'TRIGGER_QUERY_CHANGE',
TRIGGER_INCLUDE_EXCLUDE_FILTER: 'TRIGGER_INCLUDE_EXCLUDE_FILTER',
TRIGGER_REMOVE_FILTER: 'TRIGGER_REMOVE_FILTER',
TRIGGER_SET_LIMIT: 'TRIGGER_SET_LIMIT',
TRIGGER_FORMAT_SQL: 'TRIGGER_FORMAT_SQL',
COPY_DATA: 'COPY_DATA',
SET_LIMIT_VALUE: 'SET_LIMIT_VALUE',
SET_CONNECTION_STATUS: 'SET_CONNECTION_STATUS',
EXECUTION_START: 'EXECUTION_START',
EXECUTION_END: 'EXECUTION_END',
STOP_QUERY: 'STOP_QUERY',
CURSOR_ACTIVITY: 'CURSOR_ACTIVITY',
SET_MESSAGE: 'SET_MESSAGE',
ROWS_FETCHED: 'ROWS_FETCHED',
SELECTED_ROWS_COLS_CHANGED: 'SELECTED_ROWS_COLS_CHANGED',
DATAGRID_CHANGED: 'DATAGRID_CHANGED',
HIGHLIGHT_ERROR: 'HIGHLIGHT_ERROR',
FOCUS_PANEL: 'FOCUS_PANEL',
LOAD_FILE: 'LOAD_FILE',
LOAD_FILE_DONE: 'LOAD_FILE_DONE',
SAVE_FILE: 'SAVE_FILE',
SAVE_FILE_DONE: 'SAVE_FILE_DONE',
QUERY_CHANGED: 'QUERY_CHANGED',
API_ERROR: 'API_ERROR',
SAVE_DATA_DONE: 'SAVE_DATA_DONE',
TASK_START: 'TASK_START',
TASK_END: 'TASK_END',
RENDER_GEOMETRIES: 'RENDER_GEOMETRIES',
PUSH_NOTICE: 'PUSH_NOTICE',
PUSH_HISTORY: 'PUSH_HISTORY',
HANDLE_API_ERROR: 'HANDLE_API_ERROR',
SET_FILTER_INFO: 'SET_FILTER_INFO',
FETCH_MORE_ROWS: 'FETCH_MORE_ROWS',
EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE',
EDITOR_EXEC_CMD: 'EDITOR_EXEC_CMD',
EDITOR_SET_SQL: 'EDITOR_SET_SQL',
COPY_TO_EDITOR: 'COPY_TO_EDITOR',
WARN_SAVE_DATA_CLOSE: 'WARN_SAVE_DATA_CLOSE',
WARN_SAVE_TEXT_CLOSE: 'WARN_SAVE_TEXT_CLOSE',
WARN_TXN_CLOSE: 'WARN_TXN_CLOSE',
RESET_LAYOUT: 'RESET_LAYOUT',
FORCE_CLOSE_PANEL: 'FORCE_CLOSE_PANEL',
};
export const CONNECTION_STATUS = {
TRANSACTION_STATUS_IDLE: 0,
TRANSACTION_STATUS_ACTIVE: 1,
TRANSACTION_STATUS_INTRANS: 2,
TRANSACTION_STATUS_INERROR: 3,
TRANSACTION_STATUS_UNKNOWN: 4,
};
export const CONNECTION_STATUS_MESSAGE = {
[CONNECTION_STATUS.TRANSACTION_STATUS_IDLE]: gettext('The session is idle and there is no current transaction.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE]: gettext('A command is currently in progress.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS]: gettext('The session is idle in a valid transaction block.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_INERROR]: gettext('The session is idle in a failed transaction block.'),
[CONNECTION_STATUS.TRANSACTION_STATUS_UNKNOWN]: gettext('The connection with the server is bad.')
};
export const PANELS = {
QUERY: 'id-query',
MESSAGES: 'id-messages',
SCRATCH: 'id-scratch',
DATA_OUTPUT: 'id-dataoutput',
EXPLAIN: 'id-explain',
GEOMETRY: 'id-geometry',
NOTIFICATIONS: 'id-notifications',
HISTORY: 'id-history',
};

View File

@@ -0,0 +1,99 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import JSONBigNumber from 'json-bignumber';
import _ from 'lodash';
import * as clipboard from '../../../../../../static/js/clipboard';
import { CSVToArray } from '../../../../../../static/js/utils';
export default class CopyData {
constructor(options) {
this.CSVOptions = {
field_separator: '\t',
quote_char: '"',
quoting: 'strings',
...options,
};
}
setCSVOptions(options) {
this.CSVOptions = {
...this.CSVOptions,
...options,
};
}
copyRowsToCsv(rows=[], columns=[], withHeaders=false) {
let csvRows = rows.reduce((prevCsvRows, currRow)=>{
let csvRow = columns.reduce((prevCsvCols, column)=>{
prevCsvCols.push(this.csvCell(currRow[column.key], column));
return prevCsvCols;
}, []).join(this.CSVOptions.field_separator);
prevCsvRows.push(csvRow);
return prevCsvRows;
}, []);
if(withHeaders) {
let csvRow = columns.reduce((prevCsvCols, column)=>{
prevCsvCols.push(this.csvCell(column.name, column, true));
return prevCsvCols;
}, []).join(this.CSVOptions.field_separator);
csvRows.unshift(csvRow);
}
clipboard.copyToClipboard(csvRows.join('\n'));
localStorage.setItem('copied-with-headers', withHeaders);
}
escape(iStr) {
return (this.CSVOptions.quote_char == '"') ?
iStr.replace(/\"/g, '""') : iStr.replace(/\'/g, '\'\'');
}
allQuoteCell(value) {
if (value && _.isObject(value)) {
value = this.CSVOptions.quote_char + JSONBigNumber.stringify(value) + this.CSVOptions.quote_char;
} else if (value) {
value = this.CSVOptions.quote_char + this.escape(value.toString()) + this.CSVOptions.quote_char;
} else if (_.isNull(value) || _.isUndefined(value)) {
value = '';
}
return value;
}
stringQuoteCell(value, column) {
if (value && _.isObject(value)) {
value = this.CSVOptions.quote_char + JSONBigNumber.stringify(value) + this.CSVOptions.quote_char;
} else if (value && column.cell != 'number' && column.cell != 'boolean') {
value = this.CSVOptions.quote_char + this.escape(value.toString()) + this.CSVOptions.quote_char;
} else if (column.cell == 'string' && _.isNull(value)){
value = null;
} else if (_.isNull(value) || _.isUndefined(value)) {
value = '';
}
return value;
}
csvCell(value, column, header=false) {
if (this.CSVOptions.quoting == 'all' || header) {
value = this.allQuoteCell(value);
} else if(this.CSVOptions.quoting == 'strings') {
value = this.stringQuoteCell(value, column);
}
return value;
}
getCopiedRows() {
let copiedText = clipboard.getFromClipboard();
let copiedRows = CSVToArray(copiedText, this.CSVOptions.field_separator, this.CSVOptions.quote_char);
if(localStorage.getItem('copied-with-headers') == 'true') {
copiedRows = copiedRows.slice(1);
}
return copiedRows;
}
}

View File

@@ -0,0 +1,345 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles, Box, Portal } from '@material-ui/core';
import React, {useContext} from 'react';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import CloseIcon from '@material-ui/icons/Close';
import gettext from 'sources/gettext';
import clsx from 'clsx';
import JSONBigNumber from 'json-bignumber';
import JsonEditor from '../../../../../../static/js/components/JsonEditor';
import PropTypes from 'prop-types';
import { RowInfoContext } from '.';
import Notifier from '../../../../../../static/js/helpers/Notifier';
const useStyles = makeStyles((theme)=>({
textEditor: {
position: 'absolute',
zIndex: 1050,
backgroundColor: theme.palette.background.default,
padding: '0.25rem',
fontSize: '12px',
...theme.mixins.panelBorder.all,
left: 0,
// bottom: 0,
top: 0,
'& textarea': {
width: '250px',
height: '80px',
border: 0,
outline: 0,
resize: 'both',
}
},
jsonEditor: {
position: 'absolute',
zIndex: 1050,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder,
padding: '0.25rem',
'& .jsoneditor-div': {
fontSize: '12px',
minWidth: '525px',
minHeight: '300px',
...theme.mixins.panelBorder.all,
outline: 0,
resize: 'both',
overflow: 'auto',
},
'& .jsoneditor': {
height: 'abc',
border: 'none',
'& .ace-jsoneditor .ace_marker-layer .ace_active-line': {
background: theme.palette.primary.light
}
}
},
buttonMargin: {
marginLeft: '0.5rem',
},
textarea: {
resize: 'both'
},
input: {
appearance: 'none',
width: '100%',
height: '100%',
verticalAlign: 'top',
outline: 'none',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
border: 0,
boxShadow: 'inset 0 0 0 1.5px '+theme.palette.primary.main,
padding: '0 2px',
'::selection': {
background: theme.palette.primary.light,
}
},
check: {
display: 'inline-block',
verticalAlign: 'top',
width: '16px',
height: '16px',
border: '1px solid '+theme.palette.grey[800],
margin: '3px',
textAlign: 'center',
lineHeight: '16px',
'&.checked, &.unchecked': {
background: theme.palette.background.default,
},
'&.checked:after': {
content: '\'\\2713\'',
fontWeight: 'bold',
},
'&.intermediate': {
background: theme.palette.grey[200],
'&:after': {
content: '\'\\003F\'',
fontWeight: 'bold',
},
},
}
}));
function autoFocusAndSelect(input) {
input?.focus();
input?.select();
}
function isValidArray(val) {
val = val?.trim();
return !(val != '' && (val.charAt(0) != '{' || val.charAt(val.length - 1) != '}'));
}
function setEditorPosition(cellEle, editorEle) {
if(!editorEle || !cellEle) {
return;
}
let gridEle = cellEle.closest('.rdg');
let cellRect = cellEle.getBoundingClientRect();
let position = {
left: cellRect.left,
top: cellRect.top - editorEle.offsetHeight + 12,
};
if ((position.left + editorEle.offsetWidth + 10) > gridEle.offsetWidth) {
position.left -= position.left + editorEle.offsetWidth - gridEle.offsetWidth + 10;
}
editorEle.style.left = position.left + 'px';
editorEle.style.top = position.top + 'px';
}
const EditorPropTypes = {
row: PropTypes.object,
column: PropTypes.object,
onRowChange: PropTypes.func,
onClose: PropTypes.func
};
function textColumnFinalVal(columnVal, column) {
if(columnVal === '') {
columnVal = null;
} else if (!column.is_array) {
if (columnVal === '\'\'' || columnVal === '""') {
columnVal = '';
} else if (columnVal === '\\\'\\\'') {
columnVal = '\'\'';
} else if (columnVal === '\\"\\"') {
columnVal = '""';
}
}
return columnVal;
}
export function TextEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? '';
const [localVal, setLocalVal] = React.useState(value);
const {getCellElement} = useContext(RowInfoContext);
const onChange = React.useCallback((e)=>{
setLocalVal(e.target.value);
}, []);
const onOK = ()=>{
if(column.is_array && !isValidArray(value)) {
console.error(gettext('Arrays must start with "{" and end with "}"'));
} else {
let columnVal = textColumnFinalVal(localVal, column);
onRowChange({ ...row, [column.key]: columnVal}, true);
onClose();
}
};
return(
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
}} className={classes.textEditor}>
<textarea ref={autoFocusAndSelect} className={classes.textarea} value={localVal} onChange={onChange} />
<Box display="flex" justifyContent="flex-end">
<DefaultButton startIcon={<CloseIcon />} onClick={()=>onClose(false)} size="small">
{gettext('Cancel')}
</DefaultButton>
{column.can_edit &&
<>
<PrimaryButton startIcon={<CheckRoundedIcon />} onClick={onOK} size="small" className={classes.buttonMargin}>
{gettext('OK')}
</PrimaryButton>
</>}
</Box>
</Box>
</Portal>
);
}
TextEditor.propTypes = EditorPropTypes;
export function NumberEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? '';
const onBlur = ()=>{
if(!column.is_array && isNaN(value)){
Notifier.error(gettext('Please enter a valid number'));
return;
} else if(column.is_array) {
if(!isValidArray(value)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
}
let checkVal = value.trim().slice(1, -1);
if(checkVal == '') {
checkVal = [];
} else {
checkVal = checkVal.split(',');
}
for (const val of checkVal) {
if(isNaN(val)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
}
}
}
onClose(column.can_edit ? true : false);
};
const onKeyDown = (e)=>{
if(e.code == 'Tab') {
e.preventDefault();
onBlur();
}
};
return (
<input
className={classes.input}
ref={autoFocusAndSelect}
value={value}
onChange={(e)=>{
if(column.can_edit) {
onRowChange({ ...row, [column.key]: e.target.value });
}
}}
// onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
}
NumberEditor.propTypes = EditorPropTypes;
export function CheckboxEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? null;
const changeValue = ()=>{
if(!column.can_edit) {
return;
}
let newVal = true;
if(value) {
newVal = false;
} else if(value != null && !value) {
newVal = null;
}
onRowChange({ ...row, [column.key]: newVal});
};
const onBlur = ()=>{onClose(true);};
let className = 'checked';
if(!value) {
className = 'unchecked';
} else if(value == null){
className = 'intermediate';
}
return (
<div onClick={changeValue} tabIndex="0" onBlur={onBlur}>
<span className={clsx(classes.check, className)}></span>
</div>
);
}
CheckboxEditor.propTypes = EditorPropTypes;
export function JsonTextEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const {getCellElement} = useContext(RowInfoContext);
const value = React.useMemo(()=>{
let newVal = row[column.key] ?? null;
/* If jsonb or array */
if(column.column_type_internal === 'jsonb' && !Array.isArray(newVal) && newVal != null) {
newVal = JSONBigNumber.stringify(JSONBigNumber.parse(newVal), null, 2);
} else if (Array.isArray(newVal)) {
var temp = newVal.map((ele)=>{
if (typeof ele === 'object') {
return JSONBigNumber.stringify(ele, null, 2);
}
return ele;
});
newVal = '[' + temp.join() + ']';
}
/* set editor content to empty if value is null*/
if (_.isNull(newVal)){
newVal = '';
}
return newVal;
});
const [localVal, setLocalVal] = React.useState(value);
const onChange = React.useCallback((newVal)=>{
setLocalVal(newVal);
}, []);
const onOK = ()=>{
onRowChange({ ...row, [column.key]: localVal}, true);
onClose();
};
return (
<Portal container={document.body}>
<Box ref={(ele)=>{
setEditorPosition(getCellElement(column.idx), ele);
}} className={classes.jsonEditor}>
<JsonEditor
value={localVal}
options={{
onChange: onChange,
onError: (error)=>console.error('Invalid Json: ' + error.message.split(':')[0]),
}}
className={'jsoneditor-div'}
/>
<Box display="flex" justifyContent="flex-end" marginTop="0.25rem">
<DefaultButton startIcon={<CloseIcon />} onClick={()=>onClose(false)} size="small">
{gettext('Cancel')}
</DefaultButton>
{column.can_edit &&
<>
<PrimaryButton startIcon={<CheckRoundedIcon />} onClick={onOK} size="small" className={classes.buttonMargin}>
{gettext('OK')}
</PrimaryButton>
</>}
</Box>
</Box>
</Portal>
);
}
JsonTextEditor.propTypes = EditorPropTypes;

View File

@@ -0,0 +1,73 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import _ from 'lodash';
import { makeStyles } from '@material-ui/core';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
const useStyles = makeStyles((theme)=>({
disabledCell: {
opacity: theme.palette.action.disabledOpacity,
}
}));
function NullAndDefaultFormatter({value, column, children}) {
const classes = useStyles();
if (_.isUndefined(value) && column.has_default_val) {
return <span className={classes.disabledCell}>[default]</span>;
} else if ((_.isUndefined(value) && column.not_null) ||
(_.isUndefined(value) || _.isNull(value))) {
return <span className={classes.disabledCell}>[null]</span>;
}
return children;
}
NullAndDefaultFormatter.propTypes = {
value: PropTypes.any,
column: PropTypes.object,
children: CustomPropTypes.children,
};
const FormatterPropTypes = {
row: PropTypes.object,
column: PropTypes.object,
};
export function TextFormatter({row, column}) {
let value = row[column.key];
if(!_.isNull(value) && !_.isUndefined(value)) {
value = value.toString();
}
return (
<NullAndDefaultFormatter value={value} column={column}>
<>{value}</>
</NullAndDefaultFormatter>
);
}
TextFormatter.propTypes = FormatterPropTypes;
export function NumberFormatter({row, column}) {
let value = row[column.key];
return (
<NullAndDefaultFormatter value={value} column={column}>
<div style={{textAlign: 'right'}}>{value}</div>
</NullAndDefaultFormatter>
);
}
NumberFormatter.propTypes = FormatterPropTypes;
export function BinaryFormatter({row, column}) {
let value = row[column.key];
const classes = useStyles();
return (
<NullAndDefaultFormatter value={value} column={column}>
<span className={classes.disabledCell}>[{value}]</span>
</NullAndDefaultFormatter>
);
}
BinaryFormatter.propTypes = FormatterPropTypes;

View File

@@ -0,0 +1,398 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box, makeStyles } from '@material-ui/core';
import _ from 'lodash';
import React, {useState, useEffect, useCallback, useContext, useRef} from 'react';
import ReactDataGrid, {Row, useRowSelection} from 'react-data-grid';
import LockIcon from '@material-ui/icons/Lock';
import EditIcon from '@material-ui/icons/Edit';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import * as Editors from './Editors';
import * as Formatters from './Formatters';
import clsx from 'clsx';
import { PgIconButton } from '../../../../../../static/js/components/Buttons';
import MapIcon from '@material-ui/icons/Map';
import { QueryToolEventsContext } from '../QueryToolComponent';
import PropTypes, { number } from 'prop-types';
import gettext from 'sources/gettext';
export const ROWNUM_KEY = '$_pgadmin_rownum_key_$';
export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$';
const useStyles = makeStyles((theme)=>({
root: {
height: '100%',
color: theme.palette.text.primary,
backgroundColor: theme.otherVars.qtDatagridBg,
fontSize: '12px',
border: 'none',
'--rdg-selection-color': theme.palette.primary.main,
'& .rdg-cell': {
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.bottom,
fontWeight: 'abc',
'&[aria-colindex="1"]': {
padding: 0,
}
},
'& .rdg-header-row .rdg-cell': {
padding: 0,
},
'& .rdg-header-row': {
backgroundColor: theme.palette.background.default,
fontWeight: 'normal',
},
'& .rdg-row': {
backgroundColor: theme.palette.background.default,
'&[aria-selected=true]': {
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
'& .rdg-cell:nth-child(1)': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}
},
}
},
columnHeader: {
padding: '3px 6px',
height: '100%',
display: 'flex',
lineHeight: '16px',
alignItems: 'center',
},
columnName: {
fontWeight: 'bold',
},
editedCell: {
fontWeight: 'bold',
},
deletedRow: {
'&:before': {
content: '" "',
position: 'absolute',
top: '50%',
left: 0,
borderTop: '1px solid ' + theme.palette.error.main,
width: '100%',
}
},
rowNumCell: {
padding: '0px 8px',
},
colHeaderSelected: {
outlineColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
colSelected: {
outlineColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.light,
color: theme.otherVars.qtDatagridSelectFg,
}
}));
export const RowInfoContext = React.createContext();
function CustomRow(props) {
const rowRef = useRef();
const rowInfoValue = {
rowIdx: props.rowIdx,
getCellElement: (colIdx)=>{
return rowRef.current.querySelector(`.rdg-cell[aria-colindex="${colIdx+1}"]`);
}
};
return (
<RowInfoContext.Provider value={rowInfoValue}>
<Row ref={rowRef} {...props} />
</RowInfoContext.Provider>
);
}
CustomRow.propTypes = {
rowIdx: number,
};
function SelectAllHeaderRenderer(props) {
const [checked, setChecked] = useState(false);
const eventBus = useContext(QueryToolEventsContext);
const onClick = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
setChecked(!checked);
props.onAllRowsSelectionChange(!checked);
});
};
return <div style={{widht: '100%', height: '100%'}} onClick={onClick}></div>;
}
SelectAllHeaderRenderer.propTypes = {
onAllRowsSelectionChange: PropTypes.func,
};
function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsChange}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
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 isSelected = selectedColumns.has(column.idx);
return (
<Box className={clsx(classes.columnHeader, isSelected ? classes.colHeaderSelected : null)} onClick={onClick}>
{(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') &&
<Box>
<PgIconButton title={gettext('View all geometries in this column')} icon={<MapIcon />} size="small" style={{marginRight: '0.25rem'}} onClick={(e)=>{
e.stopPropagation();
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, column);
}}/>
</Box>}
<Box marginRight="auto">
<span className={classes.columnName}>{column.display_name}</span><br/>
<span>{column.display_type}</span>
</Box>
<Box marginLeft="4px">{column.can_edit ?
<EditIcon fontSize="small" style={{fontSize: '0.875rem'}} />:
<LockIcon fontSize="small" style={{fontSize: '0.875rem'}} />
}</Box>
</Box>
);
}
SelectableHeaderRenderer.propTypes = {
column: PropTypes.object,
selectedColumns: PropTypes.objectOf(Set),
onSelectedColumnsChange: PropTypes.func,
};
function setEditorFormatter(col) {
// If grid is editable then add editor else make it readonly
if (col.cell == 'oid' && col.name == 'oid') {
col.editor = null;
col.formatter = Formatters.TextFormatter;
} else if (col.cell == 'Json') {
col.editor = Editors.JsonTextEditor;
col.formatter = Formatters.TextFormatter;
} else if (['number', 'oid'].indexOf(col.cell) != -1 || ['xid', 'real'].indexOf(col.type) != -1) {
col.formatter = Formatters.NumberFormatter;
col.editor = Editors.NumberEditor;
} else if (col.cell == 'boolean') {
col.editor = Editors.CheckboxEditor;
col.formatter = Formatters.TextFormatter;
} else if (col.cell == 'binary') {
// We do not support editing binary data in SQL editor and data grid.
col.editor = null;
col.formatter = Formatters.BinaryFormatter;
} else {
col.editor = Editors.TextEditor;
col.formatter = Formatters.TextFormatter;
}
}
function cellClassGetter(col, classes, isSelected, dataChangeStore, rowKeyGetter){
return (row)=>{
let cellClasses = [];
if(dataChangeStore && rowKeyGetter) {
if(rowKeyGetter(row) in (dataChangeStore?.updated || {})
&& !_.isUndefined(dataChangeStore?.updated[rowKeyGetter(row)]?.data[col.key])
|| rowKeyGetter(row) in (dataChangeStore?.added || {})
) {
cellClasses.push(classes.editedCell);
}
if(rowKeyGetter(row) in (dataChangeStore?.deleted || {})) {
cellClasses.push(classes.deletedRow);
}
}
if(isSelected) {
cellClasses.push(classes.colSelected);
}
return clsx(cellClasses);
};
}
function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
let retColumns = [
...columns,
];
const canvas = document.createElement('canvas');
const canvasContext = canvas.getContext('2d');
canvasContext.font = '12px Roboto';
for(const col of retColumns) {
col.width = getTextWidth(col, rows, canvasContext, columnWidthBy);
col.resizable = true;
col.editorOptions = {
commitOnOutsideClick: false,
onCellKeyDown: (e)=>{
/* Do not open the editor */
e.preventDefault();
}
};
setEditorFormatter(col);
}
let rowNumCol = {
key: ROWNUM_KEY, name: '', frozen: true, resizable: false,
minWidth: 45, width: canvasContext.measureText((totalRowCount||'').toString()).width,
};
rowNumCol.cellClass = cellClassGetter(rowNumCol);
retColumns.unshift(rowNumCol);
canvas.remove();
return retColumns;
}
function formatColumns(columns, dataChangeStore, selectedColumns, onSelectedColumnsChange, rowKeyGetter, classes) {
let retColumns = [
...columns,
];
const HeaderRenderer = (props)=>{
return <SelectableHeaderRenderer {...props} selectedColumns={selectedColumns} onSelectedColumnsChange={onSelectedColumnsChange}/>;
};
for(const [idx, col] of retColumns.entries()) {
col.headerRenderer = HeaderRenderer;
col.cellClass = cellClassGetter(col, classes, selectedColumns.has(idx), dataChangeStore, rowKeyGetter);
}
let rowNumCol = retColumns[0];
rowNumCol.headerRenderer = SelectAllHeaderRenderer;
rowNumCol.formatter = ({row})=>{
const {rowIdx} = useContext(RowInfoContext);
const [isRowSelected, onRowSelectionChange] = useRowSelection();
let rowKey = rowKeyGetter(row);
let rownum = rowIdx+1;
if(rowKey in (dataChangeStore?.added || {})) {
rownum = rownum+'+';
} else if(rowKey in (dataChangeStore?.deleted || {})) {
rownum = rownum+'-';
}
return (<div className={classes.rowNumCell} onClick={()=>{
onSelectedColumnsChange(new Set());
onRowSelectionChange({ row: row, checked: !isRowSelected, isShiftClick: false});
}}>
{rownum}
</div>);
};
return retColumns;
}
function getTextWidth(column, rows, canvas, columnWidthBy) {
const dataWidthReducer = (longest, nextRow) => {
let value = nextRow[column.key];
if(_.isNull(value) || _.isUndefined(value)) {
value = '';
}
value = value.toString();
return longest.length > value.length ? longest : value;
};
let columnHeaderLen = column.display_name.length > column.display_type.length ?
canvas.measureText(column.display_name).width : canvas.measureText(column.display_type).width;
/* padding 12, icon-width 15 */
columnHeaderLen += 15 + 12;
if(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') {
columnHeaderLen += 40;
}
let width = columnHeaderLen;
if(typeof(columnWidthBy) == 'number') {
/* padding 16 */
width = 16 + Math.ceil(canvas.measureText(rows.reduce(dataWidthReducer, '')).width);
if(width > columnWidthBy && columnWidthBy > 0) {
width = columnWidthBy;
}
if(width < columnHeaderLen) {
width = columnHeaderLen;
}
}
/* Gracefull */
width += 2;
return width;
}
export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
onSelectedCellChange, rowsResetKey, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) {
const classes = useStyles();
const [readyColumns, setColumns] = useState([]);
const eventBus = useContext(QueryToolEventsContext);
const onSelectedColumnsChangeWrapped = (arg)=>{
props.onSelectedRowsChange(new Set());
onSelectedColumnsChange(arg);
};
useEffect(()=>{
if(columns.length > 0 || rows.length > 0) {
let initCols = initialiseColumns(columns, rows, totalRowCount, columnWidthBy);
setColumns(formatColumns(initCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes));
} else {
setColumns([], [], 0);
}
}, [columns, rowsResetKey]);
useEffect(()=>{
setColumns((prevCols)=>{
return formatColumns(prevCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes);
});
}, [dataChangeStore, selectedColumns]);
const onRowClick = useCallback((row, column)=>{
if(column.key === ROWNUM_KEY) {
onSelectedCellChange && onSelectedCellChange(null);
} else {
onSelectedCellChange && onSelectedCellChange([row, column]);
}
}, []);
function handleCopy() {
if (window.isSecureContext) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA);
}
}
return (
<ReactDataGrid
id="datagrid"
columns={readyColumns}
rows={rows}
className={classes.root}
headerRowHeight={40}
rowHeight={25}
mincolumnWidthBy={50}
enableCellSelect={true}
onRowClick={onRowClick}
onCopy={handleCopy}
components={{
rowRenderer: CustomRow,
}}
{...props}
/>
);
}
QueryToolDataGrid.propTypes = {
columns: PropTypes.array,
rows: PropTypes.array,
totalRowCount: PropTypes.number,
dataChangeStore: PropTypes.object,
onSelectedCellChange: PropTypes.func,
onSelectedRowsChange: PropTypes.func,
selectedColumns: PropTypes.objectOf(Set),
onSelectedColumnsChange: PropTypes.func,
rowKeyGetter: PropTypes.func,
rowsResetKey: PropTypes.any,
columnWidthBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import HTMLReactParser from 'html-react-parser';
import PropTypes from 'prop-types';
export default function ConfirmSaveContent({closeModal, text, onDontSave, onSave}) {
const classes = useModalStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.footer}>
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<DefaultButton data-test="dont-save" className={classes.margin} startIcon={<DeleteRoundedIcon />} onClick={()=>{
onDontSave?.();
closeModal();
}} >{gettext('Don\'t save')}</DefaultButton>
<PrimaryButton data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={()=>{
onSave?.();
closeModal();
}} autoFocus={true} >{gettext('Save')}</PrimaryButton>
</Box>
</Box>
);
}
ConfirmSaveContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onDontSave: PropTypes.func,
onSave: PropTypes.func
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import HTMLReactParser from 'html-react-parser';
import { CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import PropTypes from 'prop-types';
export default function ConfirmTransactionContent({closeModal, text, onRollback, onCommit}) {
const classes = useModalStyles();
return (
<Box display="flex" flexDirection="column" height="100%">
<Box flexGrow="1" p={2}>{typeof(text) == 'string' ? HTMLReactParser(text) : text}</Box>
<Box className={classes.footer}>
<DefaultButton data-test="cancel" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton data-test="rollback" className={classes.margin} startIcon={<RollbackIcon />} onClick={()=>{
onRollback?.();
closeModal();
}} >{gettext('Rollback')}</PrimaryButton>
<PrimaryButton data-test="commit" className={classes.margin} startIcon={<CommitIcon />} onClick={()=>{
onCommit?.();
closeModal();
}} autoFocus={true} >{gettext('Commit')}</PrimaryButton>
</Box>
</Box>
);
}
ConfirmTransactionContent.propTypes = {
closeModal: PropTypes.func,
text: PropTypes.string,
onRollback: PropTypes.func,
onCommit: PropTypes.func
};

View File

@@ -0,0 +1,155 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
class SortingCollection extends BaseUISchema {
constructor(columnOptions) {
super({
name: undefined,
order: 'asc',
});
this.columnOptions = columnOptions;
this.reloadColOptions = 0;
}
setColumnOptions(columnOptions) {
this.columnOptions = columnOptions;
this.reloadColOptions = this.reloadColOptions + 1;
}
get baseFields() {
return [
{
id: 'name', label: gettext('Column'), cell: 'select', controlProps: {
allowClear: false,
}, noEmpty: true, options: this.columnOptions, optionsReloadBasis: this.reloadColOptions
},
{
id: 'order', label: gettext('Order'), cell: 'select', controlProps: {
allowClear: false,
}, options: [
{label: gettext('ASC'), value: 'asc'},
{label: gettext('DESC'), value: 'desc'},
]
},
];
}
}
class FilterSchema extends BaseUISchema {
constructor(columnOptions) {
super({
sql: null,
data_sorting: [],
});
this.sortingCollObj = new SortingCollection(columnOptions);
}
setColumnOptions(columnOptions) {
this.sortingCollObj.setColumnOptions(columnOptions);
}
get baseFields() {
let obj = this;
return [
{
id: 'sql', label: gettext('SQL Filter'), type: 'sql', controlProps: {
options: {
lineWrapping: true,
},
}
},
{
id: 'data_sorting', label: gettext('Data Sorting'), type: 'collection', schema: obj.sortingCollObj,
group: 'temp', uniqueCol: ['name'], canAdd: true, canEdit: false, canDelete: true,
},
];
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
},
}));
export default function FilterDialog({onClose, onSave}) {
const classes = useStyles();
const queryToolCtx = React.useContext(QueryToolContext);
const filterSchemaObj = React.useMemo(()=>new FilterSchema([]));
const getInitData = ()=>{
return new Promise((resolve, reject)=>{
const getFilterData = async ()=>{
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_filter_data', {
'trans_id': queryToolCtx.params.trans_id,
}));
let {column_list: columns, ...filterData} = respData.data.result;
filterSchemaObj.setColumnOptions((columns||[]).map((c)=>({label: c, value: c})));
resolve(filterData);
} catch (error) {
reject(error);
}
};
getFilterData();
});
};
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
const setFilterData = async ()=>{
try {
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_filter_data', {
'trans_id': queryToolCtx.params.trans_id,
}), changeData);
if(respData.data.status) {
resolve();
onSave();
} else {
reject(respData.data.result);
}
} catch (error) {
reject(error);
}
};
setFilterData();
});
};
return (<>
<SchemaView
formType={'dialog'}
getInitData={getInitData}
schema={filterSchemaObj}
viewHelperProps={{
mode: 'create',
}}
onSave={onSaveClick}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>
</>);
}
FilterDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,189 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import _ from 'lodash';
import PropTypes from 'prop-types';
class MacrosCollection extends BaseUISchema {
constructor(keyOptions) {
super();
this.keyOptions = keyOptions;
}
get idAttribute() {
return 'mid';
}
get baseFields() {
let obj = this;
return [
{
id: 'id', label: gettext('Key'), cell: 'select', noEmpty: true,
width: 100, options: obj.keyOptions, optionsReloadBasis: obj.keyOptions.length,
controlProps: {
allowClear: false,
}
},
{
id: 'name', label: gettext('Name'), cell: 'text', noEmpty: true,
width: 100,
},
{
id: 'sql', label: gettext('SQL'), cell: 'sql', noEmpty: true,
width: 300, controlProps: {
options: {
foldGutter: false,
lineNumbers: false,
gutters: [],
readOnly: true,
lineWrapping: true,
},
}
},
];
}
}
class MacrosSchema extends BaseUISchema {
constructor(keyOptions) {
super();
this.macrosCollObj = new MacrosCollection(keyOptions);
}
get baseFields() {
let obj = this;
return [
{
id: 'macro', label: '', type: 'collection', schema: obj.macrosCollObj,
canAdd: true, canDelete: true, isFullTab: true, group: 'temp',
},
];
}
validate(state, setError) {
let allKeys = state.macro.map((m)=>m.id.toString());
if(allKeys.length != new Set(allKeys).size) {
setError('macro', gettext('Key must be unique.'));
return true;
}
return false;
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
padding: 0,
},
}));
function getChangedMacros(macrosData, changeData) {
/* For backend, added, removed is changed. Convert all added, removed to changed. */
let changed = [];
for (const m of (changeData.macro.changed || [])) {
let newM = {...m};
if('id' in m) {
/* if key changed, clear prev and add new */
changed.push({id: m.mid, name: null, sql: null});
let em = _.find(macrosData, (d)=>d.mid==m.mid);
newM = {name: em.name, sql: em.sql, ...m};
} else {
newM.id = m.mid;
}
delete newM.mid;
changed.push(newM);
}
for (const m of (changeData.macro.deleted || [])) {
changed.push({id: m.id, name: null, sql: null});
}
for (const m of (changeData.macro.added || [])) {
changed.push(m);
}
return changed;
}
export default function MacrosDialog({onClose, onSave}) {
const classes = useStyles();
const queryToolCtx = React.useContext(QueryToolContext);
const [macrosData, setMacrosData] = React.useState([]);
const [macrosErr, setMacrosErr] = React.useState(null);
React.useEffect(async ()=>{
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_macros', {
'trans_id': queryToolCtx.params.trans_id,
}));
/* Copying id to mid to track key id changes */
setMacrosData(respData.macro.map((m)=>({...m, mid: m.id})));
} catch (error) {
setMacrosErr(error);
}
}, []);
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
const setMacros = async ()=>{
try {
let changed = getChangedMacros(macrosData, changeData);
let {data: respData} = await queryToolCtx.api.put(url_for('sqleditor.set_macros', {
'trans_id': queryToolCtx.params.trans_id,
}), {changed: changed});
resolve();
onSave(respData.macro?.filter((m)=>Boolean(m.name)));
onClose();
} catch (error) {
reject(error);
}
};
setMacros();
});
};
const keyOptions = macrosData.map((m)=>({
label: m.key_label,
value: m.id,
}));
if(keyOptions.length <= 0) {
return <></>;
}
return (<>
<SchemaView
formType={'dialog'}
getInitData={()=>{
if(macrosErr) {
return Promise.reject(macrosErr);
}
return Promise.resolve({macro: macrosData.filter((m)=>Boolean(m.name))});
}}
schema={new MacrosSchema(keyOptions)}
viewHelperProps={{
mode: 'edit',
}}
onSave={onSaveClick}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>
</>);
}
MacrosDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,254 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import SchemaView from '../../../../../../static/js/SchemaView';
import BaseUISchema from '../../../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import { QueryToolContext } from '../QueryToolComponent';
import url_for from 'sources/url_for';
import _ from 'lodash';
import { flattenSelectOptions } from '../../../../../../static/js/components/FormComponents';
import PropTypes from 'prop-types';
import ConnectServerContent from '../../../../../../browser/static/js/ConnectServerContent';
class NewConnectionSchema extends BaseUISchema {
constructor(api, params, connectServer) {
super({
sid: null,
did: null,
user: null,
role: null,
server_name: null,
database_name: null,
});
this.flatServers = [];
this.groupedServers = [];
this.dbs = [];
this.params = params;
this.api = api;
this.warningText = gettext('By changing the connection you will lose all your unsaved data for the current connection. <br> Do you want to continue?');
this.connectServer = connectServer;
}
setServerConnected(sid, icon) {
for(const group of this.groupedServers) {
for(const opt of group.options) {
if(opt.value == sid) {
opt.connected = true;
opt.image = icon || 'icon-pg';
break;
}
}
}
}
isServerConnected(sid) {
return _.find(this.flatServers, (s)=>s.value==sid)?.connected;
}
getServerList() {
let obj = this;
if(this.groupedServers?.length != 0) {
return Promise.resolve(this.groupedServers);
}
return new Promise((resolve, reject)=>{
this.api.get(url_for('sqleditor.get_new_connection_servers'))
.then(({data: respData})=>{
let groupedOptions = [];
_.forIn(respData.data.result.server_list, (v, k)=>{
/* initial selection */
_.find(v, (o)=>o.value==obj.params.sid).selected = true;
groupedOptions.push({
label: k,
options: v,
});
});
/* Will be re-used for changing icon when connected */
this.groupedServers = groupedOptions.map((group)=>{
return {
label: group.label,
options: group.options.map((o)=>({...o, selected: false})),
};
});
resolve(groupedOptions);
})
.catch((error)=>{
reject(error);
});
});
}
getOtherOptions(sid, type) {
if(!sid) {
return [];
}
if(!this.isServerConnected(sid)) {
return [];
}
return new Promise((resolve, reject)=>{
this.api.get(url_for(`sqleditor.${type}`, {
'sid': sid,
'sgid': 0,
}))
.then(({data: respData})=>{
resolve(respData.data.result.data);
})
.catch((error)=>{
reject(error);
});
});
}
get baseFields() {
let self = this;
return [
{
id: 'sid', label: gettext('Server'), type: 'select', noEmpty: true,
controlProps: {
allowClear: false,
}, options: ()=>this.getServerList(),
optionsLoaded: (res)=>this.flatServers=flattenSelectOptions(res),
optionsReloadBasis: this.flatServers.map((s)=>s.connected).join(''),
depChange: (state)=>{
/* Once the option is selected get the name */
/* Force sid to null, and set only if connected */
return {
server_name: _.find(this.flatServers, (s)=>s.value==state.sid)?.label,
did: null,
user: null,
role: null,
sid: null,
};
},
deferredDepChange: (state, source, topState, actionObj)=>{
return new Promise((resolve)=>{
let sid = actionObj.value;
if(!_.find(this.flatServers, (s)=>s.value==sid)?.connected) {
this.connectServer(sid, state.user, null, (data)=>{
self.setServerConnected(sid, data.icon);
resolve(()=>({sid: sid}));
});
} else {
resolve(()=>({sid: sid}));
}
});
},
}, {
id: 'did', label: gettext('Database'), deps: ['sid'], noEmpty: true,
controlProps: {
allowClear: false,
},
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_database'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
optionsLoaded: (res)=>this.dbs=res,
depChange: (state)=>{
/* Once the option is selected get the name */
return {database_name: _.find(this.dbs, (s)=>s.value==state.did)?.label};
}
},{
id: 'user', label: gettext('User'), deps: ['sid'], noEmpty: true,
controlProps: {
allowClear: false,
},
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_user'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
},{
id: 'role', label: gettext('Role'), deps: ['sid'],
type: (state)=>({
type: 'select',
options: ()=>this.getOtherOptions(state.sid, 'get_new_connection_role'),
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
}),
},{
id: 'server_name', label: '', type: 'text', visible: false,
},{
id: 'database_name', label: '', type: 'text', visible: false,
},
];
}
}
const useStyles = makeStyles((theme)=>({
root: {
...theme.mixins.tabPanel,
},
}));
export default function NewConnectionDialog({onClose, onSave}) {
const classes = useStyles();
const [connecting, setConnecting] = useState(false);
const queryToolCtx = React.useContext(QueryToolContext);
const connectServer = async (sid, user, formData, connectCallback) => {
setConnecting(true);
try {
let {data: respData} = await queryToolCtx.api({
method: 'POST',
url: url_for('sqleditor.connect_server', {
'sid': sid,
...(user ? {
'usr': user,
}:{}),
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
});
setConnecting(false);
connectCallback?.(respData.data);
} catch (error) {
queryToolCtx.modal.showModal(gettext('Connect to server'), (closeModal)=>{
return (
<ConnectServerContent
closeModal={()=>{
setConnecting(false);
closeModal();
}}
data={error.response?.data?.result}
onOK={(formData)=>{
connectServer(sid, null, formData, connectCallback);
}}
/>
);
});
}
};
return <SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={new NewConnectionSchema(queryToolCtx.api, {
sid: queryToolCtx.params.sid, sgid: 0,
}, connectServer)}
viewHelperProps={{
mode: 'create',
}}
loadingText={connecting ? 'Connecting...' : ''}
onSave={onSave}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
formClassName={classes.root}
/>;
}
NewConnectionDialog.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
};

View File

@@ -0,0 +1,148 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box, CircularProgress, Tooltip } from '@material-ui/core';
import { DefaultButton, PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import { ConnectedIcon, DisonnectedIcon, QueryToolIcon } from '../../../../../../static/js/components/ExternalIcon';
import { QueryToolContext } from '../QueryToolComponent';
import { CONNECTION_STATUS, CONNECTION_STATUS_MESSAGE } from '../QueryToolConstants';
import HourglassEmptyRoundedIcon from '@material-ui/icons/HourglassEmptyRounded';
import QueryBuilderRoundedIcon from '@material-ui/icons/QueryBuilderRounded';
import ErrorOutlineRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
import ReportProblemRoundedIcon from '@material-ui/icons/ReportProblemRounded';
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import RotateLeftRoundedIcon from '@material-ui/icons/RotateLeftRounded';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
},
connectionButton: {
display: 'flex',
width: '450px',
backgroundColor: theme.palette.default.main,
color: theme.palette.default.contrastText,
border: '1px solid ' + theme.palette.default.borderColor,
justifyContent: 'flex-start',
},
viewDataConnTitle: {
marginTop: 'auto',
marginBottom: 'auto',
padding: '0px 4px',
},
menu: {
'& .szh-menu': {
minWidth: '450px',
}
}
}));
function ConnectionStatusIcon({connected, connecting, status}) {
if(connecting) {
return <CircularProgress style={{height: '18px', width: '18px'}} />;
} else if(connected) {
switch (status) {
case CONNECTION_STATUS.TRANSACTION_STATUS_ACTIVE:
return <HourglassEmptyRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS:
return <QueryBuilderRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_INERROR:
return <ErrorOutlineRoundedIcon />;
case CONNECTION_STATUS.TRANSACTION_STATUS_UNKNOWN:
return <ReportProblemRoundedIcon />;
default:
return <ConnectedIcon />;
}
} else {
return <DisonnectedIcon />;
}
}
ConnectionStatusIcon.propTypes = {
connected: PropTypes.bool,
connecting: PropTypes.bool,
status: PropTypes.oneOf(Object.values(CONNECTION_STATUS)),
};
export function ConnectionBar({connected, connecting, connectionStatus, connectionStatusMsg,
connectionList, onConnectionChange, onNewConnClick, onNewQueryToolClick, onResetLayout}) {
const classes = useStyles();
const connMenuRef = React.useRef();
const [connDropdownOpen, setConnDropdownOpen] = React.useState(false);
const queryToolCtx = React.useContext(QueryToolContext);
const onConnItemClick = (e)=>{
if(!e.value.is_selected) {
onConnectionChange(e.value);
}
e.keepOpen = false;
};
const connTitle = React.useMemo(()=>_.find(connectionList, (c)=>c.is_selected)?.conn_title, [connectionList]);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
{queryToolCtx.preferences?.sqleditor?.connection_status &&
<PgIconButton title={CONNECTION_STATUS_MESSAGE[connected ? connectionStatus : -1] ?? connectionStatusMsg}
icon={<ConnectionStatusIcon connecting={connecting} status={connectionStatus} connected={connected}/>}
/>}
<DefaultButton className={classes.connectionButton} ref={connMenuRef}
onClick={queryToolCtx.params.is_query_tool ? ()=>setConnDropdownOpen(true) : undefined}
style={{backgroundColor: queryToolCtx.params.bgcolor, color: queryToolCtx.params.fgcolor}}
>
<Tooltip title={queryToolCtx.params.is_query_tool ? '' : connTitle}>
<Box display="flex" width="100%">
<Box textOverflow="ellipsis" overflow="hidden" marginRight="auto">{connecting && '(Obtaining connection)'}{connTitle}</Box>
{queryToolCtx.params.is_query_tool && <Box><KeyboardArrowDownIcon /></Box>}
</Box>
</Tooltip>
</DefaultButton>
<PgIconButton title="New query tool" icon={<QueryToolIcon />} onClick={onNewQueryToolClick}/>
</PgButtonGroup>
<PgButtonGroup size="small" variant="text" style={{marginLeft: 'auto'}}>
<PgIconButton title="Reset layout" icon={<RotateLeftRoundedIcon />} onClick={onResetLayout}/>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={connMenuRef}
open={connDropdownOpen}
onClose={()=>{setConnDropdownOpen(false);}}
className={classes.menu}
>
{(connectionList||[]).map((conn)=>{
return (
<PgMenuItem key={conn.conn_title} hasCheck checked={conn.is_selected} value={conn}
onClick={onConnItemClick}>{conn.conn_title}</PgMenuItem>
);
})}
<PgMenuItem onClick={onNewConnClick}>{`< ${gettext('New connection...')} >`}</PgMenuItem>
</PgMenu>
</>
);
}
ConnectionBar.propTypes = {
connected: PropTypes.bool,
connecting: PropTypes.bool,
connectionStatus: PropTypes.oneOf(Object.values(CONNECTION_STATUS)),
connectionStatusMsg: PropTypes.string,
connectionList: PropTypes.array,
onConnectionChange: PropTypes.func,
onNewConnClick: PropTypes.func,
onNewQueryToolClick: PropTypes.func,
onResetLayout: PropTypes.func,
};

View File

@@ -0,0 +1,386 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useRef } from 'react';
import ReactDOMServer from 'react-dom/server';
import { makeStyles } from '@material-ui/styles';
import _ from 'lodash';
import { MapContainer, TileLayer, LayersControl, GeoJSON, useMap } from 'react-leaflet';
import Leaflet, { CRS } from 'leaflet';
import {Geometry as WkxGeometry} from 'wkx';
import {Buffer} from 'buffer';
import gettext from 'sources/gettext';
import Theme from 'sources/Theme';
import clsx from 'clsx';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme)=>({
mapContainer: {
backgroundColor: theme.palette.background.default,
height: '100%',
width: '100%'
},
table: {
borderSpacing: 0,
width: '100%',
...theme.mixins.panelBorder,
},
tableCell: {
margin: 0,
padding: theme.spacing(0.5),
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
tableCellHead: {
fontWeight: 'bold',
}
}));
function parseEwkbData(rows, column) {
let key = column.key;
const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
const maxRenderGeometries = 100000; // render geometries up to 100000
let geometries3D = [],
supportedGeometries = [],
unsupportedRows = [],
geometryItemMap = new Map(),
geometryTotalByteLength = 0,
tooLargeDataSize = false,
tooManyGeometries = false,
infoList = [];
_.every(rows, function (item) {
try {
let value = item[key];
let buffer = Buffer.from(value, 'hex');
let geometry = WkxGeometry.parse(buffer);
if (geometry.hasZ) {
geometries3D.push(geometry);
return true;
}
geometryTotalByteLength += buffer.byteLength;
if (geometryTotalByteLength > maxRenderByteLength) {
tooLargeDataSize = true;
return false;
}
if (supportedGeometries.length >= maxRenderGeometries) {
tooManyGeometries = true;
return false;
}
if (!geometry.srid) {
geometry.srid = 0;
}
supportedGeometries.push(geometry);
geometryItemMap.set(geometry, item);
} catch (e) {
unsupportedRows.push(item);
}
return true;
});
// generate map info content
if (tooLargeDataSize || tooManyGeometries) {
infoList.push(supportedGeometries.length + ' of ' + rows.length + ' geometries rendered.');
}
if (geometries3D.length > 0) {
infoList.push(gettext('3D geometries not rendered.'));
}
if (unsupportedRows.length > 0) {
infoList.push(gettext('Unsupported geometries not rendered.'));
}
return [
supportedGeometries,
geometryItemMap,
infoList
];
}
function parseData(rows, columns, column) {
if (rows.length === 0) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': ['Empty row.'],
};
}
let mixedSRID = false;
// parse ewkb data
let [
supportedGeometries,
geometryItemMap,
infoList
] = parseEwkbData(rows, column);
if (supportedGeometries.length === 0) {
return {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': infoList,
};
}
// group geometries by SRID
let geometriesGroupBySRID = _.groupBy(supportedGeometries, 'srid');
let SRIDGeometriesPairs = _.toPairs(geometriesGroupBySRID);
if (SRIDGeometriesPairs.length > 1) {
mixedSRID = true;
}
// select the largest group
let selectedPair = _.max(SRIDGeometriesPairs, function (pair) {
return pair[1].length;
});
let selectedSRID = parseInt(selectedPair[0]);
let selectedGeometries = selectedPair[1];
let geoJSONs = _.map(selectedGeometries, function (geometry) {
return geometry.toGeoJSON();
});
let getPopupContent;
if (columns.length >= 3) {
// add popup when geometry has properties
getPopupContent = function (geojson) {
let geometry = selectedGeometries[geoJSONs.indexOf(geojson)];
let row = geometryItemMap.get(geometry);
let retVal = [];
for (const col of columns) {
if(col.key === column.key) {
continue;
}
retVal.push({
'column': col.display_name,
'value': row[col.key],
});
}
return retVal;
};
}
if (mixedSRID) {
infoList.push(gettext('Geometries with non-SRID %s not rendered.', selectedSRID));
}
return {
'geoJSONs': geoJSONs,
'selectedSRID': selectedSRID,
'getPopupContent': getPopupContent,
'infoList': infoList,
};
}
function PopupTable({data}) {
const classes = useStyles();
return (
<table className={classes.table}>
<tbody>
{data.map((row)=>{
return (
<tr key={row.column}>
<td className={clsx(classes.tableCell, classes.tableCellHead)}>{row.column}</td>
<td className={classes.tableCell}>{row.value}</td>
</tr>
);
})}
</tbody>
</table>
);
}
PopupTable.propTypes = {
data: PropTypes.arrayOf({
column: PropTypes.string,
value: PropTypes.string,
}),
};
function GeoJsonLayer({data}) {
const vectorLayerRef = useRef(null);
const mapObj = useMap();
useEffect(() => {
if(!vectorLayerRef.current) return;
if(data.geoJSONs.length <= 0) return;
let bounds = vectorLayerRef.current.getBounds().pad(0.1);
let maxLength = Math.max(bounds.getNorth() - bounds.getSouth(),
bounds.getEast() - bounds.getWest());
let minZoom = 0;
if(data.selectedSRID !== 4326) {
if (maxLength >= 180) {
// calculate the min zoom level to enable the map to fit the whole geometry.
minZoom = Math.floor(Math.log2(360 / maxLength)) - 2;
}
}
mapObj.setMinZoom(minZoom);
if (maxLength > 0) {
mapObj.fitBounds(bounds);
} else {
mapObj.setView(bounds.getCenter(), mapObj.getZoom());
}
});
return (
<GeoJSON
ref={vectorLayerRef}
pointToLayer={(_feature, latlng)=>{
return Leaflet.circleMarker(latlng, {
radius: 4,
weight: 3,
});
}}
style={{weight: 2}}
onEachFeature={(feature, layer)=>{
if(_.isFunction(data.getPopupContent)) {
const popupContentNode = (
<Theme>
<PopupTable data={data.getPopupContent(layer.feature.geometry)}/>
</Theme>
);
const popupContentHtml = ReactDOMServer.renderToString(popupContentNode);
layer.bindPopup(popupContentHtml, {
closeButton: false,
minWidth: 260,
maxWidth: 300,
maxHeight: 300,
});
}
}}
data={data.geoJSONs}
/>
);
}
GeoJsonLayer.propTypes = {
data: PropTypes.shape({
geoJSONs: PropTypes.array,
selectedSRID: PropTypes.number,
getPopupContent: PropTypes.func,
infoList: PropTypes.array,
}),
};
function TheMap({data}) {
const mapObj = useMap();
const infoControl = useRef(null);
useEffect(()=>{
infoControl.current = Leaflet.control({position: 'topright'});
infoControl.current.onAdd = function () {
let ele = Leaflet.DomUtil.create('div', 'geometry-viewer-info-control');
ele.innerHTML = data.infoList.join('<br />');
return ele;
};
if(data.infoList.length > 0) {
infoControl.current.addTo(mapObj);
}
return ()=>{infoControl.current && infoControl.current.remove();};
}, [data]);
return (
<>
{data.selectedSRID === 4326 &&
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Empty">
<TileLayer
url=""
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer checked name="Street">
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
maxZoom={19}
attribution='&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Topography">
<TileLayer
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
maxZoom={17}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>,'
+ ' &copy; <a href="https://opentopomap.org" target="_blank">OpenTopoMap</a>'
}
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Gray Style">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Light Color">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.pn"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Dark Matter">
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
attribution={
'&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,'
+ ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>'
}
subdomains='abcd'
/>
</LayersControl.BaseLayer>
</LayersControl>}
<GeoJsonLayer key={data.geoJSONs.length} data={data}/>
</>
);
}
TheMap.propTypes = {
data: PropTypes.shape({
geoJSONs: PropTypes.array,
selectedSRID: PropTypes.number,
getPopupContent: PropTypes.func,
infoList: PropTypes.array,
}),
};
export function GeometryViewer({rows, columns, column}) {
const classes = useStyles();
const data = parseData(rows, columns, column);
const crs = data.selectedSRID === 4326 ? CRS.EPSG3857 : CRS.Simple;
return (
<MapContainer
crs={crs}
zoom={2} center={[20, 100]}
preferCanvas={true}
scrollWheelZoom={false}
className={classes.mapContainer}
>
<TheMap data={data} />
</MapContainer>
);
}
GeometryViewer.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array,
column: PropTypes.object,
};

View File

@@ -0,0 +1,607 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import FolderRoundedIcon from '@material-ui/icons/FolderRounded';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import SaveRoundedIcon from '@material-ui/icons/SaveRounded';
import StopRoundedIcon from '@material-ui/icons/StopRounded';
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
import { FilterIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import EditRoundedIcon from '@material-ui/icons/EditRounded';
import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import FormatListNumberedRoundedIcon from '@material-ui/icons/FormatListNumberedRounded';
import HelpIcon from '@material-ui/icons/HelpRounded';
import {QUERY_TOOL_EVENTS, CONNECTION_STATUS} from '../QueryToolConstants';
import { QueryToolConnectionContext, QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuDivider, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import {shortcut_key} from 'sources/keyboard_shortcuts';
import url_for from 'sources/url_for';
import _ from 'lodash';
import { InputSelectNonSearch } from '../../../../../../static/js/components/FormComponents';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
import ConfirmTransactionContent from '../dialogs/ConfirmTransactionContent';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
},
}));
const FIXED_PREF = {
find: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 70,
'char': 'F',
},
},
replace: {
'control': true,
ctrl_is_meta: true,
'shift': isMac() ? false : true,
'alt': isMac() ? true : false,
'key': {
'key_code': 70,
'char': 'F',
},
},
jump: {
'control': false,
'shift': false,
'alt': true,
'key': {
'key_code': 71,
'char': 'G',
},
},
indent: {
'control': false,
'shift': false,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
unindent: {
'control': false,
'shift': true,
'alt': false,
'key': {
'key_code': 9,
'char': 'Tab',
},
},
comment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 191,
'char': '/',
},
},
uncomment: {
'control': true,
ctrl_is_meta: true,
'shift': false,
'alt': false,
'key': {
'key_code': 190,
'char': '.',
},
},
format_sql: {
'control': true,
'shift': true,
'alt': false,
'key': {
'key_code': 75,
'char': 'k',
},
},
};
function autoCommitRollback(type, api, transId, value) {
let url = url_for(`sqleditor.${type}`, {
'trans_id': transId,
});
return api.post(url, JSON.stringify(value));
}
export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const queryToolConnCtx = useContext(QueryToolConnectionContext);
const [highlightFilter, setHighlightFilter] = useState(false);
const [limit, setLimit] = useState('-1');
const [buttonsDisabled, setButtonsDisabled] = useState({
'save': true,
'cancel': true,
'save-data': true,
'delete-rows': true,
'commit': true,
'rollback': true,
'filter': true,
'limit': false,
});
const [menuOpenId, setMenuOpenId] = React.useState(null);
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
/* Menu button refs */
const saveAsMenuRef = React.useRef(null);
const editMenuRef = React.useRef(null);
const autoCommitMenuRef = React.useRef(null);
const explainMenuRef = React.useRef(null);
const macrosMenuRef = React.useRef(null);
const filterMenuRef = React.useRef(null);
const queryToolPref = queryToolCtx.preferences.sqleditor;
const setDisableButton = useCallback((name, disable=true)=>{
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const executeQuery = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION);
}, []);
const cancelQuery = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_STOP_EXECUTION);
}, []);
const explain = useCallback((analyze=false)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, {
format: 'json',
analyze: analyze,
verbose: Boolean(checkedMenuItems['explain_verbose']),
costs: Boolean(checkedMenuItems['explain_costs']),
buffers: analyze ? Boolean(checkedMenuItems['explain_buffers']) : false,
timing: analyze ? Boolean(checkedMenuItems['explain_timing']) : false,
summary: Boolean(checkedMenuItems['explain_summary']),
settings: Boolean(checkedMenuItems['explain_settings']),
});
}, [checkedMenuItems]);
const explainAnalyse = useCallback(()=>{
explain(true);
}, [explain]);
const openMenu = useCallback((e)=>{
setMenuOpenId(e.currentTarget.name);
}, []);
const handleMenuClose = useCallback(()=>{
setMenuOpenId(null);
}, []);
const checkMenuClick = useCallback((e)=>{
setCheckedMenuItems((prev)=>{
let newVal = !prev[e.value];
if(e.value === 'auto_commit' || e.value === 'auto_rollback') {
autoCommitRollback(e.value, queryToolCtx.api, queryToolCtx.params.trans_id, newVal)
.catch ((error)=>{
newVal = prev[e.value];
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, {
checkTransaction: true,
});
});
}
return {
...prev,
[e.value]: newVal,
};
});
}, []);
const openFile = useCallback(()=>{
confirmDiscard(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_LOAD_FILE);
});
}, []);
const saveFile = useCallback((saveAs=false)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, saveAs);
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_START, ()=>{
setDisableButton('execute', true);
setDisableButton('cancel', false);
setDisableButton('explain', true);
setDisableButton('explain_analyse', true);
setDisableButton('limit', true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, ()=>{
setDisableButton('execute', false);
setDisableButton('cancel', true);
setDisableButton('explain', false);
setDisableButton('explain_analyse', false);
setDisableButton('limit', false);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS, ()=>{
setDisableButton('execute', true);
setDisableButton('explain', true);
setDisableButton('explain_analyse', true);
setDisableButton('limit', true);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS_END, ()=>{
setDisableButton('execute', false);
setDisableButton('explain', false);
setDisableButton('explain_analyse', false);
setDisableButton('limit', false);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.QUERY_CHANGED, (isDirty)=>{
setDisableButton('save', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (isDirty)=>{
setDisableButton('save-data', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows)=>{
setDisableButton('delete-rows', !rows);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_FILTER_INFO, (canFilter, filterApplied)=>{
setDisableButton('filter', !canFilter);
setHighlightFilter(filterApplied);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_LIMIT_VALUE, (l)=>{
setLimit(l);
});
}, []);
useEffect(()=>{
setDisableButton('execute', queryToolConnCtx.obtainingConn);
setDisableButton('explain', queryToolConnCtx.obtainingConn);
setDisableButton('explain_analyse', queryToolConnCtx.obtainingConn);
}, [queryToolConnCtx.obtainingConn]);
const isInTxn = ()=>(queryToolConnCtx.connectionStatus == CONNECTION_STATUS.TRANSACTION_STATUS_INTRANS
|| queryToolConnCtx.connectionStatus == CONNECTION_STATUS.TRANSACTION_STATUS_INERROR);
const onExecutionDone = ()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, (success)=>{
if(success) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL);
}
}, true);
};
const warnTxnClose = ()=>{
if(!isInTxn() || !queryToolCtx.preferences?.sqleditor.prompt_commit_transaction) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.FORCE_CLOSE_PANEL);
return;
}
queryToolCtx.modal.showModal(gettext('Commit transaction?'), (closeModal)=>(
<ConfirmTransactionContent
closeModal={closeModal}
text={gettext('The current transaction is not commited to the database. '
+'Do you want to commit or rollback the transaction?')}
onRollback={()=>{
onExecutionDone();
onRollbackClick();
}}
onCommit={()=>{
onExecutionDone();
onCommitClick();
}}
/>
));
};
useEffect(()=>{
if(isInTxn()) {
setDisableButton('commit', false);
setDisableButton('rollback', false);
} else {
setDisableButton('commit', true);
setDisableButton('rollback', true);
}
eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE, warnTxnClose);
return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE, warnTxnClose);
};
}, [queryToolConnCtx.connectionStatus]);
const onCommitClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, true);
};
const onRollbackClick=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, true);
};
const executeMacro = (m)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, m.sql, null, true);
};
const onLimitChange=(e)=>{
setLimit(e.target.value);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SET_LIMIT,e.target.value);
};
const formatSQL=()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL);
};
const clearQuery=()=>{
confirmDiscard(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, '');
});
};
const onHelpClick=()=>{
let url = url_for('help.static', {'filename': 'query_tool.html'});
window.open(url, 'pgadmin_help');
};
const confirmDiscard=(callback)=>{
queryToolCtx.modal.confirm(
gettext('Unsaved changes'),
gettext('Are you sure you wish to discard the current changes?'),
function() {
callback();
},
function() {
return true;
}
);
};
useEffect(()=>{
if(queryToolPref) {
/* Get the prefs first time */
if(_.isUndefined(checkedMenuItems.auto_commit)) {
setCheckedMenuItems({
auto_commit: queryToolPref.auto_commit,
auto_rollback: queryToolPref.auto_rollback,
explain_verbose: queryToolPref.explain_verbose,
explain_costs: queryToolPref.explain_costs,
explain_buffers: queryToolPref.explain_buffers,
explain_timing: queryToolPref.explain_timing,
explain_summary: queryToolPref.explain_summary,
explain_settings: queryToolPref.explain_settings,
});
}
}
}, [queryToolPref]);
/* Button shortcuts */
useKeyboardShortcuts([
{
shortcut: queryToolPref.execute_query,
options: {
callback: ()=>{executeQuery();}
}
},
{
shortcut: queryToolPref.explain_query,
options: {
callback: (e)=>{e.preventDefault();explain();}
}
},
{
shortcut: queryToolPref.explain_analyze_query,
options: {
callback: ()=>{explainAnalyse();}
}
},
{
shortcut: queryToolPref.commit_transaction,
options: {
callback: ()=>{onCommitClick();}
}
},
{
shortcut: queryToolPref.rollback_transaction,
options: {
callback: ()=>{onRollbackClick();}
}
},
{
shortcut: FIXED_PREF.format_sql,
options: {
callback: ()=>{formatSQL();}
}
},
{
shortcut: queryToolPref.clear_query,
options: {
callback: ()=>{clearQuery();}
}
},
], containerRef);
/* Macro shortcuts */
useKeyboardShortcuts(
queryToolCtx.params?.macros?.map((m)=>{
return {
shortcut: {
...m,
'key': {
'key_code': m.key_code,
'char': m.key,
},
},
options: {
callback: ()=>{executeMacro(m);}
}
};
}) || [],
containerRef
);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Open File')} icon={<FolderRoundedIcon />} disabled={!queryToolCtx.params.is_query_tool}
accesskey={shortcut_key(queryToolPref.btn_open_file)} onClick={openFile} />
<PgIconButton title={gettext('Save File')} icon={<SaveRoundedIcon />}
accesskey={shortcut_key(queryToolPref.btn_save_file)} disabled={buttonsDisabled['save'] || !queryToolCtx.params.is_query_tool}
onClick={()=>{saveFile(false);}} />
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton disabled={!queryToolCtx.params.is_query_tool}
name="menu-saveas" ref={saveAsMenuRef} onClick={openMenu}
/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Edit')} icon={
<><EditRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool}
name="menu-edit" ref={editMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small" color={highlightFilter ? 'primary' : 'default'}>
<PgIconButton title={gettext('Sort/Filter')} icon={<FilterIcon />}
onClick={onFilterClick} disabled={buttonsDisabled['filter']} accesskey={shortcut_key(queryToolPref.btn_filter_dialog)}/>
<PgIconButton title={gettext('Filter options')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={buttonsDisabled['filter']} name="menu-filter" ref={filterMenuRef} accesskey={shortcut_key(queryToolPref.btn_filter_options)}
onClick={openMenu} />
</PgButtonGroup>
<InputSelectNonSearch options={[
{label: gettext('No limit'), value: '-1'},
{label: gettext('1000 rows'), value: '1000'},
{label: gettext('500 rows'), value: '500'},
{label: gettext('100 rows'), value: '100'},
]} value={limit} onChange={onLimitChange} disabled={buttonsDisabled['limit'] || queryToolCtx.params.is_query_tool} />
<PgButtonGroup size="small">
<PgIconButton title={gettext('Cancel query')} icon={<StopRoundedIcon style={{height: 'unset'}} />}
onClick={cancelQuery} disabled={buttonsDisabled['cancel']} accesskey={shortcut_key(queryToolPref.btn_cancel_query)} />
<PgIconButton title={gettext('Execute/Refresh')} icon={<PlayArrowRoundedIcon style={{height: 'unset'}} />}
onClick={executeQuery} disabled={buttonsDisabled['execute']} shortcut={queryToolPref.execute_query}/>
<PgIconButton title={gettext('Execute options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-autocommit" ref={autoCommitMenuRef} accesskey={shortcut_key(queryToolPref.btn_delete_row)}
onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Explain')} icon={<ExplicitRoundedIcon />}
onClick={()=>{explain();}} disabled={buttonsDisabled['explain'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.explain_query}/>
<PgIconButton title={gettext('Explain Analyze')} icon={<AssessmentRoundedIcon />}
onClick={()=>{explainAnalyse();}} disabled={buttonsDisabled['explain_analyse'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.explain_analyze_query}/>
<PgIconButton title={gettext('Explain Settings')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={!queryToolCtx.params.is_query_tool}
name="menu-explain" ref={explainMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Commit')} icon={<CommitIcon />}
onClick={onCommitClick} disabled={buttonsDisabled['commit']} shortcut={queryToolPref.commit_transaction}/>
<PgIconButton title={gettext('Rollback')} icon={<RollbackIcon />}
onClick={onRollbackClick} disabled={buttonsDisabled['rollback']} shortcut={queryToolPref.rollback_transaction}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Macros')} icon={
<><FormatListNumberedRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool} name="menu-macros" ref={macrosMenuRef} onClick={openMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={saveAsMenuRef}
open={menuOpenId=='menu-saveas'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={()=>{saveFile(true);}}>{gettext('Save as')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={editMenuRef}
open={menuOpenId=='menu-edit'}
onClose={handleMenuClose}
>
<PgMenuItem shortcut={FIXED_PREF.find}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.replace}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, true);}}>{gettext('Replace')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.jump}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'jumpToLine');}}>{gettext('Jump')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.indent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentMore');}}>{gettext('Indent')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.unindent}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'indentLess');}}>{gettext('Unindent')}</PgMenuItem>
<PgMenuItem shortcut={FIXED_PREF.comment}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, 'toggleComment');}}>{gettext('Toggle comment')}</PgMenuItem>
<PgMenuItem shortcut={queryToolPref.clear_query}
onClick={clearQuery}>{gettext('Clear query')}</PgMenuItem>
<PgMenuDivider />
<PgMenuItem shortcut={FIXED_PREF.format_sql}onClick={formatSQL}>{gettext('Format SQL')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={filterMenuRef}
open={menuOpenId=='menu-filter'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, true);}}>{gettext('Filter by Selection')}</PgMenuItem>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, false);}}>{gettext('Exclude by Selection')}</PgMenuItem>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_REMOVE_FILTER);}}>{gettext('Remove Sort/Filter')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={autoCommitMenuRef}
open={menuOpenId=='menu-autocommit'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="auto_commit" checked={checkedMenuItems['auto_commit']}
onClick={checkMenuClick}>{gettext('Auto commit?')}</PgMenuItem>
<PgMenuItem hasCheck value="auto_rollback" checked={checkedMenuItems['auto_rollback']}
onClick={checkMenuClick}>{gettext('Auto rollback on error?')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={explainMenuRef}
open={menuOpenId=='menu-explain'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="explain_verbose" checked={checkedMenuItems['explain_verbose']}
onClick={checkMenuClick}>{gettext('Verbose')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_costs" checked={checkedMenuItems['explain_costs']}
onClick={checkMenuClick}>{gettext('Costs')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_buffers" checked={checkedMenuItems['explain_buffers']}
onClick={checkMenuClick}>{gettext('Buffers')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_timing" checked={checkedMenuItems['explain_timing']}
onClick={checkMenuClick}>{gettext('Timing')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_summary" checked={checkedMenuItems['explain_summary']}
onClick={checkMenuClick}>{gettext('Summary')}</PgMenuItem>
<PgMenuItem hasCheck value="explain_settings" checked={checkedMenuItems['explain_settings']}
onClick={checkMenuClick}>{gettext('Settings')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={macrosMenuRef}
open={menuOpenId=='menu-macros'}
onClose={handleMenuClose}
>
<PgMenuItem onClick={onManageMacros}>{gettext('Manage macros')}</PgMenuItem>
<PgMenuDivider />
{queryToolCtx.params?.macros?.map((m, i)=>{
return (
<PgMenuItem shortcut={{
...m,
'key': {
'key_code': m.key_code,
'char': m.key,
},
}} onClick={()=>executeMacro(m)} key={i}>
{m.name}
</PgMenuItem>
);
})}
</PgMenu>
</>
);
}
MainToolBar.propTypes = {
containerRef: CustomPropTypes.ref,
onFilterClick: PropTypes.func,
onManageMacros: PropTypes.func,
};

View File

@@ -0,0 +1,46 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { QueryToolEventsContext } from '../QueryToolComponent';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
const useStyles = makeStyles((theme)=>({
root: {
whiteSpace: 'pre-wrap',
fontFamily: '"Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
padding: '5px 10px',
overflow: 'auto',
height: '100%',
fontSize: '12px',
userSelect: 'text',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
...theme.mixins.fontSourceCode,
}
}));
export function Messages() {
const classes = useStyles();
const [messageText, setMessageText] = React.useState('');
const eventBus = React.useContext(QueryToolEventsContext);
React.useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.SET_MESSAGE, (text, append=false)=>{
setMessageText((prev)=>{
if(append) {
return prev+text;
}
return text;
});
});
}, []);
return (
<div className={classes.root} tabIndex="0">{messageText}</div>
);
}

View File

@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { commonTableStyles } from '../../../../../../static/js/Theme';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import gettext from 'sources/gettext';
import _ from 'lodash';
import clsx from 'clsx';
import { QueryToolEventsContext } from '../QueryToolComponent';
export function Notifications() {
const [notices, setNotices] = React.useState([]);
const tableClasses = commonTableStyles();
const eventBus = React.useContext(QueryToolEventsContext);
React.useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_NOTICE, (notice)=>{
if(_.isArray(notice)) {
setNotices((prev)=>[
...prev,
...notice,
]);
} else {
setNotices((prev)=>[
...prev,
notice,
]);
}
});
}, []);
return <table className={clsx(tableClasses.table, tableClasses.borderBottom)}>
<thead>
<tr>
<th>{gettext('Recorded time')}</th>
<th>{gettext('Event')}</th>
<th>{gettext('Process ID')}</th>
<th>{gettext('Payload')}</th>
</tr>
</thead>
<tbody>
{notices.map((notice, i)=>{
return <tr key={i}>
<td>{notice.recorded_time}</td>
<td>{notice.channel}</td>
<td>{notice.pid}</td>
<td>{notice.payload}</td>
</tr>;
})}
</tbody>
</table>;
}

View File

@@ -0,0 +1,401 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React, {useContext, useCallback, useEffect } from 'react';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import url_for from 'sources/url_for';
import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext';
import OrigCodeMirror from 'bundled_codemirror';
import Notifier from '../../../../../../static/js/helpers/Notifier';
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
const useStyles = makeStyles(()=>({
sql: {
height: '100%',
}
}));
function registerAutocomplete(api, transId, onFailure) {
OrigCodeMirror.registerHelper('hint', 'sql', function (editor) {
var data = [],
doc = editor.getDoc(),
cur = doc.getCursor(),
// Get the current cursor position
current_cur = cur.ch,
// function context
ctx = {
editor: editor,
// URL for auto-complete
url: url_for('sqleditor.autocomplete', {
'trans_id': transId,
}),
data: data,
// Get the line number in the cursor position
current_line: cur.line,
/*
* Render function for hint to add our own class
* and icon as per the object type.
*/
hint_render: function (elt, data_arg, cur_arg) {
var el = document.createElement('span');
switch (cur_arg.type) {
case 'database':
el.className = 'sqleditor-hint pg-icon-' + cur_arg.type;
break;
case 'datatype':
el.className = 'sqleditor-hint icon-type';
break;
case 'keyword':
el.className = 'fa fa-key';
break;
case 'table alias':
el.className = 'fa fa-at';
break;
default:
el.className = 'sqleditor-hint icon-' + cur_arg.type;
}
el.appendChild(document.createTextNode(cur_arg.text));
elt.appendChild(el);
},
};
data.push(doc.getValue());
// Get the text from start to the current cursor position.
data.push(
doc.getRange({
line: 0,
ch: 0,
}, {
line: ctx.current_line,
ch: current_cur,
})
);
return {
then: function (cb) {
var self_local = this;
// Make ajax call to find the autocomplete data
api.post(self_local.url, JSON.stringify(self_local.data))
.then((res) => {
var result = [];
_.each(res.data.data.result, function (obj, key) {
result.push({
text: key,
type: obj.object_type,
render: self_local.hint_render,
});
});
// Sort function to sort the suggestion's alphabetically.
result.sort(function (a, b) {
var textA = a.text.toLowerCase(),
textB = b.text.toLowerCase();
if (textA < textB) //sort string ascending
return -1;
if (textA > textB)
return 1;
return 0; //default return value (no sorting)
});
/*
* Below logic find the start and end point
* to replace the selected auto complete suggestion.
*/
var token = self_local.editor.getTokenAt(cur),
start, end, search;
if (token.end > cur.ch) {
token.end = cur.ch;
token.string = token.string.slice(0, cur.ch - token.start);
}
if (token.string.match(/^[.`\w@]\w*$/)) {
search = token.string;
start = token.start;
end = token.end;
} else {
start = end = cur.ch;
search = '';
}
/*
* Added 1 in the start position if search string
* started with "." or "`" else auto complete of code mirror
* will remove the "." when user select any suggestion.
*/
if (search.charAt(0) == '.' || search.charAt(0) == '``')
start += 1;
cb({
list: result,
from: {
line: self_local.current_line,
ch: start,
},
to: {
line: self_local.current_line,
ch: end,
},
});
})
.catch((err) => {
onFailure?.(err);
});
}.bind(ctx),
};
});
}
export default function Query() {
const classes = useStyles();
const editor = React.useRef();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const layoutEvenBus = useContext(LayoutEventsContext);
const lastSavedText = React.useRef('');
const markedLine = React.useRef(0);
const removeHighlightError = (cmObj)=>{
// Remove already existing marker
cmObj.removeLineClass(markedLine.current, 'wrap', 'CodeMirror-activeline-background');
markedLine.current = 0;
};
const highlightError = (cmObj, result)=>{
let errorLineNo = 0,
startMarker = 0,
endMarker = 0,
selectedLineNo = 0;
removeHighlightError(cmObj);
// In case of selection we need to find the actual line no
if (cmObj.getSelection().length > 0)
selectedLineNo = cmObj.getCursor(true).line;
// Fetch the LINE string using regex from the result
var line = /LINE (\d+)/.exec(result),
// Fetch the Character string using regex from the result
char = /Character: (\d+)/.exec(result);
// If line and character is null then no need to mark
if (line != null && char != null) {
errorLineNo = (parseInt(line[1]) - 1) + selectedLineNo;
var errorCharNo = (parseInt(char[1]) - 1);
/* We need to loop through each line till the error line and
* count the total no of character to figure out the actual
* starting/ending marker point for the individual line. We
* have also added 1 per line for the "\n" character.
*/
var prevLineChars = 0;
for (let i = selectedLineNo > 0 ? selectedLineNo : 0; i < errorLineNo; i++)
prevLineChars += cmObj.getLine(i).length + 1;
/* Marker starting point for the individual line is
* equal to error character index minus total no of
* character till the error line starts.
*/
startMarker = errorCharNo - prevLineChars;
// Find the next space from the character or end of line
var errorLine = cmObj.getLine(errorLineNo);
if (_.isUndefined(errorLine)) return;
endMarker = errorLine.indexOf(' ', startMarker);
if (endMarker < 0)
endMarker = errorLine.length;
// Mark the error text
cmObj.markText({
line: errorLineNo,
ch: startMarker,
}, {
line: errorLineNo,
ch: endMarker,
}, {
className: 'sql-editor-mark',
});
markedLine.current = errorLineNo;
cmObj.addLineClass(errorLineNo, 'wrap', 'CodeMirror-activeline-background');
cmObj.focus();
cmObj.setCursor(errorLineNo, 0);
}
};
const triggerExecution = (explainObject)=>{
if(queryToolCtx.params.is_query_tool) {
let query = editor.current?.getSelection() || editor.current?.getValue() || '';
if(query) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject);
}
} else {
eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null);
}
};
useEffect(()=>{
layoutEvenBus.registerListener(LAYOUT_EVENTS.ACTIVE, (currentTabId)=>{
currentTabId == PANELS.QUERY && editor.current.focus();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, triggerExecution);
eventBus.registerListener(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, (result)=>{
if(result) {
highlightError(editor.current, result);
} else {
removeHighlightError(editor.current);
}
});
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{
queryToolCtx.api.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName),
}).then((res)=>{
editor.current.setValue(res.data);
lastSavedText.current = res.data;
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, fileName, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE, (fileName)=>{
let editorValue = editor.current.getValue();
queryToolCtx.api.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': editor.current.getValue(),
}).then(()=>{
lastSavedText.current = editorValue;
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, fileName, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
Notifier.success(gettext('File saved successfully.'));
}).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, null, false);
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_EXEC_CMD, (cmd='')=>{
editor.current?.execCommand(cmd);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, (text)=>{
editor.current?.setValue(text);
eventBus.fireEvent(QUERY_TOOL_EVENTS.FOCUS_PANEL, PANELS.QUERY);
setTimeout(()=>{
editor.current?.focus();
editor.current?.setCursor(editor.current.lineCount(), 0);
}, 250);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, (replace=false)=>{
editor.current?.focus();
let key = {
keyCode: 70, metaKey: false, ctrlKey: true, shiftKey: replace, altKey: false,
};
if(isMac()) {
key.metaKey = true;
key.ctrlKey = false;
key.shiftKey = false;
key.altKey = true;
}
editor.current?.triggerOnKeyDown(
new KeyboardEvent('keydown', key)
);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_SET_SQL, (value, focus=true)=>{
focus && editor.current?.focus();
editor.current?.setValue(value);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_QUERY_CHANGE, ()=>{
change();
});
eventBus.registerListener(QUERY_TOOL_EVENTS.WARN_SAVE_TEXT_CLOSE, ()=>{
if(!isDirty() || !queryToolCtx.preferences?.sqleditor.prompt_save_query_changes) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
return;
}
queryToolCtx.modal.showModal(gettext('Save query changes?'), (closeModal)=>(
<ConfirmSaveContent
closeModal={closeModal}
text={gettext('The query text has changed. Do you want to save changes?')}
onDontSave={()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
}}
onSave={()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.SAVE_FILE_DONE, (_f, success)=>{
if(success) {
eventBus.fireEvent(QUERY_TOOL_EVENTS.WARN_TXN_CLOSE);
}
}, true);
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE);
}}
/>
));
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{
let selection = true, sql = editor.current?.getSelection();
if(sql == '') {
sql = editor.current.getValue();
selection = false;
}
queryToolCtx.api.post(url_for('sql.format'), {
'sql': sql,
}).then((res)=>{
if(selection) {
editor.current.replaceSelection(res.data.data.sql, 'around');
} else {
editor.current.setValue(res.data.data.sql);
}
}).catch(()=>{/* failure should be ignored */});
});
editor.current.focus();
}, []);
useEffect(()=>{
registerAutocomplete(queryToolCtx.api, queryToolCtx.params.trans_id, (err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
});
}, [queryToolCtx.params.trans_id]);
const isDirty = ()=>lastSavedText.current !== editor.current.getValue();
const cursorActivity = useCallback((cmObj)=>{
const c = cmObj.getCursor();
eventBus.fireEvent(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, [c.line+1, c.ch+1]);
}, []);
const change = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.QUERY_CHANGED, isDirty());
}, []);
return <CodeMirror
currEditor={(obj)=>{
editor.current=obj;
}}
value={''}
className={classes.sql}
events={{
'focus': cursorActivity,
'cursorActivity': cursorActivity,
'change': change,
}}
disabled={!queryToolCtx.params.is_query_tool}
autocomplete={true}
/>;
}

View File

@@ -0,0 +1,500 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import { PANELS, QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import gettext from 'sources/gettext';
import _ from 'lodash';
import clsx from 'clsx';
import { Box, Grid, List, ListItem, ListSubheader } from '@material-ui/core';
import url_for from 'sources/url_for';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import moment from 'moment';
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import { SaveDataIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import { InputSwitch } from '../../../../../../static/js/components/FormComponents';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import { DefaultButton } from '../../../../../../static/js/components/Buttons';
import { useDelayedCaller } from '../../../../../../static/js/custom_hooks';
import Notifier from '../../../../../../static/js/helpers/Notifier';
import Loader from 'sources/components/Loader';
import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import PropTypes from 'prop-types';
import { parseApiError } from '../../../../../../static/js/api_instance';
import * as clipboard from '../../../../../../static/js/clipboard';
const useStyles = makeStyles((theme)=>({
leftRoot: {
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.right,
},
listRoot: {
...theme.mixins.panelBorder.top,
},
listSubheader: {
padding: '0.25rem',
lineHeight: 'unset',
color: theme.palette.text.muted,
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder.bottom,
...theme.mixins.fontSourceCode,
},
removePadding: {
padding: 0,
},
fontSourceCode:{
...theme.mixins.fontSourceCode,
userSelect: 'text',
},
itemError: {
backgroundColor: theme.palette.error.light,
'&.Mui-selected': {
backgroundColor: theme.palette.error.light,
'&:hover': {
backgroundColor: theme.palette.error.light,
}
}
},
detailsQuery: {
marginTop: '0.5rem',
...theme.mixins.panelBorder.all,
},
copyBtn: {
borderRadius: 0,
paddingLeft: '8px',
paddingRight: '8px',
borderTop: 'none',
borderLeft: 'none',
borderColor: theme.otherVars.borderColor,
fontSize: '13px',
},
infoHeader: {
fontSize: '13px',
padding: '0.5rem',
backgroundColor: theme.otherVars.editorToolbarBg,
},
removeBtnMargin: {
marginLeft: '0.25rem',
}
}));
export const QuerySources = {
EXECUTE: {
ICON_CSS_CLASS: 'fa fa-play',
},
EXPLAIN: {
ICON_CSS_CLASS: 'fa fa-hand-pointer',
},
EXPLAIN_ANALYZE: {
ICON_CSS_CLASS: 'fa fa-list-alt',
},
COMMIT: {
ICON_CSS_CLASS: 'pg-font-icon icon-commit',
},
ROLLBACK: {
ICON_CSS_CLASS: 'pg-font-icon icon-rollback',
},
SAVE_DATA: {
ICON_CSS_CLASS: 'pg-font-icon icon-save_data_changes',
},
VIEW_DATA: {
ICON_CSS_CLASS: 'pg-font-icon icon-view_data',
},
};
class QueryHistoryUtils {
constructor() {
this._entries = [];
this.showInternal = true;
}
dateAsGroupKey(date) {
return moment(date).format('YYYY MM DD');
}
getItemKey(entry) {
return this.dateAsGroupKey(entry.start_time) + this.formatEntryDate(entry.start_time) + (entry.subKey ?? '');
}
getDateFormatted(date) {
return date.toLocaleDateString();
}
formatEntryDate(date) {
return moment(date).format('HH:mm:ss');
}
isDaysBefore(date, before) {
return (
this.getDateFormatted(date) ===
this.getDateFormatted(moment().subtract(before, 'days').toDate())
);
}
getDatePrefix(date) {
let prefix = '';
if (this.isDaysBefore(date, 0)) {
prefix = 'Today - ';
} else if (this.isDaysBefore(date, 1)) {
prefix = 'Yesterday - ';
}
return prefix;
}
addEntry(entry) {
entry.groupKey = this.dateAsGroupKey(entry.start_time);
entry.itemKey = this.getItemKey(entry);
let existEntry = _.find(this._entries, (e)=>e.itemKey==entry.itemKey);
if(existEntry) {
entry.itemKey = this.getItemKey(entry) + _.uniqueId();
}
let insertIndex = _.sortedIndexBy(this._entries, entry, (e)=>e.itemKey);
this._entries = [
...this._entries.slice(0, insertIndex),
entry,
...this._entries.slice(insertIndex),
];
}
getEntries() {
if(!this.showInternal) {
return this._entries.filter((e)=>!e.is_pgadmin_query);
}
return this._entries;
}
getEntry(itemKey) {
return _.find(this.getEntries(), (e)=>e.itemKey==itemKey);
}
getGroupHeader(entry) {
return this.getDatePrefix(entry.start_time)+this.getDateFormatted(entry.start_time);
}
getGroups() {
return _.sortedUniqBy(this.getEntries().map((e)=>[e.groupKey, this.getGroupHeader(e)]), (g)=>g[0]).reverse();
}
getGroupEntries(groupKey) {
return this.getEntries().filter((e)=>e.groupKey==groupKey).reverse();
}
getNextItemKey(currKey) {
let nextIndex = this.getEntries().length-1;
if(currKey) {
let currIndex = _.findIndex(this.getEntries(), (e)=>e.itemKey==currKey);
if(currIndex == 0) {
nextIndex = currIndex;
} else {
nextIndex = currIndex - 1;
}
}
return this.getEntries()[nextIndex]?.itemKey;
}
getPrevItemKey(currKey) {
let nextIndex = this.getEntries().length-1;
if(currKey) {
let currIndex = _.findIndex(this.getEntries(), (e)=>e.itemKey==currKey);
if(currIndex == this.getEntries().length-1) {
nextIndex = currIndex;
} else {
nextIndex = currIndex + 1;
}
}
return this.getEntries()[nextIndex]?.itemKey;
}
clear(itemKey) {
if(itemKey) {
let nextKey = this.getNextItemKey(itemKey);
let removeIdx = _.findIndex(this._entries, (e)=>e.itemKey==itemKey);
this._entries.splice(removeIdx, 1);
return nextKey;
} else {
this._entries = [];
}
}
size() {
return this._entries.length;
}
}
function QuerySourceIcon({source}) {
switch(JSON.stringify(source)) {
case JSON.stringify(QuerySources.EXECUTE):
return <PlayArrowRoundedIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.EXPLAIN):
return <ExplicitRoundedIcon/>;
case JSON.stringify(QuerySources.EXPLAIN_ANALYZE):
return <AssessmentRoundedIcon/>;
case JSON.stringify(QuerySources.COMMIT):
return <CommitIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.ROLLBACK):
return <RollbackIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.SAVE_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.VIEW_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
default:
return <></>;
}
}
QuerySourceIcon.propTypes = {
source: PropTypes.object,
};
function HistoryEntry({entry, formatEntryDate, itemKey, selectedItemKey, onClick}) {
const classes = useStyles();
return <ListItem tabIndex="0" ref={(ele)=>{
selectedItemKey==itemKey && ele && ele.scrollIntoView({
block: 'center',
behavior: 'smooth',
});
}} className={clsx(classes.fontSourceCode, entry.status ? '' : classes.itemError)} selected={selectedItemKey==itemKey} onClick={onClick}>
<Box whiteSpace="nowrap" textOverflow="ellipsis" overflow="hidden" >
<QuerySourceIcon source={entry.query_source}/>
{entry.query}
</Box>
<Box fontSize="12px">
{formatEntryDate(entry.start_time)}
</Box>
</ListItem>;
}
const EntryPropType = PropTypes.shape({
info: PropTypes.string,
status: PropTypes.bool,
start_time: PropTypes.objectOf(Date),
query: PropTypes.string,
row_affected: PropTypes.number,
total_time: PropTypes.string,
message: PropTypes.string,
query_source: PropTypes.object,
is_pgadmin_query: PropTypes.bool,
});
HistoryEntry.propTypes = {
entry: EntryPropType,
formatEntryDate: PropTypes.func,
itemKey: PropTypes.string,
selectedItemKey: PropTypes.string,
onClick: PropTypes.func,
};
function QueryHistoryDetails({entry}) {
const classes = useStyles();
const [copyText, setCopyText] = React.useState(gettext('Copy'));
const eventBus = React.useContext(QueryToolEventsContext);
const revertCopiedText = useDelayedCaller(()=>{
setCopyText(gettext('Copy'));
});
const onCopyClick = React.useCallback(()=>{
clipboard.copyToClipboard(entry.query);
setCopyText(gettext('Copied!'));
revertCopiedText(1500);
}, [entry]);
const onCopyToEditor = React.useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.COPY_TO_EDITOR, entry.query);
}, [entry]);
if(!entry) {
return <></>;
}
return (
<>
{entry.info && <Box className={classes.infoHeader}>{entry.info}</Box>}
<Box padding="0.5rem">
<Grid container>
<Grid item sm={4}>{entry.start_time.toLocaleDateString() + ' ' + entry.start_time.toLocaleTimeString()}</Grid>
<Grid item sm={4}>{entry?.row_affected > 0 && entry.row_affected}</Grid>
<Grid item sm={4}>{entry.total_time}</Grid>
</Grid>
<Grid container>
<Grid item sm={4}>{gettext('Date')}</Grid>
<Grid item sm={4}>{gettext('Rows affected')}</Grid>
<Grid item sm={4}>{gettext('Duration')}</Grid>
</Grid>
<Box className={classes.detailsQuery}>
<DefaultButton size="xs" className={classes.copyBtn} onClick={onCopyClick}>{copyText}</DefaultButton>
<DefaultButton size="xs" className={classes.copyBtn} onClick={onCopyToEditor}>{gettext('Copy to Query Editor')}</DefaultButton>
<CodeMirror
value={entry.query}
options={{
foldGutter: false,
lineNumbers: false,
gutters: [],
readOnly: true,
}}
/>
</Box>
<Box marginTop="0.5rem">
<Box>{gettext('Messages')}</Box>
<Box className={classes.fontSourceCode} fontSize="13px" whiteSpace="pre-wrap">{entry.message}</Box>
</Box>
</Box>
</>
);
}
QueryHistoryDetails.propTypes = {
entry: EntryPropType,
};
export function QueryHistory() {
const qhu = React.useRef(new QueryHistoryUtils());
const queryToolCtx = React.useContext(QueryToolContext);
const classes = useStyles();
const eventBus = React.useContext(QueryToolEventsContext);
const [selectedItemKey, setSelectedItemKey] = React.useState(1);
const [showInternal, setShowInternal] = React.useState(true);
const [loaderText, setLoaderText] = React.useState('');
const [,refresh] = React.useState({});
const selectedEntry = qhu.current.getEntry(selectedItemKey);
const layoutEvenBus = React.useContext(LayoutEventsContext);
const listRef = React.useRef();
React.useEffect(async ()=>{
layoutEvenBus.registerListener(LAYOUT_EVENTS.ACTIVE, (currentTabId)=>{
currentTabId == PANELS.HISTORY && listRef.current.focus();
});
setLoaderText(gettext('Fetching history...'));
try {
let {data: respData} = await queryToolCtx.api.get(url_for('sqleditor.get_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}));
respData.data.result.forEach((h)=>{
h = JSON.parse(h);
h.start_time_orig = h.start_time;
h.start_time = new Date(h.start_time);
qhu.current.addEntry(h);
});
setSelectedItemKey(qhu.current.getNextItemKey());
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to fetch query history.') + parseApiError(error));
}
setLoaderText('');
const pushHistory = (h)=>{
qhu.current.addEntry(h);
refresh({});
};
listRef.current.focus();
eventBus.registerListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.PUSH_HISTORY, pushHistory);
}, []);
const onRemove = async ()=>{
setLoaderText(gettext('Removing history entry...'));
try {
await queryToolCtx.api.delete(url_for('sqleditor.clear_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}), {
data: {
query: selectedEntry.query,
start_time: selectedEntry.start_time,
}
});
setSelectedItemKey(qhu.current.clear(selectedItemKey));
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to remove query history.') + parseApiError(error));
}
setLoaderText('');
};
const onRemoveAll = React.useCallback(()=>{
queryToolCtx.modal.confirm(gettext('Clear history'),
gettext('Are you sure you wish to clear the history?') + '</br>' +
gettext('This will remove all of your query history from this and other sessions for this database.'),
async function() {
setLoaderText(gettext('Removing history...'));
try {
await queryToolCtx.api.delete(url_for('sqleditor.clear_query_history', {
'trans_id': queryToolCtx.params.trans_id,
}));
qhu.current.clear();
setSelectedItemKey(null);
} catch (error) {
console.error(error);
Notifier.error(gettext('Failed to remove query history.') + parseApiError(error));
}
setLoaderText('');
},
function() {
return true;
}
);
}, []);
const onKeyPressed = (e) => {
if (e.keyCode == '38') {
e.preventDefault();
setSelectedItemKey(qhu.current.getPrevItemKey(selectedItemKey));
} else if (e.keyCode == '40') {
e.preventDefault();
setSelectedItemKey(qhu.current.getNextItemKey(selectedItemKey));
}
};
return (
<>
<Loader message={loaderText} />
{React.useMemo(()=>(
<Box display="flex" height="100%">
<Box flexBasis="50%" maxWidth="50%" className={classes.leftRoot}>
<Box padding="0.25rem" display="flex">
{gettext('Show queries generated internally by pgAdmin?')}
<InputSwitch value={showInternal} onChange={(e)=>{
setShowInternal(e.target.checked);
qhu.current.showInternal = e.target.checked;
setSelectedItemKey(qhu.current.getNextItemKey());
}} />
<Box marginLeft="auto">
<DefaultButton size="small" disabled={!selectedItemKey} onClick={onRemove}>Remove</DefaultButton>
<DefaultButton size="small" disabled={!qhu.current?.getGroups()?.length}
className={classes.removeBtnMargin} onClick={onRemoveAll}>Remove All</DefaultButton>
</Box>
</Box>
<Box flexGrow="1" overflow="auto" className={classes.listRoot}>
<List innerRef={listRef} className={classes.root} subheader={<li />} tabIndex="0" onKeyDown={onKeyPressed}>
{qhu.current.getGroups().map(([groupKey, groupHeader]) => (
<ListItem key={`section-${groupKey}`} className={classes.removePadding}>
<List className={classes.removePadding}>
<ListSubheader className={classes.listSubheader}>{groupHeader}</ListSubheader>
{qhu.current.getGroupEntries(groupKey).map((entry) => (
<HistoryEntry key={entry.itemKey} entry={entry} formatEntryDate={qhu.current.formatEntryDate}
itemKey={entry.itemKey} selectedItemKey={selectedItemKey} onClick={()=>{setSelectedItemKey(entry.itemKey);}}/>
))}
</List>
</ListItem>
))}
</List>
</Box>
</Box>
<Box flexBasis="50%" maxWidth="50%" overflow="auto">
<QueryHistoryDetails entry={selectedEntry}/>
</Box>
</Box>
), [selectedItemKey, showInternal, qhu.current.size()])}
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useContext, useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import PlaylistAddRoundedIcon from '@material-ui/icons/PlaylistAddRounded';
import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
import { PasteIcon, SaveDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import GetAppRoundedIcon from '@material-ui/icons/GetAppRounded';
import {QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuItem } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import {shortcut_key} from 'sources/keyboard_shortcuts';
import CopyData from '../QueryToolDataGrid/CopyData';
import PropTypes from 'prop-types';
import CustomPropTypes from '../../../../../../static/js/custom_prop_types';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
...theme.mixins.panelBorder.bottom,
},
}));
export function ResultSetToolbar({containerRef, canEdit}) {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const queryToolCtx = useContext(QueryToolContext);
const [buttonsDisabled, setButtonsDisabled] = useState({
'save-data': true,
'delete-rows': true,
'copy-rows': true,
});
const [menuOpenId, setMenuOpenId] = React.useState(null);
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
/* Menu button refs */
const copyMenuRef = React.useRef(null);
const queryToolPref = queryToolCtx.preferences.sqleditor;
const setDisableButton = useCallback((name, disable=true)=>{
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const saveData = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_DATA);
}, []);
const deleteRows = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_DELETE_ROWS);
}, []);
const pasteRows = useCallback(async ()=>{
let copyUtils = new CopyData({
quoting: queryToolPref.results_grid_quoting,
quote_char: queryToolPref.results_grid_quote_char,
field_separator: queryToolPref.results_grid_field_separator,
});
let copiedRows = copyUtils.getCopiedRows();
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, copiedRows);
}, [queryToolPref]);
const copyData = ()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.COPY_DATA, checkedMenuItems['copy_with_headers']);
};
const addRow = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_ADD_ROWS, [[]]);
}, []);
const downloadResult = useCallback(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS);
}, []);
const openMenu = useCallback((e)=>{
setMenuOpenId(e.currentTarget.name);
}, []);
const handleMenuClose = useCallback(()=>{
setMenuOpenId(null);
}, []);
const checkMenuClick = useCallback((e)=>{
setCheckedMenuItems((prev)=>{
let newVal = !prev[e.value];
return {
...prev,
[e.value]: newVal,
};
});
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.DATAGRID_CHANGED, (isDirty)=>{
setDisableButton('save-data', !isDirty);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows, cols)=>{
setDisableButton('delete-rows', !rows);
setDisableButton('copy-rows', (!rows && !cols));
});
}, []);
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA, copyData);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA, copyData);
}, [checkedMenuItems['copy_with_headers']]);
useKeyboardShortcuts([
{
shortcut: queryToolPref.save_data,
options: {
callback: ()=>{saveData();}
}
},
{
shortcut: queryToolPref.download_results,
options: {
callback: (e)=>{e.preventDefault(); downloadResult();}
}
},
], containerRef);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Add row')} icon={<PlaylistAddRoundedIcon style={{height: 'unset'}}/>}
accesskey={shortcut_key(queryToolPref.btn_add_row)} disabled={!canEdit} onClick={addRow} />
<PgIconButton title={gettext('Copy')} icon={<FileCopyRoundedIcon />}
accesskey={shortcut_key(queryToolPref.btn_copy_row)} 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 />}
accesskey={shortcut_key(queryToolPref.btn_paste_row)} disabled={!canEdit} onClick={pasteRows} />
<PgIconButton title={gettext('Delete')} icon={<DeleteRoundedIcon />}
accesskey={shortcut_key(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}/>
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={copyMenuRef}
open={menuOpenId=='menu-copyheader'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="copy_with_headers" checked={checkedMenuItems['copy_with_headers']} onClick={checkMenuClick}>Copy with headers</PgMenuItem>
</PgMenu>
</>
);
}
ResultSetToolbar.propTypes = {
containerRef: CustomPropTypes.ref,
canEdit: PropTypes.bool,
};

View File

@@ -0,0 +1,117 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState, useContext } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import clsx from 'clsx';
import _ from 'lodash';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import { useStopwatch } from '../../../../../../static/js/custom_hooks';
import { QueryToolEventsContext } from '../QueryToolComponent';
const useStyles = makeStyles((theme)=>({
root: {
display: 'flex',
alignItems: 'center',
...theme.mixins.panelBorder.top,
flexWrap: 'wrap',
backgroundColor: theme.otherVars.editorToolbarBg,
userSelect: 'text',
},
padding: {
padding: '2px 12px',
},
divider: {
...theme.mixins.panelBorder.right,
},
mlAuto: {
marginLeft: 'auto',
}
}));
export function StatusBar() {
const classes = useStyles();
const eventBus = useContext(QueryToolEventsContext);
const [position, setPosition] = useState([1, 1]);
const [lastTaskText, setLastTaskText] = useState(null);
const [rowsCount, setRowsCount] = useState([0, 0]);
const [selectedRowsCount, setSelectedRowsCount] = useState(0);
const [dataRowChangeCounts, setDataRowChangeCounts] = useState({
isDirty: false,
added: 0,
updated: 0,
deleted: 0,
});
const {seconds, minutes, hours, msec, start:startTimer, pause:pauseTimer, reset:resetTimer} = useStopwatch({});
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.CURSOR_ACTIVITY, (newPos)=>{
setPosition(newPos||[1, 1]);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.EXECUTION_END, ()=>{
pauseTimer();
setLastTaskText('Query complete');
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TASK_START, (taskText, startTime)=>{
resetTimer();
startTimer(startTime);
setLastTaskText(taskText);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.TASK_END, (taskText, endTime)=>{
pauseTimer(endTime);
setLastTaskText(taskText);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.ROWS_FETCHED, (fetched, total)=>{
setRowsCount([fetched||0, total||0]);
});
eventBus.registerListener(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CHANGED, (rows)=>{
setSelectedRowsCount(rows);
});
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,
});
});
}, []);
let stagedText = '';
if(dataRowChangeCounts.added > 0) {
stagedText += ` Added: ${dataRowChangeCounts.added};`;
}
if(dataRowChangeCounts.updated > 0) {
stagedText += ` Updated: ${dataRowChangeCounts.updated};`;
}
if(dataRowChangeCounts.deleted > 0) {
stagedText += ` Deleted: ${dataRowChangeCounts.deleted};`;
}
return (
<Box className={classes.root}>
<Box className={clsx(classes.padding, classes.divider)}>Total rows: {rowsCount[0]} of {rowsCount[1]}</Box>
{lastTaskText &&
<Box className={clsx(classes.padding, classes.divider)}>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
}
{!lastTaskText && !_.isNull(lastTaskText) &&
<Box className={clsx(classes.padding, classes.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={clsx(classes.padding, classes.divider)}>Rows selected: {selectedRowsCount}</Box>}
{stagedText &&
<Box className={clsx(classes.padding, classes.divider)}>
<span>Changes staged: {stagedText}</span>
</Box>
}
<Box className={clsx(classes.padding, classes.mlAuto)}>Ln {position[0]}, Col {position[1]}</Box>
</Box>
);
}

View File

@@ -0,0 +1,26 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import SQLEditor from './SQLEditorModule';
/* eslint-disable */
/* This is used to change publicPath of webpack at runtime for loading chunks */
/* Do not add let, var, const to this variable */
__webpack_public_path__ = window.resourceBasePath;
/* eslint-enable */
if(!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.SQLEditor = SQLEditor.getInstance(pgAdmin, pgBrowser);
module.exports = {
SQLEditor: SQLEditor,
};

View File

@@ -0,0 +1,138 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from '../../../../static/js/gettext';
import url_for from '../../../../static/js/url_for';
import {getPanelTitle} from './sqleditor_title';
import {getRandomInt} from 'sources/utils';
import $ from 'jquery';
import Notify from '../../../../static/js/helpers/Notifier';
function hasDatabaseInformation(parentData) {
return parentData.database;
}
export function generateUrl(trans_id, parentData, sqlId) {
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': trans_id,
});
url_endpoint += `?is_query_tool=${true}`
+`&sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`;
if (hasDatabaseInformation(parentData)) {
url_endpoint += `&did=${parentData.database._id}`;
if(parentData.database.label) {
url_endpoint += `&database_name=${parentData.database.label}`;
}
}
if(sqlId) {
url_endpoint += `&sql_id=${sqlId}`;
}
return url_endpoint;
}
function hasServerInformations(parentData) {
return parentData.server === undefined;
}
function generateTitle(pgBrowser, aciTreeIdentifier) {
return getPanelTitle(pgBrowser, aciTreeIdentifier);
}
export function showQueryTool(queryToolMod, pgBrowser, url, aciTreeIdentifier, transId) {
const sURL = url || '';
const queryToolTitle = generateTitle(pgBrowser, aciTreeIdentifier);
const currentNode = pgBrowser.tree.findNodeByDomElement(aciTreeIdentifier);
if (currentNode === undefined) {
Notify.alert(
gettext('Query Tool Error'),
gettext('No object selected.')
);
return;
}
const parentData = pgBrowser.tree.getTreeNodeHierarchy(currentNode);
if (hasServerInformations(parentData)) {
return;
}
const gridUrl = generateUrl(transId, parentData);
launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, sURL);
}
export function generateScript(parentData, queryToolMod) {
const queryToolTitle = `${parentData.database}/${parentData.user}@${parentData.server}`;
const transId = getRandomInt(1, 9999999);
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': transId,
});
url_endpoint += `?is_query_tool=${true}`
+`&sgid=${parentData.sgid}`
+`&sid=${parentData.sid}`
+`&server_type=${parentData.stype}`
+`&did=${parentData.did}`
+`&sql_id=${parentData.sql_id}`;
launchQueryTool(queryToolMod, transId, url_endpoint, queryToolTitle, '');
}
export function showERDSqlTool(parentData, erdSqlId, queryToolTitle, queryToolMod) {
const transId = getRandomInt(1, 9999999);
parentData = {
server_group: {
_id: parentData.sgid,
},
server: {
_id: parentData.sid,
server_type: parentData.stype,
},
database: {
_id: parentData.did,
},
};
const gridUrl = generateUrl(transId, parentData, erdSqlId);
launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, '');
}
export function launchQueryTool(queryToolMod, transId, gridUrl, queryToolTitle, sURL) {
let retVal = queryToolMod.launch(transId, gridUrl, true, queryToolTitle, sURL);
if(!retVal) {
Notify.alert(
gettext('Query tool launch error'),
gettext(
'Please allow pop-ups for this site to perform the desired action. If the main window of pgAdmin is closed then close this window and open a new pgAdmin session.'
)
);
}
}
export function _set_dynamic_tab(pgBrowser, value){
var sqleditor_panels = pgBrowser.docker.findPanels('frm_sqleditor');
const process = panel => {
if(value) {
$('#' + panel.$title.index() + ' div:first').addClass('wcPanelTab-dynamic');
} else {
$('#' + panel.$title.index() + ' div:first').removeClass('wcPanelTab-dynamic');
}
};
sqleditor_panels.forEach(process);
var debugger_panels = pgBrowser.docker.findPanels('frm_debugger');
debugger_panels.forEach(process);
}

View File

@@ -0,0 +1,335 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from '../../../../static/js/gettext';
import url_for from '../../../../static/js/url_for';
import {getDatabaseLabel, generateTitle} from './sqleditor_title';
import CodeMirror from 'bundled_codemirror';
import * as SqlEditorUtils from 'sources/sqleditor_utils';
import $ from 'jquery';
import _ from 'underscore';
import Notify from '../../../../static/js/helpers/Notifier';
export function showViewData(
queryToolMod,
pgBrowser,
alertify,
connectionData,
treeIdentifier,
transId,
filter=false,
preferences=null
) {
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
if (node === undefined || !node.getData()) {
Notify.alert(
gettext('Data Grid Error'),
gettext('No object selected.')
);
return;
}
const parentData = pgBrowser.tree.getTreeNodeHierarchy( treeIdentifier
);
if (hasServerOrDatabaseConfiguration(parentData)
|| !hasSchemaOrCatalogOrViewInformation(parentData)) {
return;
}
let applicable_nodes = ['table', 'partition', 'view', 'mview', 'foreign_table', 'catalog_object'];
if (applicable_nodes.indexOf(node.getData()._type) === -1) {
return;
}
const gridUrl = generateUrl(transId, connectionData, node.getData(), parentData);
const queryToolTitle = generateViewDataTitle(pgBrowser, treeIdentifier);
if(filter) {
initFilterDialog(alertify, pgBrowser);
const validateUrl = generateFilterValidateUrl(node.getData(), parentData);
let okCallback = function(sql) {
queryToolMod.launch(transId, gridUrl, false, queryToolTitle, null, sql);
};
$.get(url_for('sqleditor.filter'),
function(data) {
alertify.filterDialog(gettext('Data Filter - %s', queryToolTitle), data, validateUrl, preferences, okCallback)
.resizeTo(pgBrowser.stdW.sm,pgBrowser.stdH.sm);
}
);
} else {
queryToolMod.launch(transId, gridUrl, false, queryToolTitle);
}
}
export function retrieveNameSpaceName(parentData) {
if(!parentData) {
return null;
}
else if (parentData.schema !== undefined) {
return parentData.schema.label;
}
else if (parentData.view !== undefined) {
return parentData.view.label;
}
else if (parentData.catalog !== undefined) {
return parentData.catalog.label;
}
return '';
}
export function retrieveNodeName(parentData) {
if(!parentData) {
return null;
}
else if (parentData.table !== undefined) {
return parentData.table.label;
}
else if (parentData.view !== undefined) {
return parentData.view.label;
}
else if (parentData.catalog !== undefined) {
return parentData.catalog.label;
}
return '';
}
function generateUrl(trans_id, connectionData, nodeData, parentData) {
let url_endpoint = url_for('sqleditor.panel', {
'trans_id': trans_id,
});
url_endpoint += `?is_query_tool=${false}`
+`&cmd_type=${connectionData.mnuid}`
+`&obj_type=${nodeData._type}`
+`&obj_id=${nodeData._id}`
+`&sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`
+`&did=${parentData.database._id}`
+`&server_type=${parentData.server.server_type}`;
return url_endpoint;
}
function generateFilterValidateUrl(nodeData, parentData) {
// Create url to validate the SQL filter
var url_params = {
'sid': parentData.server._id,
'did': parentData.database._id,
'obj_id': nodeData._id,
};
return url_for('sqleditor.filter_validate', url_params);
}
function initFilterDialog(alertify, pgBrowser) {
// Create filter dialog using alertify
let filter_editor = null;
if (!alertify.filterDialog) {
alertify.dialog('filterDialog', function factory() {
return {
main: function(title, message, validateUrl, preferences, okCallback) {
this.set('title', title);
this.message = message;
this.validateUrl = validateUrl;
this.okCallback = okCallback;
this.preferences = preferences;
},
setup:function() {
return {
buttons:[{
text: '',
key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs: {
name: 'dialog_help',
type: 'button',
label: gettext('Data Filter'),
'aria-label': gettext('Help'),
url: url_for('help.static', {
'filename': 'viewdata_filter.html',
}),
},
},{
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-times pg-alertify-button',
},{
text: gettext('OK'),
key: null,
className: 'btn btn-primary fa fa-check pg-alertify-button',
}],
options: {
modal: 0,
resizable: true,
maximizable: false,
pinnable: false,
autoReset: false,
},
};
},
build: function() {
var that = this;
alertify.pgDialogBuild.apply(that);
// Set the tooltip of OK
$(that.__internal.buttons[2].element).attr('title', gettext('Use SHIFT + ENTER to apply filter...'));
// For sort/filter dialog we capture the keypress event
// and on "shift + enter" we clicked on "OK" button.
$(that.elements.body).on('keypress', function(evt) {
if (evt.shiftKey && evt.keyCode == 13) {
that.__internal.buttons[2].element.click();
}
});
},
prepare:function() {
var that = this,
$content = $(this.message),
$sql_filter = $content.find('#sql_filter');
$(this.elements.header).attr('data-title', this.get('title'));
$(this.elements.body.childNodes[0]).addClass(
'dataview_filter_dialog'
);
this.setContent($content.get(0));
// Disable OK button
that.__internal.buttons[2].element.disabled = true;
// Apply CodeMirror to filter text area.
filter_editor = this.filter_obj = CodeMirror.fromTextArea($sql_filter.get(0), {
lineNumbers: true,
mode: 'text/x-pgsql',
extraKeys: pgBrowser.editor_shortcut_keys,
indentWithTabs: !that.preferences.use_spaces,
indentUnit: that.preferences.tab_size,
tabSize: that.preferences.tab_size,
lineWrapping: that.preferences.wrap_code,
autoCloseBrackets: that.preferences.insert_pair_brackets,
matchBrackets: that.preferences.brace_matching,
screenReaderLabel: gettext('Filter SQL'),
});
let sql_font_size = SqlEditorUtils.calcFontSize(that.preferences.sql_font_size);
$(this.filter_obj.getWrapperElement()).css('font-size', sql_font_size);
setTimeout(function() {
// Set focus on editor
that.filter_obj.refresh();
that.filter_obj.focus();
}, 500);
that.filter_obj.on('change', function() {
if (that.filter_obj.getValue() !== '') {
that.__internal.buttons[2].element.disabled = false;
} else {
that.__internal.buttons[2].element.disabled = true;
}
});
},
callback: function(closeEvent) {
if (closeEvent.button.text == gettext('OK')) {
var sql = this.filter_obj.getValue();
var that = this;
closeEvent.cancel = true; // Do not close dialog
// Make ajax call to include the filter by selection
$.ajax({
url: that.validateUrl,
method: 'POST',
async: false,
contentType: 'application/json',
data: JSON.stringify(sql),
})
.done(function(res) {
if (res.data.status) {
that.okCallback(sql);
that.close(); // Close the dialog
}
else {
Notify.alert(
gettext('Validation Error'),
gettext(res.data.result),
function(){
filter_editor.focus();
},
);
}
})
.fail(function(e) {
if (e.status === 410){
pgBrowser.report_error(gettext('Error filtering rows - %s.', e.statusText), e.responseJSON.errormsg);
} else {
Notify.alert(
gettext('Validation Error'),
e
);
}
});
} else if(closeEvent.index == 0) {
/* help Button */
closeEvent.cancel = true;
pgBrowser.showHelp(
closeEvent.button.element.name,
closeEvent.button.element.getAttribute('url'),
null, null
);
return;
}
},
};
});
}
}
function hasServerOrDatabaseConfiguration(parentData) {
return parentData.server === undefined || parentData.database === undefined;
}
function hasSchemaOrCatalogOrViewInformation(parentData) {
return parentData.schema !== undefined || parentData.view !== undefined ||
parentData.catalog !== undefined;
}
export function generateViewDataTitle(pgBrowser, treeIdentifier, custom_title=null, backend_entity=null) {
var preferences = pgBrowser.get_preferences_for_module('browser');
const parentData = pgBrowser.tree.getTreeNodeHierarchy(
treeIdentifier
);
const namespaceName = retrieveNameSpaceName(parentData);
const db_label = !_.isUndefined(backend_entity) && backend_entity != null && backend_entity.hasOwnProperty('db_name') ? backend_entity['db_name'] : getDatabaseLabel(parentData);
const node = pgBrowser.tree.findNodeByDomElement(treeIdentifier);
var dtg_title_placeholder = '';
if(custom_title) {
dtg_title_placeholder = custom_title;
} else {
dtg_title_placeholder = preferences['vw_edt_tab_title_placeholder'];
}
var title_data = {
'database': db_label,
'username': parentData.server.user.name,
'server': parentData.server.label,
'schema': namespaceName,
'table': node.getData().label,
'type': 'view_data',
};
return generateTitle(dtg_title_placeholder, title_data);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import { retrieveNameSpaceName, retrieveNodeName } from './show_view_data';
const pgAdmin = pgWindow.pgAdmin;
export function getDatabaseLabel(parentData) {
return parentData.database ? parentData.database.label
: parentData.server.db;
}
function isServerInformationAvailable(parentData) {
return parentData.server === undefined;
}
export function getTitle(pgAdmin, browserPref, parentData=null, isConnTitle=false, server=null, database=null, username=null, isQueryTool=true) {
let titleTemplate = isQueryTool ? pgAdmin['qt_default_placeholder'] : pgAdmin['vw_edt_default_placeholder'];
if (!isConnTitle) {
if(!isQueryTool) {
titleTemplate = browserPref['vw_edt_tab_title_placeholder'] ?? pgAdmin['qt_default_placeholder'];
} else {
titleTemplate = browserPref['qt_tab_title_placeholder'] ?? pgAdmin['vw_edt_default_placeholder'];
}
}
return generateTitle(titleTemplate, {
'database': database,
'username': username,
'server': server,
'schema': retrieveNameSpaceName(parentData),
'table': retrieveNodeName(parentData),
'type': isQueryTool ? 'query_tool' : 'view_data',
});
}
export function getPanelTitle(pgBrowser, selected_item=null, custom_title=null, parentData=null, conn_title=false, db_label=null) {
var preferences = pgBrowser.get_preferences_for_module('browser');
if(selected_item == null && parentData == null) {
selected_item = pgBrowser.tree.selected();
}
if(parentData == null) {
parentData = pgBrowser.tree.getTreeNodeHierarchy(selected_item);
if(parentData == null) return;
if (isServerInformationAvailable(parentData)) {
return;
}
}
if(!db_label) {
db_label = getDatabaseLabel(parentData);
}
var qt_title_placeholder = '';
if (!conn_title) {
if (custom_title) {
qt_title_placeholder = custom_title;
} else {
qt_title_placeholder = preferences['qt_tab_title_placeholder'];
}
} else {
qt_title_placeholder = pgAdmin['qt_default_placeholder'];
}
var title_data = {
'database': db_label,
'username': parentData.server.user.name,
'server': parentData.server.label,
'type': 'query_tool',
};
return generateTitle(qt_title_placeholder, title_data);
}
export function setQueryToolDockerTitle(panel, is_query_tool, panel_title, is_file) {
let panel_icon = '', panel_tooltip = '';
// Enable/ Disabled the rename panel option if file is open.
set_renamable_option(panel, is_file);
if(is_file || is_file == 'true'){
panel_tooltip = gettext('File - ') + panel_title;
panel_icon = 'fa fa-file-alt';
}
else if (is_query_tool == 'false' || !is_query_tool) {
// Edit grid titles
panel_tooltip = gettext('View/Edit Data - ') + panel_title;
panel_icon = 'pg-font-icon icon-view_data';
} else {
// Query tool titles
panel_tooltip = gettext('Query Tool - ') + panel_title;
panel_icon = 'pg-font-icon icon-query_tool';
}
panel.title('<span title="'+ _.escape(panel_tooltip) +'">'+ _.escape(panel_title) +'</span>');
panel.icon(panel_icon);
}
export function set_renamable_option(panel, is_file) {
if(is_file || is_file == 'true') {
panel?.renamable(false);
} else {
panel?.renamable(true);
}
}
export function generateTitle(title_placeholder, title_data) {
if(title_data.type == 'query_tool' || title_data.type == 'psql_tool') {
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
title_placeholder = title_placeholder.replace('%USERNAME%', _.unescape(title_data.username));
title_placeholder = title_placeholder.replace('%SERVER%', _.unescape(title_data.server));
} else if(title_data.type == 'view_data') {
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
title_placeholder = title_placeholder.replace('%USERNAME%', _.unescape(title_data.username));
title_placeholder = title_placeholder.replace('%SERVER%', _.unescape(title_data.server));
title_placeholder = title_placeholder.replace('%SCHEMA%', _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace('%TABLE%', _.unescape(title_data.table));
} else if(title_data.type == 'debugger') {
title_placeholder = title_placeholder.replace('%FUNCTION%', _.unescape(title_data.function_name));
title_placeholder = title_placeholder.replace('%ARGS%', _.unescape(title_data.args));
title_placeholder = title_placeholder.replace('%SCHEMA%', _.unescape(title_data.schema));
title_placeholder = title_placeholder.replace('%DATABASE%', _.unescape(title_data.database));
}
return _.escape(title_placeholder);
}
/*
* This function is used refresh the db node after showing alert to the user
*/
export function refresh_db_node(message, dbNode) {
Alertify.alert()
.setting({
'title': gettext('Database moved/renamed'),
'label':gettext('OK'),
'message': gettext(message),
'onok': function(){
//Set the original db name as soon as user clicks ok button
pgAdmin.Browser.Nodes.database.callbacks.refresh(undefined, dbNode);
},
}).show();
}

View File

@@ -1,254 +0,0 @@
.query-history {
overflow: auto;
.list-item {
border-bottom: $panel-border;
background-color: $color-bg;
}
.entry {
font-family: $font-family-editor;
border: $panel-border-width solid transparent;
margin-left: 1px;
padding: 0 5px;
.other-info {
@extend .text-12;
color: $text-muted;
font-family: $font-family-editor;
display: flex;
flex-direction: row;
justify-content: space-between;
.timestamp {
align-self: flex-start;
}
}
.query {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
user-select: initial;
.query-history-icon {
width: 18px;
text-align: center;
}
}
}
.date-label {
font-family: $font-family-editor;
background: $color-gray-lighter;
padding: 2px 9px;
font-size: 11px;
font-weight: bold;
color: $color-gray;
border-bottom: $panel-border;
}
.entry.error {
background: $sql-history-error-bg;
}
.selected {
& .entry.error {
background-color: $sql-history-error-bg;
}
& .entry {
color: $sql-history-success-fg;
border: $table-hover-border;
background: $sql-history-success-bg;
font-weight: bold;
.other-info {
color: $sql-history-success-fg;
font-weight: bold;
}
}
}
}
#history-detail-query .CodeMirror {
border: $panel-border;
background-color: $sql-history-detail-bg;
width: 100%;
padding-left: 5px;
position: absolute;
}
.header-label {
@extend .text-12;
display: block;
color: $color-fg;
}
.sql-editor-history-container {
height: 100%;
overflow: hidden;
background-color: $negative-bg;
}
.query-detail {
height: 100%;
width: 100%;
min-height: 19em;
overflow: auto;
display: flex;
flex-direction: column;
background-color: $color-bg;
.error-message-block {
background: $sql-history-error-bg;
flex: 0.3;
padding-left: 20px;
.history-error-text {
@extend .text-12;
padding: 7px 0;
span {
color:$sql-history-error-fg;
font-weight: 500;
margin-right: 8px;
}
}
}
.info-message-block {
background: $sql-history-detail-bg;
flex: 0.3;
padding-left: 20px;
.history-info-text {
@extend .text-12;
padding: 7px 0;
}
}
.metadata-block {
flex: 0.4;
padding: 10px 20px;
.metadata {
display: flex;
flex-wrap: wrap;
.item {
flex: 1;
min-width: 130px;
.value {
@extend .text-14;
display: block;
}
.description {
@extend .header-label;
}
}
}
}
.query-statement-block {
flex: 5;
margin-left: 10px;
margin-right: 10px;
min-height: 4em;
position: relative;
.copy-all, .was-copied, .copy-to-editor {
float: left;
position: relative;
z-index: 10;
border: 1px solid $border-color;
color: $color-fg;
font-size: 12px;
box-shadow: 1px 2px 4px 0px $color-gray-light;
padding: 3px 12px 3px 10px;
font-weight: 500;
min-width: 75px;
}
.copy-all, .copy-to-editor {
background-color: $color-bg;
}
.was-copied {
background-color: $color-primary-light;
border-color: $color-primary-light;
color: $btn-copied-color-fg;
}
.CodeMirror-scroll {
padding-top: 25px;
}
}
.block-divider {
margin-top: 11px;
margin-bottom: 8px;
}
.message-block {
flex: 2;
display: flex;
padding: 0 20px;
min-height: 6em;
.message {
flex: 2 2 0%;
flex-direction: column;
display: flex;
.message-header {
@extend .header-label;
@extend .not-selectable;
flex: 0 0 auto;
}
.content {
flex: 0 1 auto;
overflow: auto;
position: relative;
height: 100%;
.content-value {
@extend .bg-white;
@extend .text-13;
font-family: $font-family-editor;
color: $color-fg;
border: 0;
padding-left: 0;
position: absolute;
}
}
}
}
}
#history_grid {
.gutter.gutter-horizontal {
width: $panel-border-width;
background: $panel-border-color;
&:hover {
cursor: ew-resize;
}
}
.toggle-and-history-container {
display: flex;
flex-direction: column;
height: 100%;
.query-history-toggle {
padding-top: 4px;
padding-bottom: 4px;
}
}
}

View File

@@ -1,431 +0,0 @@
.filter-title {
background-color: $color-primary;
padding: 2px;
color: $color-primary-fg;
font-size: 13px;
}
.sql-icon-lg {
font-size: 0.875rem;
line-height: 1.3;
}
.sql_textarea {
height: 100%;
}
.sql_textarea .CodeMirror-scroll {
z-index: 0;
}
.data-output-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
overflow: hidden;
}
.sql-editor-busy-text-status {
position: absolute;
padding: 1rem 1.5rem;
bottom: 0;
right: 0;
opacity: 0.8;
background-color: $color-warning-light;
color: $color-warning-fg;
}
.connection_status {
background-color: $sql-title-bg;
color: $sql-title-fg;
border-right: $border-width solid $border-color;
}
.editor-title {
padding: $sql-title-padding;
background: $sql-title-bg;
color: $sql-title-fg;
}
.connection-info {
background: $sql-title-bg;
color: $sql-title-fg;
width:100%;
display: inherit;
}
.conn-info-dd {
padding-top: 0.3em;
padding-left: 0.2em;
cursor: pointer;
}
.connection-data {
display: inherit;
cursor: pointer;
width: auto;
}
#editor-panel {
z-index: 0;
position: absolute;
top: $sql-editor-panel-top;
bottom: 0;
left: 0;
right: 0;
}
.ajs-body .warn-icon {
color: $color-warning;
font-size: 2em;
margin-right: 20px;
padding-top: 10px;
}
.connection_status_wrapper {
width: 100%;
border-top: $panel-border;
border-bottom: $panel-border;
}
li.CodeMirror-hint-active {
background: $color-primary-light;
color: $color-primary-fg;
}
.sql-editor .CodeMirror-activeline-background {
background: $color-editor-activeline-light !important;
border: 1px solid $color-editor-activeline-border-color;
}
.filter-container {
position: relative;
background-color: $color-bg;
border: 1px solid $border-color;
padding-bottom: 30px;
top: 10px;
z-index: 1;
margin: auto;
width: 60%;
}
.limit-enabled {
background-color: $color-bg;
}
.CodeMirror-hint {
margin: 0;
padding: 0 4px;
border-radius: 2px;
overflow: hidden;
white-space: pre;
color: $color-fg;
cursor: pointer;
}
.grid-header .ui-icon.ui-state-hover {
background-color: $color-bg;
}
.slick-cell.selected span[data-cell-type="row-header-selector"] {
color: $color-primary-fg;
}
.slick-cell.cell-move-handle {
font-weight: bold;
text-align: right;
border-right: solid $border-color;
background: $color-gray-lighter;
cursor: move;
}
.cell-move-handle:hover {
background: $color-gray-light;
}
.slick-row.selected .cell-move-handle {
background: $color-warning-light;
}
.slick-row.complete {
background-color: $color-success-light;
color: $color-gray-dark;
}
.cell-selection {
border-right-color: $border-color;
border-right-style: solid;
background: $color-gray-lighter;
color: $color-gray;
text-align: right;
font-size: 10px;
}
#datagrid .slick-header .slick-header-columns {
background: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
height: 40px;
border-bottom: $panel-border;
}
#datagrid .slick-header .slick-header-column.ui-state-default {
padding: 4px 0 3px 6px;
border-bottom: $panel-border;
border-right: $panel-border;
}
.slick-row:hover .slick-cell{
border-top: $table-hover-border;
border-bottom: $table-hover-border;
background-color: $table-hover-bg-color;
}
#datagrid .slick-header .slick-header-column.selected {
background-color: $color-primary;
}
.slick-row .slick-cell {
border-bottom: $panel-border;
border-right: $panel-border;
z-index: 0;
}
#datagrid {
background: none;
background-color: $datagrid-bg;
}
.ui-widget-content.slick-row {
&.even, &.odd {
background: none;
background-color: $table-bg;
}
}
/* Remove active cell border */
.slick-cell.active {
border: 1px solid transparent;
border-right: 1px solid $color-gray-light;
border-bottom-color: $color-gray-light;
}
/* To highlight all newly inserted rows */
.grid-canvas .new_row {
background: $color-success-light !important;
}
/* To highlight all the updated rows */
.grid-canvas .updated_row {
background: $color-gray-lighter;
}
/* To highlight row at fault */
.grid-canvas .new_row.error, .grid-canvas .updated_row.error {
background: $color-danger-light !important;
}
/* Disabled row */
.grid-canvas .disabled_row {
background: $color-gray-lighter;
}
/* Disabled cell */
.grid-canvas .disabled_cell {
color: $text-muted;
}
/* Highlighted (modified or new) cell */
.grid-canvas .highlighted_grid_cells {
background: $color-gray-lighter;
font-weight: bold;
}
/* Override selected row color */
#datagrid .slick-row .slick-cell.selected {
background-color: $table-bg-selected;
color: $datagrid-selected-color;
}
/* color the first column */
#datagrid .slick-row {
.slick-cell {
background-color: $sql-grid-data-cell-bg;
color: $sql-grid-data-cell-fg;
}
.slick-cell.l0.r0 {
background-color: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
}
}
#datagrid div.slick-header.ui-state-default {
background-color: $sql-grid-title-cell-bg;
color: $sql-grid-title-cell-fg;
border-bottom: none;
border-right: none;
border-top: none;
}
#datagrid .slick-row .slick-cell.l0.r0.selected {
background-color: $color-primary;
color: $color-primary-fg;
}
#datagrid .slick-row > .slick-cell:not(.l0):not(.r0).selected {
background-color: $table-hover-bg-color;
border-top: $table-hover-border;
border-bottom: $table-hover-border;
}
.pg-text-editor {
z-index:10000;
position:absolute;
background: $color-bg;
padding: 0.25rem;
border: $panel-border;
box-shadow: $dropdown-box-shadow;
& .pg-textarea {
width:250px;
height:80px;
border:0;
outline:0;
resize: both;
}
& .pg-text-invalid {
background: $color-danger-lighter;
}
& #pg-json-editor {
min-width:525px;
min-height:300px;
height:295px;
width:550px;
border: $panel-border;
outline:0;
resize: both;
overflow:auto
}
}
.pg_buttons {
padding-top: 3px;
}
.sql-editor-message {
white-space:pre-wrap;
font-family: $font-family-editor;
padding-top: 5px;
padding-left: 10px;
overflow: auto;
height: 100%;
font-size: 0.925em;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.view-geometry-property-table .td-disabled{
color: $color-gray-lighter;
}
/* For leaflet map background */
.geometry-viewer-container-plain-background {
background: $color-bg;
}
div.strikeout:before {
content: " ";
position: absolute;
top: 50%;
left: 0;
border-top: 1px solid $color-danger;
width: 100%;
font-weight: 900;
}
div.strikeout:after {
content: "\00B7";
font-size: 1px;
font-weight: 900;
}
.sql-scratch {
width: 100%;
height: 100%;
box-sizing: border-box;
overflow-y: hidden;
textarea {
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
resize: none;
}
}
.connection_status .obtaining-conn {
background-image: $loader-icon-small !important;
background-position: center center;
background-repeat: no-repeat;
&:before {
content:'';
}
min-width: 50%;
min-height: 100%;
}
.sql-editor-grid-container {
height: 100%;
overflow: auto;
.ui-widget-content {
background-color: $input-bg;
color: $input-color;
}
.ui-state-default {
color: $color-fg;
}
}
.sql-editor-grid-container.has-no-footer {
height: 100%;
}
.selected-connection {
background-color: $color-primary-light;
}
/* Setting it to hardcoded white as the SVG generated is having white bg
* Need to check what can be done.
*/
/* Css for psql */
.psql_terminal .terminal {
padding-top: 1%;
padding-left: 0.5%;
height: 100%;
}
.psql-icon-style {
font-size: inherit;
padding-left: 0em;
}
.psql-tab-style {
font-size: small;
padding-left: 0em;
}