1) Added support to show all background processes in separate panel. Fixes #3709

2) Port process watcher to React. Fixes #7404
This commit is contained in:
Aditya Toshniwal 2022-08-11 10:49:45 +05:30 committed by Akshay Joshi
parent 271b6d91fc
commit c2b23465cc
100 changed files with 1949 additions and 1638 deletions

View File

@ -203,21 +203,6 @@ command:
* Click the *Cancel* button to exit without saving work.
.. image:: images/backup_messages.png
:alt: Backup success notification popup
:align: center
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *More details* on the popup window to launch the *Process Watcher*. The *Process Watcher* logs all the activity associated with the backup and provides additional information for troubleshooting.
.. image:: images/backup_process_watcher.png
:alt: Backup process watcher
:align: center
If the backup is unsuccessful, you can review the error messages returned by the
backup command on the *Process Watcher*.
.. note:: If you are running *pgAdmin* in *Server Mode* you can click on the |sm_icon| icon in the process watcher window to open the file location in the Storage Manager. You can use the :ref:`Storage Manager <storage_manager>` to download the backup file on the client machine .
.. |sm_icon| image:: images/sm_icon.png
pgAdmin will run the backup process in background. You can view all the background
process with there running status and logs on the :ref:`Processes <processes>`
tab

View File

@ -34,24 +34,6 @@ statements that should be included in the backup.
Click the *Backup* button to build and execute a command based on your
selections; click the *Cancel* button to exit without saving work.
.. image:: images/backup_globals_messages.png
:alt: Backup globals success notification popup
:align: center
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *Click
here for details* on the popup window to launch the *Process Watcher*. The
*Process Watcher* logs all the activity associated with the backup and provides
additional information for troubleshooting.
.. image:: images/backup_globals_process_watcher.png
:alt: Backup globals process watcher
:align: center
If the backup is unsuccessful, review the error message returned by the
*Process Watcher* to resolve any issue.
.. note:: If you are running *pgAdmin* in *Server Mode* you can click on the |sm_icon| icon in the process watcher window to open the file location in the Storage Manager. You can use the :ref:`Storage Manager <storage_manager>` to download the backup file on the client machine .
.. |sm_icon| image:: images/sm_icon.png
pgAdmin will run the backup process in background. You can view all the background
process with there running status and logs on the :ref:`Processes <processes>`
tab

View File

@ -122,24 +122,6 @@ tab to provide options related to data or pgAdmin objects that correspond to *pg
Click the *Backup* button to build and execute a command based on your
selections; click the *Cancel* button to exit without saving work.
.. image:: images/backup_server_messages.png
:alt: Backup server success notification popup
:align: center
Use the **Stop Process** button to stop the Backup process.
If the backup is successful, a popup window will confirm success. Click *Click
here for details* on the popup window to launch the *Process Watcher*. The
*Process Watcher* logs all the activity associated with the backup and provides
additional information for troubleshooting.
.. image:: images/backup_server_process_watcher.png
:alt: Backup server process watcher
:align: center
If the backup is unsuccessful, review the error message returned by the
*Process Watcher* to resolve any issue.
.. note:: If you are running *pgAdmin* in *Server Mode* you can click on the |sm_icon| icon in the process watcher window to open the file location in the Storage Manager. You can use the :ref:`Storage Manager <storage_manager>` to download the backup file on the client machine .
.. |sm_icon| image:: images/sm_icon.png
pgAdmin will run the backup process in background. You can view all the background
process with there running status and logs on the :ref:`Processes <processes>`
tab

View File

@ -89,5 +89,8 @@ button to deploy the instance on Amazon RDS.
Once you click on the finish, one background process will start which will
deploy the instance in the cloud and monitor the progress of the deployment.
You can view all the background process with there running status and logs
on the :ref:`Processes <processes>` tab
The Server will be added to the tree with the cloud deployment icon. Once the
deployment is done, the server details will be updated.

View File

@ -107,6 +107,8 @@ button to deploy the instance on Azure PostgreSQL.
Once you click on the finish, one background process will start which will
deploy the instance in the cloud and monitor the progress of the deployment.
You can view all the background process with there running status and logs
on the :ref:`Processes <processes>` tab
.. image:: images/cloud_azure_bg_process_watcher.png
:alt: Cloud Deployment

View File

@ -87,5 +87,8 @@ button to deploy the instance on EDB BigAnimal.
Once you click on the finish, one background process will start which will
deploy the instance in the cloud and monitor the progress of the deployment.
You can view all the background process with there running status and logs
on the :ref:`Processes <processes>` tab
The Server will be added to the tree with the cloud deployment icon. Once the
deployment is done, the server details will be updated.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -23,6 +23,7 @@ of database objects.
management_basics
backup_and_restore
developer_tools
processes
pgagent
contributions
release_notes

View File

@ -45,18 +45,6 @@ switch to the *No* position; by default, status messages are included.
When you've completed the dialog, click *OK* to start the background process;
to exit the dialog without performing maintenance operations, click *Cancel*.
pgAdmin will inform you when the background process completes:
.. image:: images/maintenance_complete.png
:alt: Maintenance completion notification
:align: center
Use the **Stop Process** button to stop the Maintenance process.
Use the *Click here for details* link on the notification to open the *Process
Watcher* and review detailed information about the execution of the command that
performed the import or export:
.. image:: images/maintenance_pw.png
:alt: Maintenance process watcher
:align: center
pgAdmin will run the maintenance process in background. You can view all the background
process with there running status and logs on the :ref:`Processes <processes>`
tab

49
docs/en_US/processes.rst Normal file
View File

@ -0,0 +1,49 @@
.. _processes:
***********************
`Processes`:index:
***********************
There are certain tasks which pgAdmin runs in the background. The processes
running in the background can be viewed in the processes tab. It shows the
process details of Backup, Restore, Maintenance, Import/Export and Cloud instance
creation.
.. image:: images/processes_main.png
:alt: Processes Tab
:align: center
The columns of the processes table shows:
* The *PID* of the forked OS process.
* The *Type* of the task being performed.
* The *Server* name for which the task is.
* The *Object* can be a database, table, mview or anything which gives more info.
* The *Start Time* of the process, sorted descending by default.
* The current *Status* of the process. It can be Running, Finished, Failed, Terminated.
* The *Time Taken* to complete. It will keep updating if it is running.
There are two action buttons on each row:
* The *Stop Process* button allows you to kill a running process.
* The *More details* button allows you to open the process watcher which shows the
process logs and other details.
You can also select the checkboxes and click on *Delete and Acknowledge* button
on the top to clear the process info and logs.
Process Watcher
*********************
.. image:: images/processes_details.png
:alt: Process Watcher
:align: center
The Process Watcher logs all the activity associated with the process/task and provides
additional information for troubleshooting
Use the **Stop Process** button to stop the Backup process.
.. note:: If you are running *pgAdmin* in *Server Mode* you can click on the |sm_icon| icon in the process watcher window to open the file location in the Storage Manager. You can use the :ref:`Storage Manager <storage_manager>` to download the backup file on the client machine .
.. |sm_icon| image:: images/sm_icon.png

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #3709 <https://redmine.postgresql.org/issues/3709>`_ - Added support to show all background processes in separate panel.
| `Issue #7387 <https://redmine.postgresql.org/issues/7387>`_ - Added support to create triggers from existing trigger functions in EPAS.
Housekeeping
@ -16,6 +17,7 @@ Housekeeping
| `Issue #7344 <https://redmine.postgresql.org/issues/7344>`_ - Port Role Reassign dialog to React.
| `Issue #7345 <https://redmine.postgresql.org/issues/7345>`_ - Port User Management dialog to React.
| `Issue #7404 <https://redmine.postgresql.org/issues/7404>`_ - Port process watcher to React.
| `Issue #7462 <https://redmine.postgresql.org/issues/7462>`_ - Remove the SQL files for the unsupported versions of the database server.
| `Issue #7567 <https://redmine.postgresql.org/issues/7567>`_ - Port About dialog to React.
| `Issue #7568 <https://redmine.postgresql.org/issues/7568>`_ - Port change user password and 2FA dialog to React.

View File

@ -143,17 +143,6 @@ command, click the *Restore* button to start the process, or click the *Cancel*
button to exit without saving your work. A popup will confirm if the restore is
successful.
.. image:: images/restore_messages.png
:alt: Restore dialog notifications
:align: center
Use the **Stop Process** button to stop the Restore process.
Click *Click here for details* on the popup to launch the *Process Watcher*. The
*Process Watcher* logs all the activity associated with the restore, and
provides additional information for troubleshooting should the restore command
encounter problems.
.. image:: images/restore_process_watcher.png
:alt: Restore dialog process watcher
:align: center
pgAdmin will run the restore process in background. You can view all the background
process with there running status and logs on the :ref:`Processes <processes>`
tab

View File

