1) Added support to show all background processes in separate panel. Fixes #3709
2) Port process watcher to React. Fixes #7404pull/90/head
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 155 KiB |
After Width: | Height: | Size: 224 KiB |
After Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.4 KiB |
|
@ -23,6 +23,7 @@ of database objects.
|
|||
management_basics
|
||||
backup_and_restore
|
||||
developer_tools
|
||||
processes
|
||||
pgagent
|
||||
contributions
|
||||
release_notes
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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')},
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(){
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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> ` + 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> ` + 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> ';
|
||||
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> ' + 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;
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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]);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -242,7 +242,7 @@ export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTe
|
|||
onSearchTextChange(value);
|
||||
setSearchText(value);
|
||||
}}
|
||||
placeholder={'Search'}>
|
||||
placeholder={gettext('Search')}>
|
||||
</InputText>
|
||||
</Box>
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ export function useInterval(callback, delay) {
|
|||
}
|
||||
|
||||
if(delay > -1) {
|
||||
tick();
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
|
|
|
@ -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='') {
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.')}
|
||||
)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)=> {
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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>'
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|