@ -130,6 +130,16 @@ def register_browser_preferences(self):
)
)
self.table_row_count_threshold = self.preference.register(
'processes', 'process_retain_days',
gettext("Process details/logs retention days"), 'integer', 5,
category_label=gettext('Processes'),
help_str=gettext(
'After this many days, the process info and logs '
'will be automatically cleared.'
)
)
fields = [
{'name': 'key', 'type': 'keyCode', 'label': gettext('Key')},
{'name': 'shift', 'type': 'checkbox', 'label': gettext('Shift')},

View File

@ -29,7 +29,7 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error, \
from pgadmin.utils.driver import get_driver
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.tools.schema_diff.compare import SchemaDiffObjectCompare
from pgadmin.utils import html, does_utility_exist
from pgadmin.utils import html, does_utility_exist, get_server
from pgadmin.model import Server
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
@ -158,23 +158,7 @@ class Message(IProcessDesc):
@property
def message(self):
res = gettext("Refresh Materialized View")
opts = []
if not self.data['is_with_data']:
opts.append(gettext("With no data"))
else:
opts.append(gettext("With data"))
if self.data['is_concurrent']:
opts.append(gettext("Concurrently"))
return res + " ({0})".format(', '.join(str(x) for x in opts))
@property
def type_desc(self):
return gettext("Refresh Materialized View")
def details(self, cmd, args):
res = gettext("Refresh Materialized View ({0})")
msg = gettext("Refresh Materialized View ({0})")
opts = []
if not self.data['is_with_data']:
opts.append(gettext("WITH NO DATA"))
@ -184,17 +168,30 @@ class Message(IProcessDesc):
if self.data['is_concurrent']:
opts.append(gettext("CONCURRENTLY"))
res = res.format(', '.join(str(x) for x in opts))
msg = msg.format(', '.join(str(x) for x in opts))
return msg
res = '<div>' + html.safe_str(res)
@property
def type_desc(self):
return gettext("Refresh Materialized View")
res += '</div><div class="py-1">'
res += gettext("Running Query:")
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(self.query)
res += '</div></div>'
def get_server_name(self):
# Fetch the server details like hostname, port, roles etc
s = get_server(self.sid)
return res
if s is None:
return gettext("Not available")
return html.safe_str("{0} ({1}:{2})".format(s.name, s.host, s.port))
def details(self, cmd, args):
return {
"message": self.message,
"query": self.query,
"server": self.get_server_name(),
"object": "{0}/{1}".format(
self.data['database'], self.data.get('object', 'Unknown')),
"type": gettext("Refresh MView")
}
class MViewModule(ViewModule):
@ -2193,6 +2190,8 @@ class MViewNode(ViewNode, VacuumSettings):
is_concurrent=is_concurrent,
with_data=with_data
)
data['object'] = "{0}.{1}".format(res['rows'][0]['schema'],
res['rows'][0]['name'])
# Fetch the server details like hostname, port, roles etc
server = Server.query.filter_by(
@ -2273,6 +2272,7 @@ class MViewNode(ViewNode, VacuumSettings):
return make_json_response(
data={
'job_id': jid,
'desc': p.desc.message,
'status': True,
'info': gettext(
'Materialized view refresh job created.')

View File

@ -203,9 +203,8 @@ define('pgadmin.node.mview', [
})
.done(function(refreshed_res) {
if (refreshed_res.data && refreshed_res.data.status) {
//Do nothing as we are creating the job and exiting from the main dialog
Notify.success(refreshed_res.data.info);
pgBrowser.Events.trigger('pgadmin-bgprocess:created');
//Do nothing as we are creating the job and exiting from the main dialog
pgBrowser.BgProcessManager.startProcess(refreshed_res.data.job_id, refreshed_res.data.desc);
} else {
Notify.alert(
gettext('Failed to create materialized view refresh job.'),

View File

@ -240,7 +240,7 @@ define('pgadmin.node.database', [
if(res.data.info_prefix) {
res.info = `${_.escape(res.data.info_prefix)} - ${res.info}`;
}
Notify.success(_.unescape(res.info));
Notify.success(res.info);
t.removeIcon(i);
data.connected = false;
data.icon = data.isTemplate ? 'icon-database-template-not-connected':'icon-database-not-connected';
@ -437,7 +437,7 @@ define('pgadmin.node.database', [
res.info = gettext('Database already connected.');
}
if(res.data.info_prefix) {
res.info = `${res.data.info_prefix} - ${res.info}`;
res.info = `${_.escape(res.data.info_prefix)} - ${res.info}`;
}
if(res.data.already_connected) {
Notify.info(res.info);

View File

@ -629,7 +629,7 @@ define('pgadmin.node.server', [
var connect_to_server = function(obj, data, tree, item, reconnect) {
// Open properties dialog in edit mode
var server_url = obj.generate_url(item, 'obj', data, true);
let server_url = obj.generate_url(item, 'obj', data, true);
// Fetch the updated data
$.get(server_url)
.done(function(res) {
@ -651,20 +651,7 @@ define('pgadmin.node.server', [
}
}
else if (res.cloud_status == -1) {
$.ajax({
type: 'GET',
timeout: 30000,
url: url_for('cloud.update_cloud_process', {'sid': res.id}),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function() {
pgAdmin.Browser.BackgroundProcessObsorver.update_process_list();
})
.fail(function() {
console.warn(arguments);
});
pgAdmin.Browser.BgProcessManager.recheckCloudServer(data._id);
}
return;
}).always(function(){

View File

@ -266,6 +266,19 @@ define('pgadmin.browser', [
content: '<div class="negative-space p-2"><div role="status" class="pg-panel-message pg-panel-depends-message">' + select_object_msg + '</div><div class="pg-panel-dependents-container d-none"></div></div>',
events: panelEvents,
}),
// Background processes
'processes': new pgAdmin.Browser.Panel({
name: 'processes',
title: gettext('Processes'),
icon: '',
width: 500,
isCloseable: true,
isPrivate: false,
limit: 1,
canHide: true,
content: '<div class="negative-space p-2"><div class="pg-panel-processes-container d-none"></div></div>',
events: panelEvents,
}),
},
// We also support showing dashboards, HTML file, external URL
frames: {},

View File

@ -42,6 +42,8 @@ _.extend(pgBrowser, {
'dependencies', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'dependents', wcDocker.DOCK.STACKED, dashboardPanel);
docker.addPanel(
'processes', wcDocker.DOCK.STACKED, dashboardPanel);
},
save_current_layout: function(layout_id, docker) {

View File

@ -122,7 +122,7 @@ define(
that.resizedContainer.apply(myPanel);
}
if (myPanel._type == 'dashboard') {
if (myPanel._type == 'dashboard' || myPanel._type == 'processes') {
getPanelView(
pgBrowser.tree,
$container[0],
@ -263,8 +263,8 @@ define(
.scene()
.find('.pg-panel-content');
if (isPanelVisible && selectedPanel._type !== 'properties') {
if (eventName == 'panelVisibilityChanged' && selectedPanel._type !== 'properties') {
if (isPanelVisible && ['dashboard', 'statistics', 'dependencies', 'dependents', 'sql', 'processes'].includes(selectedPanel._type) ) {
if (eventName == 'panelVisibilityChanged') {
getPanelView(
pgBrowser.tree,
$container[0],

View File

@ -17,6 +17,7 @@ import SQL from '../../../misc/sql/static/js/SQL';
import Dashboard from '../../../dashboard/static/js/Dashboard';
import _ from 'lodash';
import { CollectionNodeView } from '../../../misc/properties/CollectionNodeProperties';
import Processes from '../../../misc/bgprocess/static/js/Processes';
/* The entry point for rendering React based view in properties, called in node.js */
@ -131,6 +132,14 @@ export function getPanelView(
container
);
}
if (panelType == 'processes') {
ReactDOM.render(
<Theme>
<Processes />
</Theme>,
container
);
}
}
/* When switching from normal node to collection node, clean up the React mounted DOM */

View File

@ -143,7 +143,7 @@ export default function Dashboard({
{
accessor: 'name',
Header: gettext('Name'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -153,7 +153,7 @@ export default function Dashboard({
{
accessor: 'category',
Header: gettext('Category'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -161,7 +161,7 @@ export default function Dashboard({
{
accessor: 'setting',
Header: gettext('Value'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -170,7 +170,7 @@ export default function Dashboard({
{
accessor: 'unit',
Header: gettext('Unit'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -179,7 +179,7 @@ export default function Dashboard({
{
accessor: 'short_desc',
Header: gettext('Description'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
@ -189,7 +189,7 @@ export default function Dashboard({
{
accessor: 'terminate_query',
Header: () => null,
sortble: true,
sortable: true,
resizable: false,
disableGlobalFilter: false,
width: 35,
@ -261,7 +261,7 @@ export default function Dashboard({
{
accessor: 'cancel_Query',
Header: () => null,
sortble: true,
sortable: true,
resizable: false,
disableGlobalFilter: false,
width: 35,
@ -330,7 +330,7 @@ export default function Dashboard({
{
accessor: 'view_active_query',
Header: () => null,
sortble: true,
sortable: true,
resizable: false,
disableGlobalFilter: false,
width: 35,
@ -374,7 +374,7 @@ export default function Dashboard({
{
accessor: 'pid',
Header: gettext('PID'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -383,7 +383,7 @@ export default function Dashboard({
{
accessor: 'datname',
Header: gettext('Database'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -393,7 +393,7 @@ export default function Dashboard({
{
accessor: 'usename',
Header: gettext('User'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -402,7 +402,7 @@ export default function Dashboard({
{
accessor: 'application_name',
Header: gettext('Application'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -410,7 +410,7 @@ export default function Dashboard({
{
accessor: 'client_addr',
Header: gettext('Client'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -418,7 +418,7 @@ export default function Dashboard({
{
accessor: 'backend_start',
Header: gettext('Backend start'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 100,
@ -426,7 +426,7 @@ export default function Dashboard({
{
accessor: 'xact_start',
Header: gettext('Transaction start'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -434,7 +434,7 @@ export default function Dashboard({
{
accessor: 'state',
Header: gettext('State'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -444,7 +444,7 @@ export default function Dashboard({
{
accessor: 'waiting',
Header: gettext('Waiting'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
isVisible: treeNodeInfo?.server?.version < 90600
@ -452,14 +452,14 @@ export default function Dashboard({
{
accessor: 'wait_event',
Header: gettext('Wait event'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
accessor: 'blocking_pids',
Header: gettext('Blocking PIDs'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
@ -469,7 +469,7 @@ export default function Dashboard({
{
accessor: 'pid',
Header: gettext('PID'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -478,7 +478,7 @@ export default function Dashboard({
{
accessor: 'datname',
Header: gettext('Database'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -488,7 +488,7 @@ export default function Dashboard({
{
accessor: 'locktype',
Header: gettext('Lock type'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -497,14 +497,14 @@ export default function Dashboard({
{
accessor: 'relation',
Header: gettext('Target relation'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
accessor: 'page',
Header: gettext('Page'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -513,7 +513,7 @@ export default function Dashboard({
{
accessor: 'tuple',
Header: gettext('Tuple'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -521,7 +521,7 @@ export default function Dashboard({
{
accessor: 'virtualxid',
Header: gettext('vXID (target)'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -530,7 +530,7 @@ export default function Dashboard({
{
accessor: 'transactionid',
Header: gettext('XID (target)'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -539,7 +539,7 @@ export default function Dashboard({
{
accessor: 'classid',
Header: gettext('Class'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -548,7 +548,7 @@ export default function Dashboard({
{
accessor: 'objid',
Header: gettext('Object ID'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -558,7 +558,7 @@ export default function Dashboard({
{
accessor: 'virtualtransaction',
Header: gettext('vXID (owner)'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 50,
@ -566,7 +566,7 @@ export default function Dashboard({
{
accessor: 'mode',
Header: gettext('Mode'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
@ -574,7 +574,7 @@ export default function Dashboard({
id: 'granted',
accessor: 'granted',
Header: gettext('Granted?'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 30,
@ -587,14 +587,14 @@ export default function Dashboard({
{
accessor: 'git',
Header: gettext('Name'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
accessor: 'datname',
Header: gettext('Database'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 26,
@ -604,21 +604,21 @@ export default function Dashboard({
{
accessor: 'Owner',
Header: gettext('Owner'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
accessor: 'transaction',
Header: gettext('XID'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
accessor: 'prepared',
Header: gettext('Prepared at'),
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},

View File

@ -61,7 +61,6 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
if not db_id:
self.assertTrue(False, "Database {} is not "
"created".format(self.database_name))
test_gui_helper.close_bgprocess_popup(self)
self.page.add_server(self.server)
self.wait = WebDriverWait(self.page.driver, 20)
@ -74,36 +73,23 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
# Backup
self.initiate_backup()
# Wait for the backup status alertfier
self.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR,
NavMenuLocators.bcg_process_status_alertifier_css)))
status = test_utils.get_watcher_dialogue_status(self)
self.page.retry_click(
(By.CSS_SELECTOR,
NavMenuLocators.status_alertifier_more_btn_css),
(By.XPATH,
NavMenuLocators.process_watcher_alertfier))
self.page.wait_for_element_to_disappear(
lambda driver: driver.find_element(
By.CSS_SELECTOR, ".loading-logs"), 15)
expected_backup_success_msg = "Successfully completed."
self.assertEqual(status, expected_backup_success_msg)
test_gui_helper.wait_for_process_start(self)
test_gui_helper.open_process_details(self)
backup_file = None
# Check for XSS in Backup details
if self.is_xss_check:
self._check_detailed_window_for_xss('Backup')
else:
message = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_message_css). \
text
command = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_command_canvas_css). \
NavMenuLocators.process_watcher_detailed_command_css). \
text
self.assertIn(self.server['name'], str(command))
self.assertIn("from database 'pg_utility_test_db'", str(command))
self.assertIn(self.server['name'], str(message))
self.assertIn("from database 'pg_utility_test_db'", str(message))
# On windows a modified path may be shown so skip this test
if os.name != 'nt':
@ -120,32 +106,21 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
# Restore
self.initiate_restore()
# Wait for the backup status alertfier
self.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR,
NavMenuLocators.bcg_process_status_alertifier_css)))
status = test_utils.get_watcher_dialogue_status(self)
self.page.retry_click(
(By.CSS_SELECTOR,
NavMenuLocators.status_alertifier_more_btn_css),
(By.XPATH,
NavMenuLocators.process_watcher_alertfier))
self.page.wait_for_element_to_disappear(
lambda driver: driver.find_element(
By.CSS_SELECTOR, ".loading-logs"), 10)
self.assertEqual(status, expected_backup_success_msg)
test_gui_helper.wait_for_process_start(self)
test_gui_helper.open_process_details(self)
# Check for XSS in Restore details
if self.is_xss_check:
self._check_detailed_window_for_xss('Restore')
else:
message = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_message_css). \
text
command = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_command_canvas_css). \
NavMenuLocators.process_watcher_detailed_command_css). \
text
self.assertIn(self.server['name'], str(command))
self.assertIn(self.server['name'], str(message))
if os.name != 'nt':
self.assertIn("test_backup", str(command))
@ -158,8 +133,6 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
def after(self):
try:
test_gui_helper.close_process_watcher(self)
test_gui_helper.close_bgprocess_popup(self)
self.page.remove_server(self.server)
except Exception:
print("PGUtilitiesBackupFeatureTest - "
@ -177,7 +150,7 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest):
def _check_detailed_window_for_xss(self, tool_name):
source_code = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_command_canvas_css
NavMenuLocators.process_watcher_detailed_command_css
).get_attribute('innerHTML')
self._check_escaped_characters(
source_code,

View File

@ -9,6 +9,7 @@
import random
import os
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
@ -73,7 +74,6 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest):
test_utils.create_database(self.server, self.database_name)
test_utils.create_table(self.server, self.database_name,
self.table_name)
test_gui_helper.close_bgprocess_popup(self)
self.page.add_server(self.server)
self.wait = WebDriverWait(self.page.driver, 20)
@ -84,10 +84,8 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest):
lambda driver: driver.find_element(
By.XPATH, NavMenuLocators.maintenance_operation), 10)
# Wait for the backup status alertfier
self.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR,
NavMenuLocators.bcg_process_status_alertifier_css)))
# Wait for the backup started alert
test_gui_helper.wait_for_process_start()
self.verify_command()
def _open_maintenance_dialogue(self):
@ -122,35 +120,24 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest):
NavMenuLocators.maintenance_operation, 10)
def verify_command(self):
status = test_utils.get_watcher_dialogue_status(self)
self.page.retry_click(
(By.CSS_SELECTOR,
NavMenuLocators.status_alertifier_more_btn_css),
(By.XPATH,
NavMenuLocators.process_watcher_alertfier))
self.page.wait_for_element_to_disappear(
lambda driver: driver.find_element(
By.CSS_SELECTOR, ".loading-logs"))
if status != "Successfully completed.":
self.assertEqual(status, "Successfully completed.")
test_gui_helper.open_process_details()
message = self.page.find_by_css_selector(
NavMenuLocators.process_watcher_detailed_message_css).text
command = self.page.find_by_css_selector(
NavMenuLocators.
process_watcher_detailed_command_canvas_css).text
NavMenuLocators.process_watcher_detailed_command_css).text
vacuum_details = \
"VACUUM (VERBOSE) on database '{0}' of server " \
"{1} ({2}:{3})".format(self.database_name, self.server['name'],
self.server['host'], self.server['port'])
if self.test_level == 'database':
self.assertEqual(
command, vacuum_details + "\nRunning Query:\nVACUUM VERBOSE;")
self.assertEqual(message, vacuum_details)
self.assertEqual(command, "VACUUM VERBOSE;")
elif self.is_xss_check and self.test_level == 'table':
# Check for XSS in the dialog
source_code = self.page.find_by_css_selector(
NavMenuLocators.
process_watcher_detailed_command_canvas_css
NavMenuLocators.process_watcher_detailed_command_css
).get_attribute('innerHTML')
self.check_escaped_characters(
source_code,
@ -158,15 +145,14 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest):
'Maintenance detailed window'
)
else:
self.assertEqual(
command, vacuum_details + "\nRunning Query:\nVACUUM VERBOSE"
" public." + self.table_name + ";")
self.assertEqual(message, vacuum_details)
self.assertEqual(command, "VACUUM VERBOSE"
" public." + self.table_name + ";")
test_gui_helper.close_process_watcher(self)
def after(self):
try:
test_gui_helper.close_bgprocess_popup(self)
test_utils.delete_table(self.server, self.database_name,
self.table_name)
self.page.remove_server(self.server)

View File

@ -39,7 +39,7 @@ class BGProcessModule(PgAdminModule):
return [
'bgprocess.status', 'bgprocess.detailed_status',
'bgprocess.acknowledge', 'bgprocess.list',
'bgprocess.stop_process'
'bgprocess.stop_process', 'bgprocess.update_cloud_details',
]
@ -88,6 +88,26 @@ def acknowledge(pid):
"""
User has acknowledge the process
Args:
pid: Process ID
Returns:
Positive status
"""
try:
BatchProcess.acknowledge(pid)
return success_return()
except LookupError as lerr:
return gone(errormsg=str(lerr))
@blueprint.route('/update_cloud_details/<pid>', methods=['PUT'],
endpoint='update_cloud_details')
@login_required
def update_cloud_details(pid):
"""
Update the cloud details and get instance details
Args:
pid: Process ID
@ -96,7 +116,7 @@ def acknowledge(pid):
"""
try:
process = BatchProcess(id=pid)
status, server = process.acknowledge(pid)
status, server = process.update_cloud_details()
if status and len(server) > 0:
return make_json_response(
success=1,

View File

@ -16,17 +16,19 @@ import os
import sys
import psutil
from abc import ABCMeta, abstractproperty, abstractmethod
from datetime import datetime
from datetime import datetime, timedelta
from pickle import dumps, loads
from subprocess import Popen, PIPE
import logging
import json
import shutil
from pgadmin.utils import u_encode, file_quote, fs_encoding, \
get_complete_file_path, get_storage_directory, IS_WIN
from pgadmin.browser.server_groups.servers.utils import does_server_exists
from pgadmin.utils.constants import KERBEROS
from pgadmin.utils.locker import ConnectionLocker
from pgadmin.utils.preferences import Preferences
import pytz
from dateutil import parser
@ -463,8 +465,9 @@ class BatchProcess(object):
idx = 0
c = re.compile(r"(\d+),(.*$)")
# If file is not present then
if not os.path.isfile(logfile):
return 0, False
return 0, True
with open(logfile, 'rb') as f:
eofs = os.fstat(f.fileno()).st_size
@ -496,10 +499,20 @@ class BatchProcess(object):
return pos, completed
def _get_cloud_instance_details(self, _process):
def update_cloud_details(self):
"""
Parse the output to get the cloud instance details
"""
_server = {}
_pid = self.id
_process = Process.query.filter_by(
user_id=current_user.id, pid=_pid
).first()
if _process is None:
raise LookupError(PROCESS_NOT_FOUND)
ctime = get_current_time(format='%y%m%d%H%M%S%f')
stdout = []
stderr = []
@ -507,7 +520,6 @@ class BatchProcess(object):
err = 0
cloud_server_id = 0
cloud_instance = ''
pid = self.id
enc = sys.getdefaultencoding()
if enc == 'ascii':
@ -527,20 +539,20 @@ class BatchProcess(object):
cloud_instance = json.loads(value[1])
cloud_server_id = _process.server_id
if type(cloud_instance) is dict and\
if type(cloud_instance) is dict and \
'instance' in cloud_instance:
cloud_instance['instance']['sid'] = cloud_server_id
cloud_instance['instance']['status'] = True
cloud_instance['instance']['pid'] = pid
cloud_instance['instance']['pid'] = _pid
return update_server(cloud_instance)
elif err_completed and _process.exit_code > 0:
cloud_instance = {'instance': {}}
cloud_instance['instance']['sid'] = _process.server_id
cloud_instance['instance']['status'] = False
cloud_instance['instance']['pid'] = pid
cloud_instance['instance']['pid'] = _pid
return update_server(cloud_instance)
else:
clear_cloud_session(pid)
clear_cloud_session(_pid)
return True, {}
def status(self, out=0, err=0):
@ -695,8 +707,23 @@ class BatchProcess(object):
processes = Process.query.filter_by(user_id=current_user.id)
changed = False
browser_preference = Preferences.module('browser')
expiry_add = timedelta(
browser_preference.preference('process_retain_days').get() or 1
)
res = []
for p in processes:
for p in [*processes]:
if p.start_time is not None:
# remove expired jobs
process_expiration_time = \
parser.parse(p.start_time) + expiry_add
if datetime.now(process_expiration_time.tzinfo) >= \
process_expiration_time:
shutil.rmtree(p.logdir, True)
db.session.delete(p)
changed = True
status, updated = BatchProcess.update_process_info(p)
if not status:
continue
@ -708,11 +735,6 @@ class BatchProcess(object):
):
continue
if BatchProcess._operate_orphan_process(p):
continue
execution_time = None
stime = parser.parse(p.start_time)
etime = parser.parse(p.end_time or get_current_time())
@ -732,6 +754,8 @@ class BatchProcess(object):
'acknowledge': p.acknowledge,
'execution_time': execution_time,
'process_state': p.process_state,
'utility_pid': p.utility_pid,
'server_id': p.server_id,
'current_storage_dir': current_storage_dir,
})
@ -740,35 +764,12 @@ class BatchProcess(object):
return res
@staticmethod
def _operate_orphan_process(p):
if p and p.desc:
desc = loads(p.desc)
if does_server_exists(desc.sid, current_user.id) is False:
current_app.logger.warning(
_("Server with id '{0}' is either removed or does "
"not exists for the background process "
"'{1}'").format(desc.sid, p.pid)
)
try:
process = BatchProcess(id=p.pid)
process.acknowledge(p.pid)
except LookupError as lerr:
current_app.logger.warning(
_("Status for the background process '{0}' could "
"not be loaded.").format(p.pid)
)
current_app.logger.exception(lerr)
return True
return False
@staticmethod
def total_seconds(dt):
return round(dt.total_seconds(), 2)
def acknowledge(self, _pid):
@staticmethod
def acknowledge(_pid):
"""
Acknowledge from the user, he/she has alredy watched the status.
@ -776,9 +777,6 @@ class BatchProcess(object):
And, delete the process information from the configuration, and the log
files related to the process, if it has already been completed.
"""
status = True
_server = {}
p = Process.query.filter_by(
user_id=current_user.id, pid=_pid
).first()
@ -787,16 +785,14 @@ class BatchProcess(object):
raise LookupError(PROCESS_NOT_FOUND)
if p.end_time is not None:
status, _server = self._get_cloud_instance_details(p)
logdir = p.logdir
db.session.delete(p)
import shutil
shutil.rmtree(logdir, True)
else:
p.acknowledge = get_current_time()
db.session.commit()
return status, _server
db.session.commit()
def set_env_variables(self, server, **kwargs):
"""Set environment variables"""
@ -834,13 +830,15 @@ class BatchProcess(object):
process.terminate()
# Update the process state to "Terminated"
p.process_state = PROCESS_TERMINATED
db.session.commit()
except psutil.NoSuchProcess:
p.process_state = PROCESS_TERMINATED
except psutil.Error as e:
current_app.logger.warning(
_("Unable to kill the background process '{0}'").format(
p.utility_pid)
)
current_app.logger.exception(e)
db.session.commit()
@staticmethod
def update_server_id(_pid, _sid):

View File

@ -1,59 +0,0 @@
.ajs-bg-bgprocess .col-xs-12 {
padding-right: 5px;
padding-left: 5px;
}
.ajs-bg-bgprocess > .pg-bg-bgprocess > .pg-bg-status {
padding: 2px;
margin: 0px 5px;
text-align: center;
border-radius: 2px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
}
.pg-bg-process-logs {
width: 100%;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
ol.pg-bg-process-logs {
height: 100%;
overflow: auto;
width: 100%;
}
.pg-panel-content .bg-process-stats p{
display: inline;
padding-left: 5px;
margin-bottom: 0;
font-size: 13px;
}
.pg-panel-content .bg-process-footer p {
display: inline;
padding-left: 5px;
font-size: 13px;
}
.bg-process-footer .bg-process-status {
padding-left: 0;
}
.bg-process-footer .bg-process-exec-time {
padding-right: 0;
}
.pg-bg-bgprocess .ajs-commands {
right: -13px;
top: 2px;
opacity: 0.5;
}
.pg-bg-bgprocess:hover .bg-close {
opacity: 0.95;
}

View File

@ -0,0 +1,243 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import Notifier from '../../../../static/js/helpers/Notifier';
import EventBus from '../../../../static/js/helpers/EventBus';
import * as BgProcessNotify from './BgProcessNotify';
import showDetails from './showDetails';
import gettext from 'sources/gettext';
const WORKER_INTERVAL = 1000;
export const BgProcessManagerEvents = {
LIST_UPDATED: 'LIST_UPDATED',
};
export const BgProcessManagerProcessState = {
PROCESS_NOT_STARTED: 0,
PROCESS_STARTED: 1,
PROCESS_FINISHED: 2,
PROCESS_TERMINATED: 3,
/* Supported by front end only */
PROCESS_TERMINATING: 10,
PROCESS_FAILED: 11,
};
export default class BgProcessManager {
static instance;
static getInstance(...args) {
if (!BgProcessManager.instance) {
BgProcessManager.instance = new BgProcessManager(...args);
}
return BgProcessManager.instance;
}
constructor(pgBrowser) {
this.api = getApiInstance();
this.pgBrowser = pgBrowser;
this._procList = [];
this._workerId = null;
this._pendingJobId = [];
this._eventManager = new EventBus();
}
init() {
if (this.initialized) {
return;
}
this.initialized = true;
this.startWorker();
}
get procList() {
return this._procList;
}
set procList(val) {
throw new Error('Property processList is readonly.', val);
}
async startWorker() {
let self = this;
await self.syncProcesses();
/* Fill the pending jobs initially */
self._pendingJobId = this.procList.filter((p)=>(p.process_state == BgProcessManagerProcessState.PROCESS_STARTED)).map((p)=>p.id);
this._workerId = setInterval(()=>{
if(self._pendingJobId.length > 0) {
self.syncProcesses();
}
}, WORKER_INTERVAL);
}
evaluateProcessState(p) {
let retState = p.process_state;
if((p.etime || p.exit_code !=null) && p.process_state == BgProcessManagerProcessState.PROCESS_STARTED) {
retState = BgProcessManagerProcessState.PROCESS_FINISHED;
}
if(retState == BgProcessManagerProcessState.PROCESS_FINISHED && p.exit_code != 0) {
retState = BgProcessManagerProcessState.PROCESS_FAILED;
}
return retState;
}
async syncProcesses() {
try {
let {data: resData} = await this.api.get(url_for('bgprocess.list'));
this._procList = resData?.map((p)=>{
let processState = this.evaluateProcessState(p);
return {
...p,
process_state: processState,
canDrop: ![BgProcessManagerProcessState.PROCESS_NOT_STARTED, BgProcessManagerProcessState.PROCESS_STARTED].includes(processState),
};
});
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
this.checkPending();
} catch (error) {
console.error(error);
}
}
checkPending() {
const completedProcIds = this.procList.filter((p)=>{
if(![
BgProcessManagerProcessState.PROCESS_NOT_STARTED,
BgProcessManagerProcessState.PROCESS_STARTED,
BgProcessManagerProcessState.PROCESS_TERMINATING].includes(p.process_state)) {
return true;
}
}).map((p)=>p.id);
this._pendingJobId = this._pendingJobId.filter((id)=>{
if(completedProcIds.includes(id)) {
let p = this.procList.find((p)=>p.id==id);
BgProcessNotify.processCompleted(p?.desc, p?.process_state, this.openProcessesPanel.bind(this));
if(p.server_id != null) {
this.updateCloudDetails(p.id);
}
return false;
}
return true;
});
}
startProcess(jobId, desc) {
if(jobId) {
this._pendingJobId.push(jobId);
BgProcessNotify.processStarted(desc, this.openProcessesPanel.bind(this));
}
}
stopProcess(jobId) {
this.procList.find((p)=>p.id == jobId).process_state = BgProcessManagerProcessState.PROCESS_TERMINATING;
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
this.api.put(url_for('bgprocess.stop_process', {
pid: jobId,
}))
.then(()=>{
this.procList.find((p)=>p.id == jobId).process_state = BgProcessManagerProcessState.PROCESS_TERMINATED;
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
})
.catch((err)=>{
Notifier.error(parseApiError(err));
});
}
acknowledge(jobIds) {
const removeJob = (jobId)=>{
this._procList = this.procList.filter((p)=>p.id!=jobId);
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
};
jobIds.forEach((jobId)=>{
this.api.put(url_for('bgprocess.acknowledge', {
pid: jobId,
}))
.then(()=>{
removeJob(jobId);
})
.catch((err)=>{
if(err.response?.status == 410) {
/* Object not available */
removeJob(jobId);
} else {
Notifier.error(parseApiError(err));
}
});
});
}
viewJobDetails(jobId) {
showDetails(this.procList.find((p)=>p.id==jobId));
}
updateCloudDetails(jobId) {
this.api.put(url_for('bgprocess.update_cloud_details', {
pid: jobId,
}))
.then((res)=>{
let _server = res.data?.data?.node;
if(!_server) {
Notifier.error(gettext('Cloud server information not available'));
return;
}
let _server_path = '/browser/server_group_' + _server.gid + '/' + _server.id,
_tree = this.pgBrowser.tree,
_item = _tree.findNode(_server_path);
if (_item) {
if(_server.status) {
let _dom = _item.domNode;
_tree.addIcon(_dom, {icon: _server.icon});
let d = _tree.itemData(_dom);
d.cloud_status = _server.cloud_status;
_tree.update(_dom, d);
}
else {
_tree.remove(_item.domNode);
_tree.refresh(_item.domNode.parent);
}
}
})
.catch((err)=>{
if(err.response?.status != 410) {
Notifier.error(gettext('Failed Cloud Deployment.'));
}
});
}
recheckCloudServer(sid) {
let self = this;
let process = self.procList.find((p)=>p.server_id==sid);
if(process) {
this.updateCloudDetails(process.id);
}
}
openProcessesPanel() {
let processPanel = this.pgBrowser.docker.findPanels('processes');
if(processPanel.length > 0) {
processPanel = processPanel[0];
} else {
let propertiesPanel = this.pgBrowser.docker.findPanels('properties');
processPanel = this.pgBrowser.docker.addPanel('processes', window.wcDocker.DOCK.STACKED, propertiesPanel[0]);
}
processPanel.focus();
}
registerListener(event, callback) {
this._eventManager.registerListener(event, callback);
}
deregisterListener(event, callback) {
this._eventManager.deregisterListener(event, callback);
}
}

View File

@ -0,0 +1,98 @@
import { Box, makeStyles } from '@material-ui/core';
import React from 'react';
import Notifier from '../../../../static/js/helpers/Notifier';
import CloseIcon from '@material-ui/icons/CloseRounded';
import { DefaultButton, PgIconButton } from '../../../../static/js/components/Buttons';
import clsx from 'clsx';
import DescriptionOutlinedIcon from '@material-ui/icons/DescriptionOutlined';
import { BgProcessManagerProcessState } from './BgProcessManager';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
const useStyles = makeStyles((theme)=>({
container: {
borderRadius: theme.shape.borderRadius,
padding: '0.25rem 1rem 1rem',
minWidth: '325px',
...theme.mixins.panelBorder.all,
},
containerHeader: {
height: '32px',
display: 'flex',
justifyContent: 'space-between',
fontWeight: 'bold',
alignItems: 'center',
borderTopLeftRadius: 'inherit',
borderTopRightRadius: 'inherit',
},
containerBody: {
marginTop: '1rem',
},
containerSuccess: {
borderColor: theme.palette.success.main,
backgroundColor: theme.palette.success.light,
},
iconSuccess: {
color: theme.palette.success.main,
},
containerError: {
borderColor: theme.palette.error.main,
backgroundColor: theme.palette.error.light,
},
iconError: {
color: theme.palette.error.main,
},
}));
function ProcessNotifyMessage({title, desc, onClose, onViewProcess, success=true, dataTestSuffix=''}) {
const classes = useStyles();
return (
<Box className={clsx(classes.container, (success ? classes.containerSuccess : classes.containerError))} data-test={'process-popup-' + dataTestSuffix}>
<Box display="flex" justifyContent="space-between" className={classes.containerHeader}>
<Box marginRight={'1rem'}>{title}</Box>
<PgIconButton size="xs" noBorder icon={<CloseIcon />} onClick={onClose} title={'Close'} className={success ? classes.iconSuccess : classes.iconError} />
</Box>
<Box className={classes.containerBody}>
<Box>{desc}</Box>
<Box marginTop={'1rem'} display="flex">
<DefaultButton startIcon={<DescriptionOutlinedIcon />} onClick={onViewProcess}>View Processes</DefaultButton>
</Box>
</Box>
</Box>
);
}
ProcessNotifyMessage.propTypes = {
title: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired,
onClose: PropTypes.func,
onViewProcess: PropTypes.func,
success: PropTypes.bool,
dataTestSuffix: PropTypes.string,
};
export function processStarted(desc, onViewProcess) {
Notifier.notify(
<ProcessNotifyMessage title={gettext('Process started')} desc={desc} onViewProcess={onViewProcess} dataTestSuffix="start"/>,
null
);
}
export function processCompleted(desc, process_state, onViewProcess) {
let title = gettext('Process completed');
let success = true;
if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATED) {
title = gettext('Process terminated');
success = false;
} else if(process_state == BgProcessManagerProcessState.PROCESS_FAILED) {
title = gettext('Process failed');
success = false;
}
Notifier.notify(
<ProcessNotifyMessage title={title} desc={desc} onViewProcess={onViewProcess} success={success} dataTestSuffix="end"/>,
null
);
}

View File

@ -0,0 +1,182 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useState, useMemo } from 'react';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import { Box, makeStyles } from '@material-ui/core';
import PropTypes from 'prop-types';
import { MESSAGE_TYPE, NotifierMessage } from '../../../../static/js/components/FormComponents';
import { BgProcessManagerProcessState } from './BgProcessManager';
import { DefaultButton, PgIconButton } from '../../../../static/js/components/Buttons';
import HighlightOffRoundedIcon from '@material-ui/icons/HighlightOffRounded';
import AccessTimeRoundedIcon from '@material-ui/icons/AccessTimeRounded';
import { useInterval } from '../../../../static/js/custom_hooks';
import getApiInstance from '../../../../static/js/api_instance';
import pgAdmin from 'sources/pgadmin';
import FolderSharedRoundedIcon from '@material-ui/icons/FolderSharedRounded';
const useStyles = makeStyles((theme)=>({
container: {
backgroundColor: theme.palette.background.default,
height: '100%',
display: 'flex',
flexDirection: 'column',
padding: '8px',
userSelect: 'text',
},
cmd: {
...theme.mixins.panelBorder.all,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.otherVars.inputDisabledBg,
wordBreak: 'break-word',
margin: '8px 0px',
padding: '4px',
},
logs: {
flexGrow: 1,
borderRadius: theme.shape.borderRadius,
padding: '4px',
overflow: 'auto',
textOverflow: 'wrap-text',
margin: '8px 0px',
...theme.mixins.panelBorder.all,
},
logErr: {
color: theme.palette.error.main,
},
terminateBtn: {
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText,
border: 0,
'&:hover': {
backgroundColor: theme.palette.error.dark,
color: theme.palette.error.contrastText,
},
'&.Mui-disabled': {
color: theme.palette.error.contrastText + ' !important',
border: 0,
}
}
}));
async function getDetailedStatus(api, jobId, out, err) {
let res = await api.get(url_for(
'bgprocess.detailed_status', {
'pid': jobId,
'out': out,
'err': err,
}
));
return res.data;
}
export default function ProcessDetails({data}) {
const classes = useStyles();
const api = useMemo(()=>getApiInstance());
const [logs, setLogs] = useState(null);
const [completed, setCompleted] = useState(false);
const [[outPos, errPos], setOutErrPos] = useState([0, 0]);
const [exitCode, setExitCode] = useState(data.exit_code);
const [timeTaken, setTimeTaken] = useState(data.execution_time);
let notifyType = MESSAGE_TYPE.INFO;
let notifyText = gettext('Not started');
const process_state = pgAdmin.Browser.BgProcessManager.evaluateProcessState({
...data,
exit_code: exitCode,
});
if(process_state == BgProcessManagerProcessState.PROCESS_STARTED) {
notifyText = gettext('Running...');
} else if(process_state == BgProcessManagerProcessState.PROCESS_FINISHED) {
notifyType = MESSAGE_TYPE.SUCCESS;
notifyText = gettext('Successfully completed.');
} else if(process_state == BgProcessManagerProcessState.PROCESS_FAILED) {
notifyType = MESSAGE_TYPE.ERROR;
notifyText = gettext('Failed (exit code: %s).', String(exitCode));
} else if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATED) {
notifyType = MESSAGE_TYPE.ERROR;
notifyText = gettext('Terminated by user.');
} else if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATING) {
notifyText = gettext('Terminating the process...');
}
useInterval(async ()=>{
const logsSortComp = (l1, l2)=>{
return l1[0].localeCompare(l2[0]);
};
let resData = await getDetailedStatus(api, data.id, outPos, errPos);
resData.out.lines.sort(logsSortComp);
resData.err.lines.sort(logsSortComp);
if(resData.out?.done && resData.err?.done && resData.exit_code != null) {
setExitCode(resData.exit_code);
setCompleted(true);
}
setTimeTaken(resData.execution_time);
setOutErrPos([resData.out.pos, resData.err.pos]);
setLogs((prevLogs)=>{
return [
...(prevLogs || []),
...resData.out.lines.map((l)=>l[1]),
...resData.err.lines.map((l)=>l[1]),
];
});
}, completed ? -1 : 1000);
const errRe = new RegExp(': (' + gettext('error') + '|' + gettext('fatal') + '):', 'i');
return (
<Box display="flex" flexDirection="column" className={classes.container} data-test="process-details">
<Box data-test="process-message">{data.details?.message}</Box>
{data.details?.cmd && <>
<Box>{gettext('Running command')}:</Box>
<Box data-test="process-cmd" className={classes.cmd}>{data.details.cmd}</Box>
</>}
{data.details?.query && <>
<Box>{gettext('Running query')}:</Box>
<Box data-test="process-cmd" className={classes.cmd}>{data.details.query}</Box>
</>}
<Box display="flex" justifyContent="space-between" alignItems="center" flexWrap="wrap">
<Box><span><AccessTimeRoundedIcon /> {gettext('Start time')}: {new Date(data.stime).toString()}</span></Box>
<Box>
{pgAdmin.server_mode == 'True' && data.current_storage_dir &&
<PgIconButton icon={<FolderSharedRoundedIcon />} title={gettext('Storage Manager')} onClick={()=>{
pgAdmin.Tools.FileManager.openStorageManager(data.current_storage_dir);
}} style={{marginRight: '4px'}} />}
<DefaultButton disabled={process_state != BgProcessManagerProcessState.PROCESS_STARTED || data.server_id != null}
startIcon={<HighlightOffRoundedIcon />} className={classes.terminateBtn}>Stop Process</DefaultButton></Box>
</Box>
<Box flexGrow={1} className={classes.logs}>
{logs == null && <span data-test="loading-logs">{gettext('Loading process logs...')}</span>}
{logs?.length == 0 && gettext('No logs available.')}
{logs?.map((log, i)=>{
return <div ref={(el)=>{
if(i==logs.length-1) {
el?.scrollIntoView();
}
}} key={i} className={errRe.test(log) ? classes.logErr : ''}>{log}</div>;
})}
</Box>
<Box display="flex" alignItems="center">
<NotifierMessage type={notifyType} message={notifyText} closable={false} textCenter={true} style={{flexGrow: 1, marginRight: '8px'}} />
<Box>{gettext('Execution time')}: {timeTaken} {gettext('seconds')}</Box>
</Box>
</Box>
);
}
ProcessDetails.propTypes = {
closeModal: PropTypes.func,
data: PropTypes.object,
onOK: PropTypes.func,
setHeight: PropTypes.func
};

View File

@ -0,0 +1,290 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect } from 'react';
import PgTable from 'sources/components/PgTable';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import pgAdmin from 'sources/pgadmin';
import { BgProcessManagerEvents, BgProcessManagerProcessState } from './BgProcessManager';
import { PgIconButton } from '../../../../static/js/components/Buttons';
import CancelIcon from '@material-ui/icons/Cancel';
import DescriptionOutlinedIcon from '@material-ui/icons/DescriptionOutlined';
import DeleteIcon from '@material-ui/icons/Delete';
import HelpIcon from '@material-ui/icons/HelpRounded';
import url_for from 'sources/url_for';
import { Box } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
stopButton: {
color: theme.palette.error.main
},
buttonClick: {
backgroundColor: theme.palette.grey[400]
},
emptyPanel: {
minHeight: '100%',
minWidth: '100%',
background: theme.otherVars.emptySpaceBg,
overflow: 'auto',
padding: '8px',
display: 'flex',
},
panelIcon: {
width: '80%',
margin: '0 auto',
marginTop: '25px !important',
position: 'relative',
textAlign: 'center',
},
panelMessage: {
marginLeft: '0.5rem',
fontSize: '0.875rem',
},
autoResizer: {
height: '100% !important',
width: '100% !important',
background: theme.palette.grey[400],
padding: '7.5px',
overflow: 'auto !important',
minHeight: '100%',
minWidth: '100%',
},
noPadding: {
padding: 0,
},
bgSucess: {
backgroundColor: theme.palette.success.light,
height: '100%',
padding: '4px',
},
bgFailed: {
backgroundColor: theme.palette.error.light,
height: '100%',
padding: '4px',
},
bgTerm: {
backgroundColor: theme.palette.warning.light,
height: '100%',
padding: '4px',
},
bgRunning: {
backgroundColor: theme.palette.primary.light,
height: '100%',
padding: '4px',
},
}));
const ProcessStateTextAndColor = {
[BgProcessManagerProcessState.PROCESS_NOT_STARTED]: [gettext('Not started'), 'bgRunning'],
[BgProcessManagerProcessState.PROCESS_STARTED]: [gettext('Running'), 'bgRunning'],
[BgProcessManagerProcessState.PROCESS_FINISHED]: [gettext('Finished'), 'bgSucess'],
[BgProcessManagerProcessState.PROCESS_TERMINATED]: [gettext('Terminated'), 'bgTerm'],
[BgProcessManagerProcessState.PROCESS_TERMINATING]: [gettext('Terminating...'), 'bgTerm'],
[BgProcessManagerProcessState.PROCESS_FAILED]: [gettext('Failed'), 'bgFailed'],
};
export default function Processes() {
const classes = useStyles();
const [tableData, setTableData] = React.useState([]);
const [selectedRows, setSelectedRows] = React.useState([]);
let columns = [
{
accessor: 'stop_process',
Header: () => null,
sortable: false,
resizable: false,
disableGlobalFilter: true,
width: 35,
maxWidth: 35,
minWidth: 35,
id: 'btn-stop',
// eslint-disable-next-line react/display-name
Cell: ({ row }) => {
return (
<PgIconButton
size="xs"
noBorder
icon={<CancelIcon />}
className={classes.stopButton}
disabled={row.original.process_state != BgProcessManagerProcessState.PROCESS_STARTED
|| row.original.server_id != null}
onClick={(e) => {
e.preventDefault();
pgAdmin.Browser.BgProcessManager.stopProcess(row.original.id);
}}
aria-label="Stop Process"
title={gettext('Stop Process')}
></PgIconButton>
);
},
},
{
accessor: 'view_details',
Header: () => null,
sortable: false,
resizable: false,
disableGlobalFilter: true,
width: 35,
maxWidth: 35,
minWidth: 35,
id: 'btn-logs',
// eslint-disable-next-line react/display-name
Cell: ({ row }) => {
return (
<PgIconButton
size="xs"
icon={<DescriptionOutlinedIcon />}
noBorder
onClick={(e) => {
e.preventDefault();
pgAdmin.Browser.BgProcessManager.viewJobDetails(row.original.id);
}}
aria-label="View details"
title={gettext('View details')}
/>
);
},
},
{
Header: gettext('PID'),
accessor: 'utility_pid',
sortable: true,
resizable: false,
width: 70,
minWidth: 70,
disableGlobalFilter: false,
},
{
Header: gettext('Type'),
accessor: (row)=>row.details?.type,
sortable: true,
resizable: true,
width: 100,
minWidth: 70,
disableGlobalFilter: false,
},
{
Header: gettext('Server'),
accessor: (row)=>row.details?.server,
sortable: true,
resizable: true,
width: 200,
minWidth: 120,
disableGlobalFilter: false,
},
{
Header: gettext('Object'),
accessor: (row)=>row.details?.object,
sortable: true,
resizable: true,
width: 200,
minWidth: 120,
disableGlobalFilter: false,
},
{
id: 'stime',
Header: gettext('Start Time'),
sortable: true,
resizable: true,
disableGlobalFilter: true,
width: 150,
minWidth: 150,
accessor: (row)=>(new Date(row.stime)),
Cell: ({row})=>(new Date(row.original.stime).toLocaleString()),
},
{
Header: gettext('Status'),
sortable: true,
resizable: false,
disableGlobalFilter: false,
width: 120,
minWidth: 120,
accessor: (row)=>ProcessStateTextAndColor[row.process_state][0],
dataClassName: classes.noPadding,
Cell: ({row})=>{
const [text, bgcolor] = ProcessStateTextAndColor[row.original.process_state];
return <Box className={classes[bgcolor]}>{text}</Box>;
},
},
{
Header: gettext('Time Taken'),
accessor: 'execution_time',
sortable: true,
resizable: true,
disableGlobalFilter: true,
},
];
const updateList = ()=>{
if(pgAdmin.Browser.BgProcessManager.procList) {
setTableData([...pgAdmin.Browser.BgProcessManager.procList]);
}
};
useEffect(() => {
updateList();
pgAdmin.Browser.BgProcessManager.registerListener(BgProcessManagerEvents.LIST_UPDATED, updateList);
return ()=>{
pgAdmin.Browser.BgProcessManager.deregisterListener(BgProcessManagerEvents.LIST_UPDATED, updateList);
};
}, []);
return (
<>
<PgTable
data-test="processes"
className={classes.autoResizer}
columns={columns}
data={tableData}
sortOptions={[{id: 'stime', desc: true}]}
getSelectedRows={(rows)=>{setSelectedRows(rows);}}
isSelectRow={true}
CustomHeader={()=>{
return (
<Box>
<PgIconButton
className={classes.dropButton}
icon={<DeleteIcon/>}
aria-label="Acknowledge and Remove"
title={gettext('Acknowledge and Remove')}
onClick={() => {
pgAdmin.Browser.BgProcessManager.acknowledge(selectedRows.map((p)=>p.original.id));
}}
disabled={selectedRows.length <= 0}
></PgIconButton>
<PgIconButton
icon={<HelpIcon/>}
aria-label="Help"
title={gettext('Help')}
style={{marginLeft: '8px'}}
onClick={() => {
window.open(url_for('help.static', {'filename': 'processes.html'}));
}}
></PgIconButton>
</Box>
);
}}
></PgTable>
</>
);
}
Processes.propTypes = {
res: PropTypes.array,
nodeData: PropTypes.object,
treeNodeInfo: PropTypes.object,
node: PropTypes.func,
item: PropTypes.object,
row: PropTypes.object,
};

View File

@ -1,764 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define('misc.bgprocess', [
'sources/pgadmin', 'sources/gettext', 'sources/url_for', 'underscore',
'jquery', 'pgadmin.browser', 'alertify', 'pgadmin.tools.file_manager',
], function(
pgAdmin, gettext, url_for, _, $, pgBrowser, Alertify
) {
pgBrowser.BackgroundProcessObsorver = pgBrowser.BackgroundProcessObsorver || {};
if (pgBrowser.BackgroundProcessObsorver.initialized) {
return pgBrowser.BackgroundProcessObsorver;
}
var isServerMode = (function() { return pgAdmin.server_mode == 'True'; })();
var wcDocker = window.wcDocker;
var BGProcess = function(info, notify) {
var self = this;
setTimeout(
function() {
self.initialize.apply(self, [info, notify]);
}, 1
);
};
_.extend(
BGProcess.prototype, {
success_status_tpl: _.template(`
<div class="d-flex px-2 py-1 bg-success-light border border-success rounded">
<div class="pr-2">
<i class="fa fa-check text-success pg-bg-status-icon" aria-hidden="true" role="img"></i>
</div>
<div class="mx-auto pg-bg-status-text alert-text-body"><%-status_text%></div>
</div>`),
failed_status_tpl: _.template(`
<div class="d-flex px-2 py-1 bg-danger-lighter border border-danger rounded">
<div class="pr-2">
<i class="fa fa-times fa-lg text-danger pg-bg-status-icon" aria-hidden="true" role="img"></i>
</div>
<div class="mx-auto pg-bg-status-text alert-text-body"><%-status_text%></div>
</div>`),
other_status_tpl: _.template(`
<div class="d-flex px-2 py-1 bg-primary-light border border-primary rounded">
<div class="pr-2">
<i class="fa fa-info fa-lg text-primary pg-bg-status-icon" aria-hidden="true" role="img"></i>
</div>
<div class="mx-auto pg-bg-status-text alert-text-body"><%-status_text%></div>
</div>`),
initialize: function(info, notify) {
_.extend(this, {
details: false,
notify: (_.isUndefined(notify) || notify),
curr_status: null,
state: 0, // 0: NOT Started, 1: Started, 2: Finished, 3: Terminated
completed: false,
current_storage_dir: null,
id: info['id'],
type_desc: null,
desc: null,
detailed_desc: null,
stime: null,
exit_code: null,
acknowledge: info['acknowledge'],
execution_time: null,
out: -1,
err: -1,
lot_more: false,
notifier: null,
container: null,
panel: null,
logs: $('<ol></ol>', {
class: 'pg-bg-process-logs',
}),
});
if (this.notify) {
pgBrowser.Events && pgBrowser.Events.on(
'pgadmin-bgprocess:started:' + this.id,
function(process) {
if (!process.notifier)
process.show.apply(process);
}
);
pgBrowser.Events && pgBrowser.Events.on(
'pgadmin-bgprocess:finished:' + this.id,
function(process) {
if (!process.notifier) {
if (process.cloud_process == 1) process.update_cloud_server.apply(process);
process.show.apply(process);
}
}
);
}
var self = this;
setTimeout(
function() {
self.update.apply(self, [info]);
}, 1
);
},
bgprocess_url: function(type) {
switch (type) {
case 'status':
if (this.details && this.out != -1 && this.err != -1) {
return url_for(
'bgprocess.detailed_status', {
'pid': this.id,
'out': this.out,
'err': this.err,
}
);
}
return url_for('bgprocess.status', {
'pid': this.id,
});
case 'acknowledge':
return url_for('bgprocess.acknowledge', {
'pid': this.id,
});
case 'stop_process':
return url_for('bgprocess.stop_process', {
'pid': this.id,
});
default:
return url_for('bgprocess.list');
}
},
update: function(data) {
var self = this,
out = [],
err = [];
if ('stime' in data)
self.stime = new Date(data.stime);
if ('execution_time' in data)
self.execution_time = parseFloat(data.execution_time);
if ('type_desc' in data)
self.type_desc = data.type_desc;
if ('desc' in data)
self.desc = data.desc;
if ('details' in data)
self.detailed_desc = data.details;
if ('exit_code' in data)
self.exit_code = data.exit_code;
if ('process_state' in data)
self.state = data.process_state;
if ('current_storage_dir' in data)
self.current_storage_dir = data.current_storage_dir;
if ('out' in data) {
self.out = data.out && data.out.pos;
if (data.out && data.out.lines) {
out = data.out.lines;
}
}
if ('cloud_process' in data && data.cloud_process == 1) {
self.cloud_process = data.cloud_process;
self.cloud_instance = data.cloud_instance;
self.cloud_server_id = data.cloud_server_id;
}
if ('err' in data) {
self.err = data.err && data.err.pos;
if (data.err && data.err.lines) {
err = data.err.lines;
}
}
self.completed = self.completed || (
'err' in data && 'out' in data && data.err.done && data.out.done
) || (!self.details && !_.isNull(self.exit_code));
var io = 0,
ie = 0,
res = [],
escapeEl = document.createElement('textarea'),
escapeHTML = function(html) {
escapeEl.textContent = html;
return escapeEl.innerHTML;
};
while (io < out.length && ie < err.length) {
if (pgAdmin.natural_sort(out[io][0], err[ie][0]) <= 0) {
res.push('<li class="pg-bg-res-out">' + escapeHTML(out[io++][1]) + '</li>');
} else {
let log_msg = escapeHTML(err[ie++][1]);
let regex_obj = new RegExp(': (' + gettext('error') + '|' + gettext('fatal') + '):', 'i');
if (regex_obj.test(log_msg)) {
res.push('<li class="pg-bg-res-err">' + log_msg + '</li>');
} else {
res.push('<li class="pg-bg-res-out">' + log_msg + '</li>');
}
}
}
while (io < out.length) {
res.push('<li class="pg-bg-res-out">' + escapeHTML(out[io++][1]) + '</li>');
}
while (ie < err.length) {
let log_msg = escapeHTML(err[ie++][1]);
let regex_obj = new RegExp(': (' + gettext('error') + '|' + gettext('fatal') + '):', 'i');
if (regex_obj.test(log_msg)) {
res.push('<li class="pg-bg-res-err">' + log_msg + '</li>');
} else {
res.push('<li class="pg-bg-res-out">' + log_msg + '</li>');
}
}
if (res.length) {
self.logs.append(res.join(''));
setTimeout(function() {
self.logs[0].scrollTop = self.logs[0].scrollHeight;
});
}
if(self.logs_loading) {
self.logs_loading.remove();
self.logs_loading = null;
}
if (self.stime) {
self.curr_status = self.other_status_tpl({status_text:gettext('Started')});
if (self.execution_time >= 2) {
self.curr_status = self.other_status_tpl({status_text:gettext('Running...')});
}
if (!_.isNull(self.exit_code)) {
if (self.state === 3) {
self.curr_status = self.failed_status_tpl({status_text:gettext('Terminated by user.')});
} else if (self.exit_code == 0) {
self.curr_status = self.success_status_tpl({status_text:gettext('Successfully completed.')});
} else {
self.curr_status = self.failed_status_tpl(
{status_text:gettext('Failed (exit code: %s).', String(self.exit_code))}
);
}
} else if (_.isNull(self.exit_code) && self.state === 3) {
self.curr_status = self.other_status_tpl({status_text:gettext('Terminating the process...')});
}
if (self.state == 0 && self.stime) {
self.state = 1;
pgBrowser.Events && pgBrowser.Events.trigger(
'pgadmin-bgprocess:started:' + self.id, self, self
);
}
if (self.state == 1 && !_.isNull(self.exit_code)) {
self.state = 2;
pgBrowser.Events && pgBrowser.Events.trigger(
'pgadmin-bgprocess:finished:' + self.id, self, self
);
}
setTimeout(function() {
self.show.apply(self);
}, 10);
}
if (!self.completed) {
setTimeout(
function() {
self.status.apply(self);
}, 1000
);
}
},
status: function() {
var self = this;
$.ajax({
typs: 'GET',
timeout: 30000,
url: self.bgprocess_url('status'),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function(res) {
setTimeout(function() {
self.update(res);
}, 10);
})
.fail(function(res) {
// Try after some time only if job id present
if (res.status != 410)
setTimeout(function() {
self.update(res);
}, 10000);
});
},
update_cloud_server: function() {
var self = this,
_url = url_for('cloud.update_cloud_server'),
_data = {},
cloud_instance = self.cloud_instance;
if (cloud_instance != '') {
_data = JSON.parse(cloud_instance);
}
_data['instance']['sid'] = self.cloud_server_id;
$.ajax({
type: 'POST',
url: _url,
async: true,
data: JSON.stringify(_data),
contentType: 'application/json',
})
.done(function(res) {
setTimeout(function() {
let _server = res.data.node,
_server_path = '/browser/server_group_' + _server.gid + '/' + _server.id,
_tree = pgBrowser.tree,
_item = _tree.findNode(_server_path);
if (_item) {
_tree.addIcon(_item.domNode, {icon: _server.icon});
let d = _tree.itemData(_item);
d.cloud_status = 1;
_tree.update(_item, d);
}
}, 10);
})
.fail(function(res) {
// Try after some time only if job id present
if (res.status != 410)
console.warn('Failed Cloud Deployment.');
});
},
show: function() {
var self = this;
if (self.notify && !self.details) {
if (!self.notifier) {
let content = $(`
<div class="card">
<div class="card-header bg-primary d-flex">
<div>${self.type_desc}</div>
<div class="ml-auto">
<button class="btn btn-sm-sq btn-primary pg-bg-close" aria-label='close'><i class="fa fa-lg fa-times" role="img"></i></button>
</div>
</div>
<div class="card-body px-2">
<div class="py-1">${self.desc}</div>
<div class="py-1">${self.stime.toString()}</div>
<div class="d-flex py-1">
<div class="my-auto mr-2">
<span class="fa fa-clock fa-lg" role="img"></span>
</div>
<div class="pg-bg-etime my-auto mr-2"></div>
<div class="ml-auto">
<button class="btn btn-secondary pg-bg-more-details" title="More Details"><span class="fa fa-info-circle" role="img"></span>&nbsp;` + gettext('More details...') + `</button>
<button class="btn btn-danger bg-process-stop" disabled><span class="fa fa-times-circle" role="img" title="Stop the operation"></span>&nbsp;` + gettext('Stop Process') + `</button>
</div>
</div>
<div class="pg-bg-status py-1">
</div>
</div>
</div>
`);
let for_details = content.find('.pg-bg-more-details');
let close_me = content.find('.pg-bg-close');
self.container = content;
self.notifier = Alertify.notify(
content.get(0), 'bg-bgprocess', 0, null
);
for_details.on('click', function(ev) {
ev = ev || window.event;
ev.cancelBubble = true;
ev.stopPropagation();
this.notifier.dismiss();
this.notifier = null;
this.completed = false;
this.show_detailed_view.apply(this);
}.bind(self));
close_me.on('click', function() {
this.notifier.dismiss();
this.notifier = null;
this.acknowledge_server.apply(this);
}.bind(this));
// Do not close the notifier, when clicked on the container, which
// is a default behaviour.
self.container.on('click', function(ev) {
ev = ev || window.event;
ev.cancelBubble = true;
ev.stopPropagation();
});
// On Click event to stop the process.
content.find('.bg-process-stop').off('click').on('click', self.stop_process.bind(this));
}
// TODO:: Formatted execution time
self.container.find('.pg-bg-etime').empty().append(
$('<span></span>').text(
String(self.execution_time)
)
).append(
$('<span></span>').text(' ' + gettext('seconds'))
);
var $status_bar = $(self.container.find('.pg-bg-status'));
$status_bar.html(self.curr_status);
var $btn_stop_process = $(self.container.find('.bg-process-stop'));
// Enable Stop Process button only when process is running
if (parseInt(self.state) === 1) {
$btn_stop_process.attr('disabled', false);
} else {
$btn_stop_process.attr('disabled', true);
}
} else {
self.show_detailed_view.apply(self);
}
},
show_detailed_view: function() {
var self = this,
panel = this.panel,
is_new = false;
if (!self.panel) {
is_new = true;
panel = this.panel =
pgBrowser.BackgroundProcessObsorver.create_panel();
panel.title(gettext('Process Watcher - %s', self.type_desc));
panel.focus();
}
var container = panel.$container,
$logs = container.find('.bg-process-watcher'),
$header = container.find('.bg-process-details'),
$footer = container.find('.bg-process-footer'),
$btn_stop_process = container.find('.bg-process-stop'),
$btn_storage_manager = container.find('.bg-process-storage-manager');
if(self.current_storage_dir && isServerMode) { //for backup & exports with server mode, operate over storage manager
if($btn_storage_manager.length == 0) {
var str_storage_manager_btn = '<button id="bg-process-storage-manager" class="btn btn-secondary bg-process-storage-manager" title="Click to open file location" aria-label="Storage Manager" tabindex="0" disabled><span class="pg-font-icon icon-storage_manager" role="img"></span></button>&nbsp;';
container.find('.bg-process-details .bg-btn-section').prepend(str_storage_manager_btn);
$btn_storage_manager = container.find('.bg-process-storage-manager');
}
// Disable storage manager button only when process is running
if (parseInt(self.state) === 1) {
$btn_storage_manager.attr('disabled', true);
}
else {
$btn_storage_manager.attr('disabled', false);
}
// On Click event for storage manager button.
$btn_storage_manager.off('click').on('click', self.storage_manager.bind(this));
}
// Enable Stop Process button only when process is running
if (parseInt(self.state) === 1) {
$btn_stop_process.attr('disabled', false);
} else {
$btn_stop_process.attr('disabled', true);
}
// On Click event to stop the process.
$btn_stop_process.off('click').on('click', self.stop_process.bind(this));
if (is_new) {
// set logs
$logs.html(self.logs);
setTimeout(function() {
self.logs[0].scrollTop = self.logs[0].scrollHeight;
});
self.logs_loading = $('<li class="pg-bg-res-out loading-logs">' + gettext('Loading process logs...') + '</li>');
self.logs.append(self.logs_loading);
// set bgprocess detailed description
$header.find('.bg-detailed-desc').html(self.detailed_desc);
}
// set bgprocess start time
$header.find('.bg-process-stats .bgprocess-start-time').html(
self.stime
);
// set status
$footer.find('.bg-process-status').html(self.curr_status);
// set bgprocess execution time
$footer.find('.bg-process-exec-time p').empty().append(
$('<span></span>').text(
String(self.execution_time)
)
).append(
$('<span></span>').text(' ' + gettext('seconds'))
);
if (is_new) {
self.details = true;
self.err = 0;
self.out = 0;
setTimeout(
function() {
self.status.apply(self);
}, 1000
);
var resize_log_container = function(logs, header, footer) {
var h = header.outerHeight() + footer.outerHeight();
logs.css('padding-bottom', h);
}.bind(panel, $logs, $header, $footer);
panel.on(wcDocker.EVENT.RESIZED, resize_log_container);
panel.on(wcDocker.EVENT.ATTACHED, resize_log_container);
panel.on(wcDocker.EVENT.DETACHED, resize_log_container);
resize_log_container();
panel.on(wcDocker.EVENT.CLOSED, function(process) {
process.panel = null;
process.details = false;
if (process.exit_code != null) {
process.acknowledge_server.apply(process);
}
}.bind(panel, this));
}
},
acknowledge_server: function() {
var self = this;
$.ajax({
type: 'PUT',
timeout: 30000,
url: self.bgprocess_url('acknowledge'),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function(res) {
if (res.data && res.data.node) {
setTimeout(function() {
let _server = res.data.node,
_server_path = '/browser/server_group_' + _server.gid + '/' + _server.id,
_tree = pgBrowser.tree,
_item = _tree.findNode(_server_path);
if (_item) {
if(_server.status == true) {
let _dom = _item.domNode;
_tree.addIcon(_dom, {icon: _server.icon});
let d = _tree.itemData(_dom);
d.cloud_status = _server.cloud_status;
_tree.update(_dom, d);
}
else {
_tree.remove(_item.domNode);
_tree.refresh(_item.domNode.parent);
}
}
}, 10);
} else return;
})
.fail(function() {
console.warn(arguments);
});
},
stop_process: function() {
var self = this;
// Set the state to terminated.
self.state = 3;
$.ajax({
type: 'PUT',
timeout: 30000,
url: self.bgprocess_url('stop_process'),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function() {
return;
})
.fail(function() {
console.warn(arguments);
});
},
storage_manager: function() {
var self = this;
if(self.current_storage_dir) {
pgAdmin.Tools.FileManager.openStorageManager(self.current_storage_dir);
}
},
});
_.extend(
pgBrowser.BackgroundProcessObsorver, {
bgprocesses: {},
init: function() {
var self = this;
if (self.initialized) {
return;
}
self.initialized = true;
setTimeout(
function() {
self.update_process_list.apply(self);
}, 1000
);
pgBrowser.Events.on(
'pgadmin-bgprocess:created',
function() {
setTimeout(
function() {
pgBrowser.BackgroundProcessObsorver.update_process_list(true);
}, 1000
);
}
);
},
update_process_list: function(recheck) {
var observer = this;
$.ajax({
type: 'GET',
timeout: 30000,
url: url_for('bgprocess.list'),
cache: false,
async: true,
contentType: 'application/json',
})
.done(function(res) {
if (!res || !_.isArray(res)) {
return;
}
for (var idx in res) {
var process = res[idx];
if ('id' in process) {
if (!(process.id in observer.bgprocesses)) {
observer.bgprocesses[process.id] = new BGProcess(process);
}
}
}
if (recheck && res.length == 0) {
// Recheck after some more time
setTimeout(
function() {
observer.update_process_list(false);
}, 3000
);
}
})
.fail(function() {
// FIXME:: What to do now?
console.warn(arguments);
});
},
create_panel: function() {
this.register_panel();
return pgBrowser.docker.addPanel(
'bg_process_watcher',
wcDocker.DOCK.FLOAT,
null, {
w: (screen.width < 700 ?
screen.width * 0.95 : screen.width * 0.5),
h: (screen.height < 500 ?
screen.height * 0.95 : screen.height * 0.5),
x: (screen.width < 700 ? '2%' : '25%'),
y: (screen.height < 500 ? '2%' : '25%'),
});
},
register_panel: function() {
var w = pgBrowser.docker,
panels = w.findPanels('bg_process_watcher');
if (panels && panels.length >= 1)
return;
var p = new pgBrowser.Panel({
name: 'bg_process_watcher',
showTitle: true,
isCloseable: true,
isPrivate: true,
isLayoutMember: false,
content: '<div class="bg-process-details">' +
'<div class="bg-detailed-desc"></div>' +
'<div class="bg-process-stats d-flex py-1">' +
'<div class="my-auto mr-2">' +
'<span class="fa fa-clock fa-lg" role="img"></span>' +
'</div>' +
'<div class="pg-bg-etime my-auto mr-2">'+
'<span>' + gettext('Start time') + ': <span class="bgprocess-start-time"></span>' +
'</span>'+
'</div>' +
'<div class="ml-auto bg-btn-section">' +
'<button type="button" class="btn btn-danger bg-process-stop" disabled><span class="fa fa-times-circle" role="img"></span>&nbsp;' + gettext('Stop Process') + '</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="bg-process-watcher">' +
'</div>' +
'<div class="bg-process-footer p-2 d-flex">' +
'<div class="bg-process-status flex-grow-1">' +
'</div>' +
'<div class="bg-process-exec-time ml-4 my-auto">' +
'<div class="exec-div">' +
'<span>' + gettext('Execution time') + ':</span><p></p>' +
'</div>' +
'</div>' +
'</div>',
onCreate: function(myPanel, $container) {
$container.addClass('pg-no-overflow p-2');
},
});
p.load(pgBrowser.docker);
},
});
return pgBrowser.BackgroundProcessObsorver;
});

View File

@ -0,0 +1,22 @@
/////////////////////////////////////////////////////////////
//
// 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 BgProcessManager from './BgProcessManager';
if (!pgAdmin.Browser) {
pgAdmin.Browser = {};
}
pgAdmin.Browser.BgProcessManager = BgProcessManager.getInstance(pgBrowser);
module.exports = {
BgProcessManager: BgProcessManager,
};

View File

@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import pgAdmin from 'sources/pgadmin';
import Theme from '../../../../static/js/Theme';
import ProcessDetails from './ProcessDetails';
import gettext from 'sources/gettext';
export default function showDetails(p) {
let pgBrowser = pgAdmin.Browser;
// Register dialog panel
pgBrowser.Node.registerUtilityPanel();
let panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
j = panel.$container.find('.obj_properties').first();
panel.title(gettext('Process Watcher - %s', p.type_desc));
panel.focus();
panel.on(window.wcDocker.EVENT.CLOSED, ()=>{
ReactDOM.unmountComponentAtNode(j[0]);
});
ReactDOM.render(
<Theme>
<ProcessDetails
data={p}
closeModal={()=>{
panel.close();
}}
/>
</Theme>, j[0]);
}

View File

@ -1,127 +0,0 @@
$bgproc-container-pad: 2px;
.ajs-bg-bgprocess.ajs-visible {
border: none;
padding: 0px !important;
text-align: left;
color: $color-fg;
min-width: 500px;
max-width: 500px;
.card {
border:none;
& .card-header {
border: $border-width solid $color-primary;
background: $color-primary;
color: $color-primary-fg;
}
& .card-body {
padding: 5px;
border: $border-width solid $card-border-color;
border-bottom-left-radius: $card-border-radius;
border-bottom-right-radius: $card-border-radius;
}
}
}
.ajs-bg-bgprocess > .pg-bg-bgprocess {
background-color: $color-primary;
color: $color-primary-fg;
padding: $bgproc-container-pad;
text-align: left;
font-size: 0.9em;
}
.ajs-bg-bgprocess > .pg-bg-bgprocess > .pg-bg-notify-header {
background-color: $color-gray-dark;
margin: (-$bgproc-container-pad) (-$bgproc-container-pad) 5px;
padding: 5px;
padding-right: 20px;
white-space: pre-wrap;
text-align: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.ajs-bg-bgprocess > .pg-bg-bgprocess > .pg-bg-notify-body {
font-family: $font-family-editor;
white-space: nowrap;
padding: 5px 0px;
}
.pg-bg-click {
color: $color-gray-lighter;
text-decoration: underline;
cursor: pointer;
padding: 5px 0px;
}
.pg-bg-click:hover {
color: $color-gray-dark;
}
.pg-bg-res-out, .pg-bg-res-err {
white-space: pre-wrap;
}
.pg-bg-res-err {
color: $color-danger;
}
.ajs-bg-bgprocess > .pg-bg-bgprocess > .pg-bg-status.bg-success,
.bg-process-status .bg-bgprocess-success {
color: $color-success-light;
font-weight: bold;
}
.bg-process-status .bg-bgprocess-failed {
color: $color-danger;
font-weight: bold;
}
.ajs-bg-bgprocess > .pg-bg-bgprocess > .pg-bg-status.bg-failed {
color: $color-fg;
background-color: $color-danger-lighter;
}
.pg-panel-content div.bg-process-watcher {
height: 100%;
padding: 0px 0px 0px 0px;
}
.pg-bg-process-logs {
border: $border-width solid $input-border-color;
border-radius: $input-border-radius;
padding-inline-start: 1rem;
}
.pg-bg-cmd {
border: $border-width solid $input-border-color;
border-radius: $input-border-radius;
background: $input-disabled-bg;
word-break: break-word;
}
.pg-panel-content .bg-process-footer {
position: absolute;
bottom: 0;
right: 0px;
left: 0px;
}
.pg-bg-bgprocess .bg-close {
display: inline-block;
position: relative;
height: 25px;
width: 25px;
right: -12px;
padding: 2px;
border: 2px solid $color-primary;
border-radius: 4px;
opacity: 0.5;
background-color: $color-bg;
color: $color-danger;
}

View File

@ -130,11 +130,11 @@ def deploy_on_cloud():
data = json.loads(request.data, encoding='utf-8')
if data['cloud'] == 'rds':
status, resp = deploy_on_rds(data)
status, p, resp = deploy_on_rds(data)
elif data['cloud'] == 'biganimal':
status, resp = deploy_on_biganimal(data)
status, p, resp = deploy_on_biganimal(data)
elif data['cloud'] == 'azure':
status, resp = deploy_on_azure(data)
status, p, resp = deploy_on_azure(data)
else:
status = False
resp = gettext('No cloud implementation.')
@ -149,19 +149,22 @@ def deploy_on_cloud():
# Return response
return make_json_response(
success=1,
data={'job_id': 1, 'node': {
'_id': resp['sid'],
'_pid': data['db_details']['gid'],
'connected': False,
'_type': 'server',
'icon': 'icon-server-cloud-deploy',
'id': 'server_{}'.format(resp['sid']),
'inode': True,
'label': resp['label'],
'server_type': 'pg',
'module': 'pgadmin.node.server',
'cloud_status': -1
}}
data={
'job_id': p.id,
'desc': p.desc.message,
'node': {
'_id': resp['sid'],
'_pid': data['db_details']['gid'],
'connected': False,
'_type': 'server',
'icon': 'icon-server-cloud-deploy',
'id': 'server_{}'.format(resp['sid']),
'inode': True,
'label': resp['label'],
'server_type': 'pg',
'module': 'pgadmin.node.server',
'cloud_status': -1
}}
)

View File

@ -721,10 +721,10 @@ def deploy_on_azure(data):
else:
session['azure_cache_files_list'] = {p.id: azure.azure_cache_name}
return True, {'label': _label, 'sid': sid}
return True, p, {'label': _label, 'sid': sid}
except Exception as e:
current_app.logger.exception(e)
return False, str(e)
return False, None, str(e)
finally:
del session['azure']['azure_obj']

View File

@ -429,8 +429,8 @@ def deploy_on_biganimal(data):
p.update_server_id(p.id, sid)
p.start()
return True, {'label': _label, 'sid': sid}
return True, p, {'label': _label, 'sid': sid}
except Exception as e:
current_app.logger.exception(e)
return False, str(e)
return False, None, str(e)

View File

@ -324,7 +324,7 @@ def deploy_on_rds(data):
p.update_server_id(p.id, sid)
p.start()
return True, {'label': _label, 'sid': sid}
return True, p, {'label': _label, 'sid': sid}
except Exception as e:
current_app.logger.exception(e)
return False, str(e)
return False, None, str(e)

View File

@ -153,7 +153,7 @@ export default function CloudWizard({ nodeInfo, nodeData, onClose}) {
axiosApi.post(_url, post_data)
.then((res) => {
pgAdmin.Browser.Events.trigger('pgadmin:browser:tree:add', res.data.data.node, {'server_group': nodeInfo['server_group']});
pgAdmin.Browser.Events.trigger('pgadmin-bgprocess:created');
pgAdmin.Browser.BgProcessManager.startProcess(res.data.data.job_id, res.data.data.desc);
onClose();
})
.catch((error) => {

View File

@ -14,6 +14,7 @@ from pgadmin.misc.bgprocess.processes import IProcessDesc
from pgadmin.utils import html
from pgadmin.model import db, Server
from flask_babel import gettext
from pgadmin.utils import get_server
def get_my_ip():
@ -79,13 +80,15 @@ class CloudProcessDesc(IProcessDesc):
self.provider, self.instance_name))
def details(self, cmd, args):
res = '<div>' + self.message
res += '</div><div class="py-1">'
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(self.cmd)
res += '</div></div>'
server = getattr(get_server(self.sid), 'name', "Not available")
return res
return {
"message": self.message,
"cmd": cmd,
"server": server,
"object": self.instance_name,
"type": self.provider,
}
@property
def type_desc(self):

View File

@ -80,21 +80,21 @@ export default function Dependencies({ nodeData, item, node, ...props }) {
{
Header: 'Type',
accessor: 'type',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Name',
accessor: 'name',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Restriction',
accessor: 'field',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 280,

View File

@ -81,21 +81,21 @@ export default function Dependents({ nodeData, item, node, ...props }) {
{
Header: 'Type',
accessor: 'type',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Name',
accessor: 'name',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Restriction',
accessor: 'field',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 280,

View File

@ -664,7 +664,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
await openDir();
}} icon={<SyncRoundedIcon />} disabled={showUploader} />
</PgButtonGroup>
<InputText type="search" className={classes.inputSearch} data-label="search" placeholder='Search' value={search} onChange={setSearch} />
<InputText type="search" className={classes.inputSearch} data-label="search" placeholder={gettext('Search')} value={search} onChange={setSearch} />
<PgButtonGroup size="small" style={{marginLeft: '4px'}}>
{params.dialog_type == 'storage_dialog' &&
<PgIconButton title={gettext('Download')} icon={<GetAppRoundedIcon />}

View File

@ -65,7 +65,7 @@ const useStyles = makeStyles((theme) => ({
overflowX: 'auto !important'
},
dropButton: {
marginRight: '5px !important'
marginRight: '8px !important'
},
readOnlySwitch: {
opacity: 0.75,
@ -99,14 +99,14 @@ export function CollectionNodeView({
{
Header: 'properties',
accessor: 'Properties',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'value',
accessor: 'value',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
@ -209,7 +209,7 @@ export function CollectionNodeView({
column = {
Header: field.label,
accessor: field.id,
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 0,
@ -222,7 +222,7 @@ export function CollectionNodeView({
column = {
Header: field.label,
accessor: field.id,
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 0,
@ -236,7 +236,7 @@ export function CollectionNodeView({
column = {
Header: field,
accessor: field,
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 0,
@ -267,7 +267,7 @@ export function CollectionNodeView({
}
}, [itemNodeData, node, item, reload]);
const customHeader = () => {
const CustomHeader = () => {
return (
<Box >
<PgIconButton
@ -308,7 +308,7 @@ export function CollectionNodeView({
(
<PgTable
isSelectRow={!('catalog' in treeNodeInfo) && (itemNodeData.label !== 'Catalogs') && _.isUndefined(node?.canSelect)}
customHeader={customHeader}
CustomHeader={CustomHeader}
className={classes.autoResizer}
columns={pgTableColumns}
data={data}

View File

@ -61,7 +61,7 @@ function getColumn(data, singleLineStatistics) {
column = {
Header: row.name,
accessor: row.name,
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
sortType: ((rowA, rowB, id) => {
@ -72,7 +72,7 @@ function getColumn(data, singleLineStatistics) {
column = {
Header: row.name,
accessor: row.name,
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
};
@ -87,14 +87,14 @@ function getColumn(data, singleLineStatistics) {
{
Header: gettext('Statistics'),
accessor: 'name',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Value',
accessor: 'value',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
@ -165,14 +165,14 @@ export default function Statistics({ nodeData, item, node, ...props }) {
{
Header: 'Statictics',
accessor: 'name',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},
{
Header: 'Value',
accessor: 'value',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
},

View File

@ -242,7 +242,7 @@ export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTe
onSearchTextChange(value);
setSearchText(value);
}}
placeholder={'Search'}>
placeholder={gettext('Search')}>
</InputText>
</Box>
}

View File

@ -14,14 +14,13 @@ import axios from 'axios';
/* Get the axios instance to call back end APIs.
Do not import axios directly, instead use this */
export default function getApiInstance(headers={}) {
const api = axios.create({
return axios.create({
headers: {
'Content-type': 'application/json',
[pgAdmin.csrf_token_header]: pgAdmin.csrf_token,
...headers,
}
});
return api;
}
export function parseApiError(error) {

View File

@ -81,6 +81,17 @@ const useStyles = makeStyles((theme)=>({
},
noBorder: {
border: 0,
color: 'inherit',
backgroundColor: 'transparent',
'&:hover': {
border: 0,
color: 'inherit',
backgroundColor: 'inherit',
filter: 'brightness(85%)',
},
'&.Mui-disabled': {
border: 0,
},
}
}));

View File

@ -371,7 +371,8 @@ export const InputText = forwardRef(({
maxLength: controlProps?.multiline ? null : maxlength,
'aria-describedby': helpid,
...(type ? { pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type] } : {}),
style: inputStyle || {}
style: inputStyle || {},
autoComplete: 'new-password',
}}
readOnly={Boolean(readonly)}
disabled={Boolean(disabled)}
@ -1167,6 +1168,7 @@ const useStylesFormFooter = makeStyles((theme) => ({
padding: theme.spacing(0.5),
display: 'flex',
alignItems: 'center',
minHeight: '36px',
},
containerSuccess: {
borderColor: theme.palette.success.main,
@ -1290,11 +1292,13 @@ FormInputSelectThemes.propTypes = {
};
export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, showIcon=true, textCenter=false, onClose = () => {/*This is intentional (SonarQube)*/ } }) {
export function NotifierMessage({
type = MESSAGE_TYPE.SUCCESS, message, style, closable = true, showIcon=true, textCenter=false,
onClose = () => {/*This is intentional (SonarQube)*/ }}) {
const classes = useStylesFormFooter();
return (
<Box className={clsx(classes.container, classes[`container${type}`])}>
<Box className={clsx(classes.container, classes[`container${type}`])} style={style}>
{showIcon && <FormIcon type={type} className={classes[`icon${type}`]} />}
<Box className={textCenter ? classes.messageCenter : classes.message}>{HTMLReactParse(message || '')}</Box>
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
@ -1311,6 +1315,7 @@ NotifierMessage.propTypes = {
showIcon: PropTypes.bool,
textCenter: PropTypes.bool,
onClose: PropTypes.func,
style: PropTypes.object,
};

View File

@ -28,6 +28,8 @@ import _ from 'lodash';
import gettext from 'sources/gettext';
import SchemaView from '../SchemaView';
import EmptyPanelMessage from './EmptyPanelMessage';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
/* eslint-disable react/display-name */
const useStyles = makeStyles((theme) => ({
@ -50,31 +52,18 @@ const useStyles = makeStyles((theme) => ({
overflowX: 'hidden !important',
overflow: 'overlay !important',
},
customHeader:{
CustomHeader:{
marginTop: '8px',
marginLeft: '4px'
},
searchBox: {
display: 'flex',
background: theme.palette.background.default
},
warning: {
backgroundColor: theme.palette.warning.main + '!important'
},
alert: {
backgroundColor: theme.palette.error.main + '!important'
},
searchPadding: {
flex: 2.5
},
searchInput: {
flex: 1,
marginTop: 8,
borderLeft: 'none',
paddingLeft: 5,
marginRight: 8,
marginBottom: 8,
minWidth: '300px'
},
tableContainer: {
overflowX: 'auto',
@ -93,14 +82,18 @@ const useStyles = makeStyles((theme) => ({
flexDirection: 'column',
height: '100%',
},
pgTableHeadar: {
pgTableContainer: {
display: 'flex',
flexGrow: 1,
overflow: 'hidden !important',
height: '100% !important',
flexDirection: 'column'
overflow: 'hidden',
flexDirection: 'column',
height: '100%',
},
pgTableHeader: {
display: 'flex',
background: theme.palette.background.default,
padding: '8px',
},
tableRowContent:{
display: 'flex',
flexDirection: 'column',
@ -134,10 +127,9 @@ const useStyles = makeStyles((theme) => ({
fontWeight: theme.typography.fontWeightBold,
padding: theme.spacing(1, 0.5),
textAlign: 'left',
overflowY: 'auto',
overflowX: 'hidden',
alignContent: 'center',
backgroundColor: theme.otherVars.tableBg,
overflow: 'hidden',
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
...theme.mixins.panelBorder.top,
@ -221,7 +213,7 @@ IndeterminateCheckbox.propTypes = {
};
const ROW_HEIGHT = 35;
export default function PgTable({ columns, data, isSelectRow, caveTable=true, ...props }) {
export default function PgTable({ columns, data, isSelectRow, caveTable=true, schema, ExpandedComponent, sortOptions, ...props }) {
// Use the state and functions returned from useTable to build your UI
const classes = useStyles();
const [searchVal, setSearchVal] = React.useState('');
@ -277,6 +269,9 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
defaultColumn,
isSelectRow,
autoResetSortBy: false,
initialState: {
sortBy: sortOptions || [],
}
},
useGlobalFilter,
useSortBy,
@ -333,7 +328,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
/>
</div>
),
sortble: false,
sortable: false,
width: 35,
maxWidth: 35,
minWidth: 0
@ -403,7 +398,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
}, [expandComplete]);
return (
<div style={style} key={row.id} ref={rowRef}>
<div style={style} key={row.id} ref={rowRef} data-test="row-container">
<div className={classes.tableRowContent}>
<div {...row.getRowProps()} className={classes.tr}>
{row.cells.map((cell) => {
@ -421,7 +416,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
classNames.push(classes.alert);
}
return (
<div key={cell.column.id} {...cell.getCellProps()} className={clsx(classNames, row.original.icon && row.original.icon[cell.column.id], row.original.icon[cell.column.id] && classes.cellIcon)}
<div key={cell.column.id} {...cell.getCellProps()} className={clsx(classNames, cell.column?.dataClassName, row.original.icon?.[cell.column.id], row.original.icon?.[cell.column.id] && classes.cellIcon)}
title={_.isUndefined(cell.value) || _.isNull(cell.value) ? '': String(cell.value)}>
{cell.render('Cell')}
</div>
@ -430,13 +425,14 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
</div>
{!_.isUndefined(row) && row.isExpanded && (
<Box key={row.id} className={classes.expandedForm}>
<SchemaView
{schema && <SchemaView
getInitData={()=>Promise.resolve({})}
viewHelperProps={{ mode: 'properties' }}
schema={props.schema[row.id]}
schema={schema[row.id]}
showFooter={false}
onDataChange={()=>{setExpandComplete(true);}}
/>
/>}
{ExpandedComponent && <ExpandedComponent row={row} onExpandComplete={()=>setExpandComplete(true)}/>}
</Box>
)}
</div>
@ -447,24 +443,30 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
);
// Render the UI for your table
return (
<Box className={classes.pgTableHeadar}>
<Box className={classes.searchBox}>
{props.customHeader && (<Box className={classes.customHeader}> <props.customHeader /></Box>)}
<Box className={classes.searchPadding}></Box>
<InputText
placeholder={'Search'}
className={classes.searchInput}
value={searchVal}
onChange={(val) => {
setSearchVal(val);
}}
/>
<Box className={classes.pgTableContainer} data-test={props['data-test']}>
<Box className={classes.pgTableHeader}>
{props.CustomHeader && (<Box className={classes.customHeader}> <props.CustomHeader /></Box>)}
<Box marginLeft="auto">
<InputText
placeholder={'Search'}
className={classes.searchInput}
value={searchVal}
onChange={(val) => {
setSearchVal(val);
}}
/>
</Box>
</Box>
<div className={classes.tableContainer}>
<div {...getTableProps({style:{minWidth: totalColumnsWidth}})} className={clsx(classes.table, caveTable ? classes.caveTable : '')}>
<div>
{headerGroups.map((headerGroup) => (
<div key={''} {...headerGroup.getHeaderGroupProps()}>
<div key={''} {...headerGroup.getHeaderGroupProps((column)=>({
style: {
...column.style,
height: '40px',
}
}))}>
{headerGroup.headers.map((column) => (
<div
key={column.id}
@ -472,14 +474,14 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
className={clsx(classes.tableCellHeader, column.className)}
>
<div
{...(column.sortble ? column.getSortByToggleProps() : {})}
{...(column.sortable ? column.getSortByToggleProps() : {})}
>
{column.render('Header')}
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
? <KeyboardArrowDownIcon style={{fontSize: '1.2rem'}} />
: <KeyboardArrowUpIcon style={{fontSize: '1.2rem'}} />
: ''}
</span>
</div>
@ -507,14 +509,13 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
height={height}
itemCount={rows.length}
itemSize={getRowHeight}
sorted={props?.sortOptions}
>
{RenderRow}
</VariableSizeList>)}
</AutoSizer>
</div>
) : (
<EmptyPanelMessage text={gettext('No record found')}/>
<EmptyPanelMessage text={gettext('No rows found')}/>
)
}
</div>
@ -526,7 +527,7 @@ export default function PgTable({ columns, data, isSelectRow, caveTable=true, ..
PgTable.propTypes = {
stepId: PropTypes.number,
height: PropTypes.number,
customHeader: PropTypes.func,
CustomHeader: PropTypes.func,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
caveTable: PropTypes.bool,
fixedSizeList: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@ -544,8 +545,9 @@ PgTable.propTypes = {
setSelectedRows: PropTypes.func,
getSelectedRows: PropTypes.func,
searchText: PropTypes.string,
type: PropTypes.string,
sortOptions: PropTypes.array,
schema: PropTypes.object,
rows: PropTypes.object
rows: PropTypes.object,
ExpandedComponent: PropTypes.node,
'data-test': PropTypes.string
};

View File

@ -13,6 +13,7 @@ export function useInterval(callback, delay) {
}
if(delay > -1) {
tick();
let id = setInterval(tick, delay);
return () => clearInterval(id);
}

View File

@ -92,7 +92,7 @@ const useModalStyles = makeStyles((theme)=>({
},
margin: {
marginLeft: '0.25rem',
}
},
}));
function AlertContent({text, confirm, okLabel=gettext('OK'), cancelLabel=gettext('Cancel'), onOkClick, onCancelClick}) {
const classes = useModalStyles();
@ -144,11 +144,10 @@ var Notifier = {
}
},
_callNotify(msg, type, autoHideDuration) {
if (!_.isNull(autoHideDuration)) {
this.notify(<NotifierMessage type={type} message={msg} closable={false} />, autoHideDuration);
} else {
this.notify(<NotifierMessage type={type} message={msg}/>, null);
}
this.notify(
<NotifierMessage style={{maxWidth: '50vw'}} type={type} message={msg} closable={_.isNull(autoHideDuration) ? true : false} />,
autoHideDuration
);
},
pgRespErrorNotify(xhr, error, prefixMsg='') {

View File

@ -6,7 +6,6 @@
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements Backup Utility"""
import simplejson as json
@ -103,9 +102,12 @@ class BackupMessage(IProcessDesc):
else:
self.cmd += cmd_arg(arg)
def get_server_details(self):
def get_server_name(self):
s = get_server(self.sid)
if s is None:
return _("Not available")
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(self.sid)
@ -113,7 +115,10 @@ class BackupMessage(IProcessDesc):
host = manager.local_bind_host if manager.use_ssh_tunnel else s.host
port = manager.local_bind_port if manager.use_ssh_tunnel else s.port
return s.name, host, port
s.name = html.safe_str(s.name)
host = html.safe_str(host)
port = html.safe_str(port)
return "{0} ({1}:{2})".format(s.name, host, port)
@property
def type_desc(self):
@ -129,77 +134,45 @@ class BackupMessage(IProcessDesc):
@property
def message(self):
name, host, port = self.get_server_details()
name = html.safe_str(name)
host = html.safe_str(host)
port = html.safe_str(port)
server_name = self.get_server_name()
if self.backup_type == BACKUP.OBJECT:
return _(
"Backing up an object on the server '{0}' "
"from database '{1}'"
).format(self.args_str.format(name, host, port),
).format(server_name,
html.safe_str(self.database)
)
if self.backup_type == BACKUP.GLOBALS:
return _("Backing up the global objects on "
"the server '{0}'").format(
self.args_str.format(
name, host, port
)
server_name
)
elif self.backup_type == BACKUP.SERVER:
return _("Backing up the server '{0}'").format(
self.args_str.format(
name, host, port
)
server_name
)
else:
# It should never reach here.
return "Unknown Backup"
def details(self, cmd, args):
name, host, port = self.get_server_details()
res = '<div>'
server_name = self.get_server_name()
backup_type = _("Backup")
if self.backup_type == BACKUP.OBJECT:
msg = _(
"Backing up an object on the server '{0}' "
"from database '{1}'..."
).format(
self.args_str.format(
name, host, port
),
self.database
)
res += html.safe_str(msg)
backup_type = _("Backup Object")
elif self.backup_type == BACKUP.GLOBALS:
msg = _("Backing up the global objects on "
"the server '{0}'...").format(
self.args_str.format(
name, host, port
)
)
res += html.safe_str(msg)
backup_type = _("Backup Globals")
elif self.backup_type == BACKUP.SERVER:
msg = _("Backing up the server '{0}'...").format(
self.args_str.format(
name, host, port
)
)
res += html.safe_str(msg)
else:
# It should never reach here.
res += "Backup"
backup_type = _("Backup Server")
res += '</div><div class="py-1">'
res += _("Running command:")
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(cmd + self.cmd)
res += '</div></div>'
return res
return {
"message": self.message,
"cmd": cmd + self.cmd,
"server": server_name,
"object": self.database,
"type": backup_type,
}
@blueprint.route("/")
@ -487,7 +460,7 @@ def create_backup_objects_job(sid):
# Return response
return make_json_response(
data={'job_id': jid, 'Success': 1}
data={'job_id': jid, 'desc': p.desc.message, 'Success': 1}
)

View File

@ -181,7 +181,7 @@ define([
gettext(data.errormsg)
);
} else {
pgBrowser.Events.trigger('pgadmin-bgprocess:created');
pgBrowser.BgProcessManager.startProcess(data.data.job_id, data.data.desc);
}
},
url_for_utility_exists(id, params){

View File

@ -731,6 +731,7 @@ class BackupCreateJobTest(BaseTestGenerator):
self.class_params['username'],
self.class_params['database']
)
mock_result = server_mock.query.filter_by.return_value
mock_result.first.return_value = mock_obj
@ -743,7 +744,8 @@ class BackupCreateJobTest(BaseTestGenerator):
batch_process_mock.return_value.start = MagicMock(
return_value=True
)
backup_message_mock.message = 'test'
batch_process_mock.return_value.desc = backup_message_mock
export_password_env_mock.return_value = True
server_response = server_utils.connect_server(self, self.server_id)

View File

@ -125,12 +125,13 @@ class BackupMessageTest(BaseTestGenerator):
]
@patch('pgadmin.utils.get_storage_directory')
@patch('pgadmin.tools.backup.BackupMessage.get_server_details')
def runTest(self, get_server_details_mock, get_storage_directory_mock):
get_server_details_mock.return_value = \
self.class_params['name'],\
self.class_params['host'],\
self.class_params['port']
@patch('pgadmin.tools.backup.BackupMessage.get_server_name')
def runTest(self, get_server_name_mock, get_storage_directory_mock):
get_server_name_mock.return_value = "{0} ({1}:{2})"\
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
backup_obj = BackupMessage(
self.class_params['type'],
@ -149,4 +150,4 @@ class BackupMessageTest(BaseTestGenerator):
obj_details = backup_obj.details(self.class_params['cmd'],
self.class_params['args'])
self.assertIn(self.expected_details_cmd, obj_details)
self.assertEqual(self.expected_details_cmd, obj_details['cmd'])

View File

@ -56,16 +56,16 @@ def run_backup_job(tester, job_id, expected_params, assert_in, assert_not_in,
backup_file = None
if 'details' in the_process:
backup_det = the_process['details']
backup_file = backup_det[int(backup_det.find('--file')) +
8:int(backup_det.find('--host')) - 2]
backup_cmd = the_process['details']['cmd']
backup_file = backup_cmd[int(backup_cmd.find('--file')) +
8:int(backup_cmd.find('--host')) - 2]
if expected_params['expected_cmd_opts']:
for opt in expected_params['expected_cmd_opts']:
assert_in(opt, the_process['details'])
assert_in(opt, the_process['details']['cmd'])
if expected_params['not_expected_cmd_opts']:
for opt in expected_params['not_expected_cmd_opts']:
assert_not_in(opt, the_process['details'])
assert_not_in(opt, the_process['details']['cmd'])
# Check the process details
p_details = tester.get('/misc/bgprocess/{0}?_={1}'.format(

View File

@ -13,6 +13,12 @@ from pgadmin.tools.backup import BackupMessage, BACKUP
from pgadmin.utils.route import BaseTestGenerator
from pickle import dumps, loads
from unittest.mock import patch, MagicMock
from pgadmin.utils.preferences import Preferences
import datetime
import pytz
start_time = \
datetime.datetime.now(pytz.utc).strftime("%Y-%m-%d %H:%M:%S.%f %z")
class BatchProcessTest(BaseTestGenerator):
@ -101,13 +107,14 @@ class BatchProcessTest(BaseTestGenerator):
))
]
@patch('pgadmin.tools.backup.BackupMessage.get_server_details')
@patch.object(Preferences, 'module', return_value=MagicMock())
@patch('pgadmin.tools.backup.BackupMessage.get_server_name')
@patch('pgadmin.misc.bgprocess.processes.Popen')
@patch('pgadmin.misc.bgprocess.processes.db')
@patch('pgadmin.tools.backup.current_user')
@patch('pgadmin.misc.bgprocess.processes.current_user')
def runTest(self, current_user_mock, current_user, db_mock,
popen_mock, get_server_details_mock):
popen_mock, get_server_name_mock, pref_module):
with self.app.app_context():
current_user.id = 1
current_user_mock.id = 1
@ -137,10 +144,14 @@ class BatchProcessTest(BaseTestGenerator):
db_mock.session.add.side_effect = db_session_add_mock
db_mock.session.commit = MagicMock(return_value=True)
get_server_details_mock.return_value = \
self.class_params['name'], \
self.class_params['host'], \
self.class_params['port']
pref_module.return_value.preference.return_value.get.\
return_value = 5
get_server_name_mock.return_value = "{0} ({1}:{2})" \
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
backup_obj = BackupMessage(
self.class_params['type'],
@ -171,13 +182,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
mock_result = process_mock.query.filter_by.return_value
mock_result.first.return_value = TestMockProcess(
@ -205,9 +218,7 @@ class BatchProcessTest(BaseTestGenerator):
@patch('pgadmin.misc.bgprocess.processes.Process')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'update_process_info')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'_operate_orphan_process')
def _check_list(self, p, backup_obj, _operate_orphan_process_mock,
def _check_list(self, p, backup_obj,
update_process_info_mock, process_mock,
get_complete_file_path_mock, get_storage_directory_mock,
realpath_mock):
@ -215,13 +226,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
process_mock.query.filter_by.return_value = [
TestMockProcess(backup_obj,
@ -232,7 +245,6 @@ class BatchProcessTest(BaseTestGenerator):
get_complete_file_path_mock.return_value = self.class_params['bfile']
realpath_mock.return_value = self.class_params['bfile']
get_storage_directory_mock.return_value = '//'
_operate_orphan_process_mock.return_value = False
ret_value = p.list()
self.assertEqual(1, len(ret_value))

View File

@ -65,21 +65,21 @@ export default function GrantWizard({ sid, did, nodeInfo, nodeData, onClose }) {
Header: 'Object Type',
accessor: 'object_type',
sortble: true,
sortable: true,
resizable: false,
disableGlobalFilter: true
},
{
Header: 'Schema',
accessor: 'nspname',
sortble: true,
sortable: true,
resizable: false,
disableGlobalFilter: true
},
{
Header: 'Name',
accessor: 'name_with_args',
sortble: true,
sortable: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 280
@ -87,7 +87,7 @@ export default function GrantWizard({ sid, did, nodeInfo, nodeData, onClose }) {
{
Header: 'parameters',
accessor: 'proargs',
sortble: false,
sortable: false,
resizable: false,
disableGlobalFilter: false,
minWidth: 280,
@ -96,7 +96,7 @@ export default function GrantWizard({ sid, did, nodeInfo, nodeData, onClose }) {
{
Header: 'Name',
accessor: 'name',
sortble: false,
sortable: false,
resizable: false,
disableGlobalFilter: false,
minWidth: 280,
@ -105,7 +105,7 @@ export default function GrantWizard({ sid, did, nodeInfo, nodeData, onClose }) {
{
Header: 'ID',
accessor: 'oid',
sortble: false,
sortable: false,
resizable: false,
disableGlobalFilter: false,
minWidth: 280,

View File

@ -90,28 +90,27 @@ class IEMessage(IProcessDesc):
else:
self._cmd += cmd_arg(arg)
def get_server_details(self):
def get_server_name(self):
# Fetch the server details like hostname, port, roles etc
s = Server.query.filter_by(
id=self.sid, user_id=current_user.id
).first()
return s.name, s.host, s.port
if s is None:
return _("Not available")
return html.safe_str("{0} ({1}:{2})".format(s.name, s.host, s.port))
@property
def message(self):
# Fetch the server details like hostname, port, roles etc
name, host, port = self.get_server_details()
return _(
"Copying table data '{0}.{1}' on database '{2}' "
"and server ({3}:{4})"
"and server '{3}'"
).format(
html.safe_str(self.schema),
html.safe_str(self.table),
html.safe_str(self.database),
html.safe_str(host),
html.safe_str(port)
self.get_server_name()
)
@property
@ -121,30 +120,14 @@ class IEMessage(IProcessDesc):
def details(self, cmd, args):
# Fetch the server details like hostname, port, roles etc
name, host, port = self.get_server_details()
res = '<div>'
res += _(
"Copying table data '{0}.{1}' on database '{2}' "
"for the server '{3}'"
).format(
html.safe_str(self.schema),
html.safe_str(self.table),
html.safe_str(self.database),
"{0} ({1}:{2})".format(
html.safe_str(name),
html.safe_str(host),
html.safe_str(port)
)
)
res += '</div><div class="py-1">'
res += _("Running command:")
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(self._cmd)
res += '</div></div>'
return res
return {
"message": self.message,
"cmd": self._cmd,
"server": self.get_server_name(),
"object": "{0}/{1}.{2}".format(self.database, self.schema,
self.table),
"type": _("Import Data") if self.is_import else _("Export Data")
}
@blueprint.route("/")
@ -387,7 +370,7 @@ def create_import_export_job(sid):
# Return response
return make_json_response(
data={'job_id': jid, 'success': 1}
data={'job_id': jid, 'desc': p.desc.message, 'success': 1}
)

View File

@ -82,8 +82,7 @@ define([
gettext(data.errormsg)
);
} else {
Notify.success(gettext('Import/Export job created.'));
pgBrowser.Events.trigger('pgadmin-bgprocess:created');
pgBrowser.BgProcessManager.startProcess(data.data.job_id, data.data.desc);
}
},

View File

@ -13,6 +13,12 @@ from pgadmin.tools.import_export import IEMessage
from pgadmin.utils.route import BaseTestGenerator
from pickle import dumps, loads
from unittest.mock import patch, MagicMock
from pgadmin.utils.preferences import Preferences
import datetime
import pytz
start_time = \
datetime.datetime.now(pytz.utc).strftime("%Y-%m-%d %H:%M:%S.%f %z")
class BatchProcessTest(BaseTestGenerator):
@ -94,13 +100,14 @@ class BatchProcessTest(BaseTestGenerator):
))
]
@patch('pgadmin.tools.import_export.IEMessage.get_server_details')
@patch.object(Preferences, 'module', return_value=MagicMock())
@patch('pgadmin.tools.import_export.IEMessage.get_server_name')
@patch('pgadmin.misc.bgprocess.processes.Popen')
@patch('pgadmin.misc.bgprocess.processes.db')
@patch('pgadmin.tools.import_export.current_user')
@patch('pgadmin.misc.bgprocess.processes.current_user')
def runTest(self, current_user_mock, current_user, db_mock,
popen_mock, get_server_details_mock):
popen_mock, get_server_name_mock, pref_module):
with self.app.app_context():
current_user.id = 1
current_user_mock.id = 1
@ -128,10 +135,14 @@ class BatchProcessTest(BaseTestGenerator):
db_mock.session.add.side_effect = db_session_add_mock
db_mock.session.commit = MagicMock(return_value=True)
get_server_details_mock.return_value = \
self.class_params['name'], \
self.class_params['host'], \
self.class_params['port']
pref_module.return_value.preference.return_value.get. \
return_value = 5
get_server_name_mock.return_value = "{0} ({1}:{2})" \
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
args = self.class_params['args'][1].format(
self.params['schema'],
@ -176,13 +187,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
mock_result = process_mock.query.filter_by.return_value
mock_result.first.return_value = TestMockProcess(
@ -212,9 +225,7 @@ class BatchProcessTest(BaseTestGenerator):
@patch('pgadmin.misc.bgprocess.processes.Process')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'update_process_info')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'_operate_orphan_process')
def _check_list(self, p, import_export_obj, _operate_orphan_process_mock,
def _check_list(self, p, import_export_obj,
update_process_info_mock, process_mock,
get_complete_file_path_mock, get_storage_directory_mock,
realpath_mock):
@ -222,13 +233,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
process_mock.query.filter_by.return_value = [
TestMockProcess(import_export_obj,
@ -239,7 +252,6 @@ class BatchProcessTest(BaseTestGenerator):
get_complete_file_path_mock.return_value = self.params['filename']
realpath_mock.return_value = self.params['filename']
get_storage_directory_mock.return_value = '//'
_operate_orphan_process_mock.return_value = False
ret_value = p.list()
self.assertEqual(1, len(ret_value))

View File

@ -315,6 +315,8 @@ class IECreateJobTest(BaseTestGenerator):
return_value=True
)
ie_message_mock.message = 'test'
batch_process_mock.return_value.desc = ie_message_mock
export_password_env_mock.return_value = True
server_response = server_utils.connect_server(self, self.server_id)

View File

@ -39,7 +39,7 @@ class IEMessageTest(BaseTestGenerator):
]
),
expected_msg="Copying table data '{0}.{1}' on "
"database '{2}' and server ({3}:{4})",
"database '{2}' and server '{3} ({4}:{5})'",
expected_storage_dir='/'
)),
@ -66,7 +66,7 @@ class IEMessageTest(BaseTestGenerator):
]
),
expected_msg="Copying table data '{0}.{1}' on "
"database '{2}' and server ({3}:{4})",
"database '{2}' and server '{3} ({4}:{5})'",
expected_storage_dir='/test_path'
)),
@ -75,17 +75,17 @@ class IEMessageTest(BaseTestGenerator):
@patch('os.path.realpath')
@patch('pgadmin.misc.bgprocess.processes.get_storage_directory')
@patch('pgadmin.misc.bgprocess.processes.get_complete_file_path')
@patch('pgadmin.tools.import_export.IEMessage.get_server_details')
def runTest(self, get_server_details_mock,
@patch('pgadmin.tools.import_export.IEMessage.get_server_name')
def runTest(self, get_server_name_mock,
get_complete_file_path_mock,
get_storage_directory_mock,
realpath_mock):
name = self.class_params['name']
host = self.class_params['host']
port = self.class_params['port']
get_server_details_mock.return_value = name, host, port
get_server_name_mock.return_value = "{0} ({1}:{2})" \
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
get_complete_file_path_mock.return_value \
= self.class_params['filename']
@ -109,6 +109,7 @@ class IEMessageTest(BaseTestGenerator):
self.class_params['schema'],
self.class_params['table'],
self.class_params['database'],
self.class_params['name'],
self.class_params['host'],
self.class_params['port']
)
@ -120,11 +121,11 @@ class IEMessageTest(BaseTestGenerator):
obj_details = import_export_obj.details(self.class_params['cmd'],
self.class_params['args'])
self.assertIn(self.class_params['schema'], obj_details)
self.assertIn(self.class_params['table'], obj_details)
self.assertIn(self.class_params['database'], obj_details)
self.assertIn(self.class_params['host'], obj_details)
self.assertIn(str(self.class_params['port']), obj_details)
self.assertIn(self.class_params['schema'], obj_details['message'])
self.assertIn(self.class_params['table'], obj_details['message'])
self.assertIn(self.class_params['database'], obj_details['message'])
self.assertIn(self.class_params['host'], obj_details['message'])
self.assertIn(str(self.class_params['port']), obj_details['message'])
if config.SERVER_MODE is False:
self.skipTest(

View File

@ -70,7 +70,7 @@ def run_import_export_job(tester, job_id, expected_params, assert_in,
io_file = None
if 'details' in the_process:
io_det = the_process['details']
io_det = the_process['details']['message']
temp_io_det = io_det.upper()
@ -82,10 +82,10 @@ def run_import_export_job(tester, job_id, expected_params, assert_in,
if expected_params['expected_cmd_opts']:
for opt in expected_params['expected_cmd_opts']:
assert_in(opt, the_process['details'])
assert_in(opt, the_process['details']['cmd'])
if expected_params['not_expected_cmd_opts']:
for opt in expected_params['not_expected_cmd_opts']:
assert_not_in(opt, the_process['details'])
assert_not_in(opt, the_process['details']['cmd'])
# Check the process details
p_details = tester.get('/misc/bgprocess/{0}?_={1}'.format(

View File

@ -63,6 +63,9 @@ class Message(IProcessDesc):
def get_server_name(self):
s = get_server(self.sid)
if s is None:
return _("Not available")
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(self.sid)
@ -129,15 +132,13 @@ class Message(IProcessDesc):
return res
def details(self, cmd, args):
res = '<div>' + self.message
res += '</div><div class="py-1">'
res += _("Running Query:")
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(self.query)
res += '</div></div>'
return res
return {
"message": self.message,
"query": self.query,
"server": self.get_server_name(),
"object": self.data['database'],
"type": self.type_desc,
}
@blueprint.route("/")
@ -272,7 +273,7 @@ def create_maintenance_job(sid, did):
# Return response
return make_json_response(
data={'job_id': jid, 'status': True,
data={'job_id': jid, 'desc': p.desc.message, 'status': True,
'info': _('Maintenance job created.')}
)

View File

@ -92,8 +92,7 @@ define([
gettext(data.errormsg)
);
} else {
Notify.success(data.data.info);
pgBrowser.Events.trigger('pgadmin-bgprocess:created');
pgBrowser.BgProcessManager.startProcess(data.data.job_id, data.data.desc);
}
},
setExtraParameters(treeInfo) {

View File

@ -13,6 +13,12 @@ from pgadmin.tools.maintenance import Message
from pgadmin.utils.route import BaseTestGenerator
from pickle import dumps, loads
from unittest.mock import patch, MagicMock
from pgadmin.utils.preferences import Preferences
import datetime
import pytz
start_time = \
datetime.datetime.now(pytz.utc).strftime("%Y-%m-%d %H:%M:%S.%f %z")
class BatchProcessTest(BaseTestGenerator):
@ -53,13 +59,14 @@ class BatchProcessTest(BaseTestGenerator):
))
]
@patch.object(Preferences, 'module', return_value=MagicMock())
@patch('pgadmin.tools.maintenance.Message.get_server_name')
@patch('pgadmin.misc.bgprocess.processes.Popen')
@patch('pgadmin.misc.bgprocess.processes.db')
@patch('pgadmin.tools.maintenance.Server')
@patch('pgadmin.misc.bgprocess.processes.current_user')
def runTest(self, current_user_mock, server_mock, db_mock,
popen_mock, get_server_name_mock):
popen_mock, get_server_name_mock, pref_module):
get_server_name_mock.return_value = self.SERVER_NAME
with self.app.app_context():
current_user_mock.id = 1
@ -87,6 +94,9 @@ class BatchProcessTest(BaseTestGenerator):
db_mock.session.add.side_effect = db_session_add_mock
db_mock.session.commit = MagicMock(return_value=True)
pref_module.return_value.preference.return_value.get. \
return_value = 5
maintenance_obj = Message(
self.class_params['sid'],
self.class_params['data'],
@ -114,13 +124,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
mock_result = process_mock.query.filter_by.return_value
mock_result.first.return_value = TestMockProcess(
@ -146,21 +158,21 @@ class BatchProcessTest(BaseTestGenerator):
@patch('pgadmin.misc.bgprocess.processes.Process')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'update_process_info')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'_operate_orphan_process')
def _check_list(self, p, maintenance_obj, _operate_orphan_process_mock,
def _check_list(self, p, maintenance_obj,
update_process_info_mock, process_mock):
class TestMockProcess():
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
process_mock.query.filter_by.return_value = [
TestMockProcess(maintenance_obj,
@ -169,7 +181,6 @@ class BatchProcessTest(BaseTestGenerator):
]
update_process_info_mock.return_value = [True, True]
_operate_orphan_process_mock.return_value = False
ret_value = p.list()
self.assertEqual(1, len(ret_value))

View File

@ -101,7 +101,7 @@ class MaintenanceJobTest(BaseTestGenerator):
self.assertTrue(the_process['exit_code'] in
self.expected_exit_code)
self.assertIn(self.expected_cmd, the_process['details'])
self.assertIn(self.expected_cmd, the_process['details']['query'])
# Check the process details
p_details = self.tester.get('/misc/bgprocess/{0}?_={1}'.format(

View File

@ -177,6 +177,8 @@ class MaintenanceCreateJobTest(BaseTestGenerator):
batch_process_mock.return_value.start = MagicMock(
return_value=True
)
message_mock.message = 'test'
batch_process_mock.return_value.desc = message_mock
export_password_env_mock.return_value = True
server_response = server_utils.connect_server(self, self.server_id)

View File

@ -128,4 +128,4 @@ class MaintenanceMessageTest(BaseTestGenerator):
# Check the command
obj_details = maintenance_obj.details(self.class_params['cmd'], None)
self.assertIn(self.expected_details_cmd, obj_details)
self.assertIn(self.expected_details_cmd, obj_details['query'])

View File

@ -56,9 +56,10 @@ blueprint = RestoreModule(
class RestoreMessage(IProcessDesc):
def __init__(self, _sid, _bfile, *_args):
def __init__(self, _sid, _bfile, *_args, **_kwargs):
self.sid = _sid
self.bfile = _bfile
self.database = _kwargs['database'] if 'database' in _kwargs else None
self.cmd = ''
def cmd_arg(x):
@ -75,11 +76,12 @@ class RestoreMessage(IProcessDesc):
else:
self.cmd += cmd_arg(arg)
def get_server_details(self):
# Fetch the server details like hostname, port, roles etc
def get_server_name(self):
s = get_server(self.sid)
if s is None:
return _("Not available")
from pgadmin.utils.driver import get_driver
driver = get_driver(PG_DEFAULT_DRIVER)
manager = driver.connection_manager(self.sid)
@ -87,42 +89,28 @@ class RestoreMessage(IProcessDesc):
host = manager.local_bind_host if manager.use_ssh_tunnel else s.host
port = manager.local_bind_port if manager.use_ssh_tunnel else s.port
return s.name, host, port
s.name = html.safe_str(s.name)
host = html.safe_str(host)
port = html.safe_str(port)
return "{0} ({1}:{2})".format(s.name, host, port)
@property
def message(self):
name, host, port = self.get_server_details()
return _("Restoring backup on the server '{0}'").format(
"{0} ({1}:{2})".format(
html.safe_str(name),
html.safe_str(host),
html.safe_str(port)
),
)
return _("Restoring backup on the server '{0}'")\
.format(self.get_server_name())
@property
def type_desc(self):
return _("Restoring backup on the server")
def details(self, cmd, args):
name, host, port = self.get_server_details()
res = '<div>'
res += html.safe_str(
_(
"Restoring backup on the server '{0}'..."
).format(
"{0} ({1}:{2})".format(name, host, port)
)
)
res += '</div><div class="py-1">'
res += _("Running command:")
res += '<div class="pg-bg-cmd enable-selection p-1">'
res += html.safe_str(cmd + self.cmd)
res += '</div></div>'
return res
return {
"message": self.message,
"cmd": cmd + self.cmd,
"server": self.get_server_name(),
"object": getattr(self, 'database', ''),
"type": _("Restore"),
}
@blueprint.route("/")
@ -409,7 +397,8 @@ def create_restore_job(sid):
data['file'].encode('utf-8') if hasattr(
data['file'], 'encode'
) else data['file'],
*args
*args,
database=data['database']
),
cmd=utility, args=args
)
@ -434,7 +423,7 @@ def create_restore_job(sid):
)
# Return response
return make_json_response(
data={'job_id': jid, 'Success': 1}
data={'job_id': jid, 'desc': p.desc.message, 'Success': 1}
)

View File

@ -97,7 +97,7 @@ define('tools.restore', [
gettext(data.errormsg)
);
} else {
pgBrowser.Events.trigger('pgadmin-bgprocess:created');
pgBrowser.BgProcessManager.startProcess(data.data.job_id, data.data.desc);
}
},
setExtraParameters: function(treeInfo, nodeData) {

View File

@ -13,6 +13,12 @@ from pgadmin.tools.restore import RestoreMessage
from pgadmin.utils.route import BaseTestGenerator
from pickle import dumps, loads
from unittest.mock import patch, MagicMock
from pgadmin.utils.preferences import Preferences
import datetime
import pytz
start_time = \
datetime.datetime.now(pytz.utc).strftime("%Y-%m-%d %H:%M:%S.%f %z")
class BatchProcessTest(BaseTestGenerator):
@ -46,13 +52,14 @@ class BatchProcessTest(BaseTestGenerator):
))
]
@patch('pgadmin.tools.restore.RestoreMessage.get_server_details')
@patch.object(Preferences, 'module', return_value=MagicMock())
@patch('pgadmin.tools.restore.RestoreMessage.get_server_name')
@patch('pgadmin.misc.bgprocess.processes.Popen')
@patch('pgadmin.misc.bgprocess.processes.db')
@patch('pgadmin.tools.restore.current_user')
@patch('pgadmin.misc.bgprocess.processes.current_user')
def runTest(self, current_user_mock, current_user, db_mock,
popen_mock, get_server_details_mock):
popen_mock, get_server_name_mock, pref_module):
with self.app.app_context():
current_user.id = 1
current_user_mock.id = 1
@ -75,10 +82,14 @@ class BatchProcessTest(BaseTestGenerator):
self.class_params['database']
))
get_server_details_mock.return_value = \
self.class_params['name'], \
self.class_params['host'], \
self.class_params['port']
pref_module.return_value.preference.return_value.get. \
return_value = 5
get_server_name_mock.return_value = "{0} ({1}:{2})" \
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
db_mock.session.add.side_effect = db_session_add_mock
db_mock.session.commit = MagicMock(return_value=True)
@ -110,13 +121,15 @@ class BatchProcessTest(BaseTestGenerator):
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
mock_result = process_mock.query.filter_by.return_value
mock_result.first.return_value = TestMockProcess(
@ -142,21 +155,21 @@ class BatchProcessTest(BaseTestGenerator):
@patch('pgadmin.misc.bgprocess.processes.Process')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'update_process_info')
@patch('pgadmin.misc.bgprocess.processes.BatchProcess.'
'_operate_orphan_process')
def _check_list(self, p, restore_obj, _operate_orphan_process_mock,
def _check_list(self, p, restore_obj,
update_process_info_mock, process_mock):
class TestMockProcess():
def __init__(self, desc, args, cmd):
self.pid = 1
self.exit_code = 1
self.start_time = '2018-04-17 06:18:56.315445 +0000'
self.start_time = start_time
self.end_time = None
self.desc = dumps(desc)
self.arguments = " ".join(args)
self.command = cmd
self.acknowledge = None
self.process_state = 0
self.utility_pid = 123
self.server_id = None
process_mock.query.filter_by.return_value = [
TestMockProcess(restore_obj,
@ -165,7 +178,6 @@ class BatchProcessTest(BaseTestGenerator):
]
update_process_info_mock.return_value = [True, True]
_operate_orphan_process_mock.return_value = False
ret_value = p.list()
self.assertEqual(1, len(ret_value))

View File

@ -143,10 +143,10 @@ class RestoreJobTest(BaseTestGenerator):
if self.expected_cmd_opts:
for opt in self.expected_cmd_opts:
self.assertIn(opt, the_process['details'])
self.assertIn(opt, the_process['details']['cmd'])
if self.not_expected_cmd_opts:
for opt in self.not_expected_cmd_opts:
self.assertNotIn(opt, the_process['details'])
self.assertNotIn(opt, the_process['details']['cmd'])
# Check the process details
p_details = self.tester.get('/misc/bgprocess/{0}?_={1}'.format(

View File

@ -348,6 +348,8 @@ class RestoreCreateJobTest(BaseTestGenerator):
return_value=True
)
restore_message_mock.message = 'test'
batch_process_mock.return_value.desc = restore_message_mock
export_password_env_mock.return_value = True
server_response = server_utils.connect_server(self, self.server_id)

View File

@ -49,12 +49,13 @@ class RestoreMessageTest(BaseTestGenerator):
))
]
@patch('pgadmin.tools.restore.RestoreMessage.get_server_details')
def runTest(self, get_server_details_mock):
get_server_details_mock.return_value = \
self.class_params['name'],\
self.class_params['host'],\
self.class_params['port']
@patch('pgadmin.tools.restore.RestoreMessage.get_server_name')
def runTest(self, get_server_name_mock):
get_server_name_mock.return_value = "{0} ({1}:{2})" \
.format(
self.class_params['name'],
self.class_params['host'],
self.class_params['port'])
restore_obj = RestoreMessage(
self.class_params['sid'],
@ -68,4 +69,4 @@ class RestoreMessageTest(BaseTestGenerator):
# Check the command
obj_details = restore_obj.details(self.class_params['cmd'],
self.class_params['args'])
self.assertIn(self.expected_details_cmd, obj_details)
self.assertEqual(self.expected_details_cmd, obj_details['cmd'])

View File

@ -82,7 +82,7 @@ class UserManagementCollection extends BaseUISchema {
deps: ['auth_source'],
depChange: (state)=>{
if (obj.isUserNameEnabled(state) && obj.isNew(state) && !isEmptyString(obj.username)) {
state.username = undefined;
return {username: undefined};
}
},
editable: (state)=> {

View File

@ -86,8 +86,10 @@ class NavMenuLocators:
"//div[contains(@class,'wcFrameTitleBar')]" \
"//div[contains(text(),'Process Watcher')]"
process_watcher_detailed_command_canvas_css = \
".bg-process-details .bg-detailed-desc"
process_watcher_detailed_message_css = \
"div[data-test='process-details'] div[data-test='process-message']"
process_watcher_detailed_command_css = \
"div[data-test='process-details'] div[data-test='process-cmd']"
process_watcher_close_button_xpath = \
"//div[contains(@class,'wcFloating')]//" \
@ -107,6 +109,10 @@ class NavMenuLocators:
rcdock_tab = "div.dock-tab-btn[id$='{0}']"
process_start_close_selector = \
"div[data-test='process-popup-start'] button[data-label='Close']"
process_end_close_selector = \
"div[data-test='process-popup-end'] button[data-label='Close']"
process_watcher_error_close_xpath = \
".btn.btn-sm-sq.btn-primary.pg-bg-close > i"

View File

@ -36,5 +36,9 @@ define(function () {
'file_manager.save_file_dialog_view': '/file_manager/save_file_dialog_view/<int:trans_id>',
'file_manager.save_show_hidden_file_option': '/file_manager/save_show_hidden_file_option/<int:trans_id>',
'settings.save_file_format_setting': '/settings/save_file_format_setting/',
'bgprocess.detailed_status': '/misc/bgprocess/<pid>/<int:out>/<int:err>/',
'bgprocess.list': '/misc/bgprocess/',
'bgprocess.stop_process': '/misc/bgprocess/stop/<pid>',
'bgprocess.acknowledge': '/misc/bgprocess/<pid>'
};
});

View File

@ -0,0 +1,120 @@
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
import BgProcessManager, { BgProcessManagerProcessState } from '../../../pgadmin/misc/bgprocess/static/js/BgProcessManager';
import * as BgProcessNotify from '../../../pgadmin/misc/bgprocess/static/js/BgProcessNotify';
describe('BgProcessManager', ()=>{
let obj;
let networkMock;
const pgBrowser = jasmine.createSpyObj('pgBrowser', [], {
docker: {
findPanels: ()=>{/* dummy */},
addPanel: ()=>{/* dummy */}
}
});
beforeAll(()=>{
networkMock = new MockAdapter(axios);
networkMock.onGet('/misc/bgprocess/').reply(200, [{}]);
networkMock.onPut('/misc/bgprocess/stop/12345').reply(200,{});
networkMock.onPut('/misc/bgprocess/12345').reply(200,{});
});
afterAll(() => {
networkMock.restore();
});
beforeEach(()=>{
obj = new BgProcessManager(pgBrowser);
});
it('init', ()=>{
spyOn(obj, 'startWorker');
obj.init();
expect(obj.startWorker).toHaveBeenCalled();
});
it('procList', ()=>{
obj._procList = [{a: '1'}];
expect(obj.procList).toEqual([{a: '1'}]);
});
it('startWorker', (done)=>{
spyOn(obj, 'syncProcesses');
obj._pendingJobId = ['123123123123'];
obj.startWorker();
setTimeout(()=>{
expect(obj.syncProcesses).toHaveBeenCalled();
done();
}, 2000);
});
it('startProcess', ()=>{
let nSpy = spyOn(BgProcessNotify, 'processStarted');
obj.startProcess('12345', 'process desc');
expect(obj._pendingJobId).toEqual(['12345']);
expect(nSpy.calls.mostRecent().args[0]).toBe('process desc');
});
it('stopProcess', (done)=>{
obj._procList = [{
id: '12345',
process_state: BgProcessManagerProcessState.PROCESS_STARTED,
}];
obj.stopProcess('12345');
setTimeout(()=>{
expect(obj._procList[0].process_state).toBe(BgProcessManagerProcessState.PROCESS_TERMINATED);
done();
}, 500);
});
it('acknowledge', (done)=>{
obj._procList = [{
id: '12345',
process_state: BgProcessManagerProcessState.PROCESS_FINISHED,
}];
obj.acknowledge(['12345']);
setTimeout(()=>{
expect(obj._procList.length).toBe(0);
done();
}, 500);
});
it('checkPending', ()=>{
obj._procList = [{
id: '12345',
process_state: BgProcessManagerProcessState.PROCESS_FINISHED,
}];
obj._pendingJobId = ['12345'];
let nSpy = spyOn(BgProcessNotify, 'processCompleted');
obj.checkPending();
expect(nSpy).toHaveBeenCalled();
});
it('openProcessesPanel', ()=>{
const panel = {
focus: ()=>{/* dummy */}
};
spyOn(pgBrowser.docker, 'addPanel').and.returnValue(panel);
/* panel open */
spyOn(pgBrowser.docker, 'findPanels').and.returnValue([panel]);
obj.openProcessesPanel();
expect(pgBrowser.docker.addPanel).not.toHaveBeenCalled();
/* panel closed */
spyOn(pgBrowser.docker, 'findPanels')
.withArgs('processes').and.returnValue([])
.withArgs('properties').and.returnValue([panel]);
obj.openProcessesPanel();
expect(pgBrowser.docker.addPanel).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,75 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import BgProcessManager, { BgProcessManagerProcessState } from '../../../pgadmin/misc/bgprocess/static/js/BgProcessManager';
import pgAdmin from 'sources/pgadmin';
import * as BgProcessNotify from '../../../pgadmin/misc/bgprocess/static/js/BgProcessNotify';
import Notifier from '../../../pgadmin/static/js/helpers/Notifier';
describe('BgProcessNotify', ()=>{
let mount;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
jasmineEnzyme();
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.BgProcessManager = new BgProcessManager(pgAdmin.Browser);
});
it('processStarted', ()=>{
const nspy = spyOn(Notifier, 'notify');
BgProcessNotify.processStarted('some desc', ()=>{/* dummy */});
expect(nspy.calls.mostRecent().args[0].props).toEqual(jasmine.objectContaining({
title: 'Process started',
desc: 'some desc',
}));
});
it('processCompleted success', ()=>{
const nspy = spyOn(Notifier, 'notify');
BgProcessNotify.processCompleted('some desc', BgProcessManagerProcessState.PROCESS_FINISHED, ()=>{/* dummy */});
expect(nspy.calls.mostRecent().args[0].props).toEqual(jasmine.objectContaining({
title: 'Process completed',
desc: 'some desc',
success: true,
}));
});
it('processCompleted failed', ()=>{
const nspy = spyOn(Notifier, 'notify');
BgProcessNotify.processCompleted('some desc', BgProcessManagerProcessState.PROCESS_FAILED, ()=>{/* dummy */});
expect(nspy.calls.mostRecent().args[0].props).toEqual(jasmine.objectContaining({
title: 'Process failed',
desc: 'some desc',
success: false,
}));
});
it('processCompleted terminated', ()=>{
const nspy = spyOn(Notifier, 'notify');
BgProcessNotify.processCompleted('some desc', BgProcessManagerProcessState.PROCESS_TERMINATED, ()=>{/* dummy */});
expect(nspy.calls.mostRecent().args[0].props).toEqual(jasmine.objectContaining({
title: 'Process terminated',
desc: 'some desc',
success: false,
}));
});
});

View File

@ -0,0 +1,76 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import Theme from '../../../pgadmin/static/js/Theme';
import BgProcessManager, { BgProcessManagerProcessState } from '../../../pgadmin/misc/bgprocess/static/js/BgProcessManager';
import pgAdmin from 'sources/pgadmin';
import Processes from '../../../pgadmin/misc/bgprocess/static/js/Processes';
const processData = {
acknowledge: null,
current_storage_dir: null,
desc: 'Doing some operation on the server \'PostgreSQL 12 (localhost:5432)\'',
details: {
cmd: '/Library/PostgreSQL/12/bin/mybin --testing',
message: 'Doing some detailed operation on the server \'PostgreSQL 12 (localhost:5432)\'...'
},
etime: null,
execution_time: 0.09,
exit_code: null,
id: '220803091429498400',
process_state: BgProcessManagerProcessState.PROCESS_STARTED,
stime: '2022-08-03T09:14:30.191940+00:00',
type_desc: 'Operation on the server',
utility_pid: 140391
};
describe('Proceses', ()=>{
let mount;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
jasmineEnzyme();
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.BgProcessManager = new BgProcessManager(pgAdmin.Browser);
pgAdmin.Browser.BgProcessManager._procList = [processData];
});
describe('ProcessDetails', ()=>{
let ctrlMount = (props)=>{
return mount(<Theme>
<Processes
{...props}
/>
</Theme>);
};
it('init', (done)=>{
let ctrl = ctrlMount({});
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('PgTable').length).toBe(1);
done();
}, 1000);
});
});
});

View File

@ -0,0 +1,116 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import Theme from '../../../pgadmin/static/js/Theme';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
import ProcessDetails from '../../../pgadmin/misc/bgprocess/static/js/ProcessDetails';
import BgProcessManager, { BgProcessManagerProcessState } from '../../../pgadmin/misc/bgprocess/static/js/BgProcessManager';
import pgAdmin from 'sources/pgadmin';
import { MESSAGE_TYPE } from '../../../pgadmin/static/js/components/FormComponents';
import _ from 'lodash';
const processData = {
acknowledge: null,
current_storage_dir: null,
desc: 'Doing some operation on the server \'PostgreSQL 12 (localhost:5432)\'',
details: {
cmd: '/Library/PostgreSQL/12/bin/mybin --testing',
message: 'Doing some detailed operation on the server \'PostgreSQL 12 (localhost:5432)\'...'
},
etime: null,
execution_time: 0.09,
exit_code: null,
id: '220803091429498400',
process_state: BgProcessManagerProcessState.PROCESS_STARTED,
stime: '2022-08-03T09:14:30.191940+00:00',
type_desc: 'Operation on the server',
utility_pid: 140391
};
const detailsResponse = {
err: {
done: true,
lines: [['220803091259931276', 'INFO: operation log err']],
pos: 123
},
execution_time: 1.27,
exit_code: 0,
out: {
done: true,
lines: [['220803091259931276', 'INFO: operation log out']],
pos: 123
},
start_time: '2022-08-03 09:12:59.774503 +0000'
};
describe('ProcessDetails', ()=>{
let mount;
let networkMock;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
networkMock = new MockAdapter(axios);
let initialResp = _.cloneDeep(detailsResponse);
initialResp.err.done = false;
initialResp.out.done = false;
initialResp.exit_code = null;
networkMock.onGet(`/misc/bgprocess/${processData.id}/0/0/`).reply(200, initialResp);
networkMock.onGet(`/misc/bgprocess/${processData.id}/123/123/`).reply(200, detailsResponse);
});
afterAll(() => {
mount.cleanUp();
networkMock.restore();
});
beforeEach(()=>{
jasmineEnzyme();
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.BgProcessManager = new BgProcessManager(pgAdmin.Browser);
});
describe('ProcessDetails', ()=>{
let ctrlMount = (props)=>{
return mount(<Theme>
<ProcessDetails
data={processData}
{...props}
/>
</Theme>);
};
it('running and success', (done)=>{
let ctrl = ctrlMount({});
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('NotifierMessage').props()).toEqual(jasmine.objectContaining({
type: MESSAGE_TYPE.INFO,
message: 'Running...',
}));
setTimeout(()=>{
ctrl.update();
expect(ctrl.find('NotifierMessage').props()).toEqual(jasmine.objectContaining({
type: MESSAGE_TYPE.SUCCESS,
message: 'Successfully completed.',
}));
ctrl.unmount();
done();
}, 2000);
}, 500);
});
});
});

View File

@ -6,48 +6,60 @@
# This software is released under the PostgreSQL Licence
#
##########################################################################
import time
from regression.feature_utils.locators import NavMenuLocators
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
def close_bgprocess_popup(tester):
def close_bgprocess_start_popup(tester):
"""
Allows us to close the background process popup window
"""
# In cases where backup div is not closed (sometime due to some error)
try:
tester.page.wait_for_element_to_disappear(
lambda x: tester.driver.find_element(
By.XPATH, ".ajs-message.ajs-bg-bgprocess.ajs-visible"))
except Exception:
tester.driver.find_element(
By.CSS_SELECTOR,
NavMenuLocators.process_watcher_error_close_xpath).click()
tester.driver.find_element(
By.CSS_SELECTOR,
NavMenuLocators.process_start_close_selector).click()
# In cases where restore div is not closed (sometime due to some error)
try:
tester.page.wait_for_element_to_disappear(
lambda x: tester.driver.find_element(
By.XPATH,
"//div[@class='card-header bg-primary d-flex']/div"
"[contains(text(), 'Restoring backup')]"))
except Exception:
tester.driver.find_element(
By.CSS_SELECTOR,
NavMenuLocators.process_watcher_error_close_xpath).click()
# In cases where maintenance window is not closed (sometime due to some
# error)
try:
tester.page.wait_for_element_to_disappear(
lambda x: tester.driver.find_element(
By.XPATH,
"//div[@class='card-header bg-primary d-flex']/div"
"[contains(text(), 'Maintenance')]"))
except Exception:
tester.driver.find_element(
By.CSS_SELECTOR,
NavMenuLocators.process_watcher_error_close_xpath).click()
def wait_for_process_start(tester):
tester.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR,
NavMenuLocators.process_start_close_selector)))
close_bgprocess_start_popup(tester)
def wait_for_process_end(self):
"""This will wait for process to complete dialogue status"""
attempts = 120
status = False
while attempts > 0:
try:
button = self.page.find_by_css_selector(
NavMenuLocators.process_end_close_selector)
status = True
button.click()
break
except Exception:
attempts -= 1
time.sleep(.5)
return status
def open_process_details(tester):
status = wait_for_process_end(tester)
if not status:
raise RuntimeError("Process not completed")
tester.page.click_tab("Processes")
time.sleep(3)
tester.page.find_by_css_selector(
"div[data-test='processes'] "
"div[data-test='row-container']:nth-child(1) "
"div[role='row'] div[role='cell']:nth-child(3) button").click()
tester.page.wait_for_element_to_disappear(
lambda driver: driver.find_element(
By.CSS_SELECTOR, "span[data-test='loading-logs']"))
def close_process_watcher(tester):

View File

@ -1185,28 +1185,6 @@ def check_binary_path_or_skip_test(cls, utility_name):
cls.skipTest(ret_val)
def get_watcher_dialogue_status(self):
"""This will get watcher dialogue status"""
import time
attempts = 120
status = None
while attempts > 0:
try:
status = self.page.find_by_css_selector(
".pg-bg-status-text").text
if 'Failed' in status:
break
if status == 'Started' or status == 'Running...':
attempts -= 1
time.sleep(.5)
else:
break
except Exception:
attempts -= 1
return status
def get_driver_version():
version = getattr(psycopg2, '__version__', None)
return version

View File

@ -473,7 +473,7 @@ module.exports = [{
imports: [
'pure|pgadmin.browser.quick_search',
'pure|pgadmin.tools.user_management',
'pure|pgadmin.browser.bgprocess',
'pure|pgadmin.browser.bgprocessmanager',
'pure|pgadmin.node.server_group',
'pure|pgadmin.node.server',
'pure|pgadmin.node.database',

View File

@ -179,7 +179,7 @@ var webpackShimConfig = {
'pgadmin.about': path.join(__dirname, './pgadmin/about/static/js/about'),
'pgadmin.authenticate.kerberos': path.join(__dirname, './pgadmin/authenticate/static/js/kerberos'),
'pgadmin.browser': path.join(__dirname, './pgadmin/browser/static/js/browser'),
'pgadmin.browser.bgprocess': path.join(__dirname, './pgadmin/misc/bgprocess/static/js/bgprocess'),
'pgadmin.browser.bgprocessmanager': path.join(__dirname, './pgadmin/misc/bgprocess/static/js'),
'pgadmin.browser.collection': path.join(__dirname, './pgadmin/browser/static/js/collection'),
'pgadmin.browser.datamodel': path.join(__dirname, './pgadmin/browser/static/js/datamodel'),
'pgadmin.browser.endpoints': '/browser/js/endpoints',