mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
1) Added support to show all background processes in separate panel. Fixes #3709
2) Port process watcher to React. Fixes #7404
This commit is contained in:
committed by
Akshay Joshi
parent
271b6d91fc
commit
c2b23465cc
243
web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js
Normal file
243
web/pgadmin/misc/bgprocess/static/js/BgProcessManager.js
Normal file
@@ -0,0 +1,243 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
|
||||
import url_for from 'sources/url_for';
|
||||
import Notifier from '../../../../static/js/helpers/Notifier';
|
||||
import EventBus from '../../../../static/js/helpers/EventBus';
|
||||
import * as BgProcessNotify from './BgProcessNotify';
|
||||
import showDetails from './showDetails';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
const WORKER_INTERVAL = 1000;
|
||||
|
||||
export const BgProcessManagerEvents = {
|
||||
LIST_UPDATED: 'LIST_UPDATED',
|
||||
};
|
||||
|
||||
export const BgProcessManagerProcessState = {
|
||||
PROCESS_NOT_STARTED: 0,
|
||||
PROCESS_STARTED: 1,
|
||||
PROCESS_FINISHED: 2,
|
||||
PROCESS_TERMINATED: 3,
|
||||
/* Supported by front end only */
|
||||
PROCESS_TERMINATING: 10,
|
||||
PROCESS_FAILED: 11,
|
||||
};
|
||||
|
||||
|
||||
export default class BgProcessManager {
|
||||
static instance;
|
||||
|
||||
static getInstance(...args) {
|
||||
if (!BgProcessManager.instance) {
|
||||
BgProcessManager.instance = new BgProcessManager(...args);
|
||||
}
|
||||
return BgProcessManager.instance;
|
||||
}
|
||||
|
||||
constructor(pgBrowser) {
|
||||
this.api = getApiInstance();
|
||||
this.pgBrowser = pgBrowser;
|
||||
this._procList = [];
|
||||
this._workerId = null;
|
||||
this._pendingJobId = [];
|
||||
this._eventManager = new EventBus();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.initialized = true;
|
||||
this.startWorker();
|
||||
}
|
||||
|
||||
get procList() {
|
||||
return this._procList;
|
||||
}
|
||||
|
||||
set procList(val) {
|
||||
throw new Error('Property processList is readonly.', val);
|
||||
}
|
||||
|
||||
async startWorker() {
|
||||
let self = this;
|
||||
await self.syncProcesses();
|
||||
/* Fill the pending jobs initially */
|
||||
self._pendingJobId = this.procList.filter((p)=>(p.process_state == BgProcessManagerProcessState.PROCESS_STARTED)).map((p)=>p.id);
|
||||
this._workerId = setInterval(()=>{
|
||||
if(self._pendingJobId.length > 0) {
|
||||
self.syncProcesses();
|
||||
}
|
||||
}, WORKER_INTERVAL);
|
||||
}
|
||||
|
||||
evaluateProcessState(p) {
|
||||
let retState = p.process_state;
|
||||
if((p.etime || p.exit_code !=null) && p.process_state == BgProcessManagerProcessState.PROCESS_STARTED) {
|
||||
retState = BgProcessManagerProcessState.PROCESS_FINISHED;
|
||||
}
|
||||
if(retState == BgProcessManagerProcessState.PROCESS_FINISHED && p.exit_code != 0) {
|
||||
retState = BgProcessManagerProcessState.PROCESS_FAILED;
|
||||
}
|
||||
return retState;
|
||||
}
|
||||
|
||||
async syncProcesses() {
|
||||
try {
|
||||
let {data: resData} = await this.api.get(url_for('bgprocess.list'));
|
||||
this._procList = resData?.map((p)=>{
|
||||
let processState = this.evaluateProcessState(p);
|
||||
return {
|
||||
...p,
|
||||
process_state: processState,
|
||||
canDrop: ![BgProcessManagerProcessState.PROCESS_NOT_STARTED, BgProcessManagerProcessState.PROCESS_STARTED].includes(processState),
|
||||
};
|
||||
});
|
||||
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
|
||||
this.checkPending();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
checkPending() {
|
||||
const completedProcIds = this.procList.filter((p)=>{
|
||||
if(![
|
||||
BgProcessManagerProcessState.PROCESS_NOT_STARTED,
|
||||
BgProcessManagerProcessState.PROCESS_STARTED,
|
||||
BgProcessManagerProcessState.PROCESS_TERMINATING].includes(p.process_state)) {
|
||||
return true;
|
||||
}
|
||||
}).map((p)=>p.id);
|
||||
this._pendingJobId = this._pendingJobId.filter((id)=>{
|
||||
if(completedProcIds.includes(id)) {
|
||||
let p = this.procList.find((p)=>p.id==id);
|
||||
BgProcessNotify.processCompleted(p?.desc, p?.process_state, this.openProcessesPanel.bind(this));
|
||||
if(p.server_id != null) {
|
||||
this.updateCloudDetails(p.id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
startProcess(jobId, desc) {
|
||||
if(jobId) {
|
||||
this._pendingJobId.push(jobId);
|
||||
BgProcessNotify.processStarted(desc, this.openProcessesPanel.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
stopProcess(jobId) {
|
||||
this.procList.find((p)=>p.id == jobId).process_state = BgProcessManagerProcessState.PROCESS_TERMINATING;
|
||||
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
|
||||
this.api.put(url_for('bgprocess.stop_process', {
|
||||
pid: jobId,
|
||||
}))
|
||||
.then(()=>{
|
||||
this.procList.find((p)=>p.id == jobId).process_state = BgProcessManagerProcessState.PROCESS_TERMINATED;
|
||||
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
|
||||
})
|
||||
.catch((err)=>{
|
||||
Notifier.error(parseApiError(err));
|
||||
});
|
||||
}
|
||||
|
||||
acknowledge(jobIds) {
|
||||
const removeJob = (jobId)=>{
|
||||
this._procList = this.procList.filter((p)=>p.id!=jobId);
|
||||
this._eventManager.fireEvent(BgProcessManagerEvents.LIST_UPDATED);
|
||||
};
|
||||
jobIds.forEach((jobId)=>{
|
||||
this.api.put(url_for('bgprocess.acknowledge', {
|
||||
pid: jobId,
|
||||
}))
|
||||
.then(()=>{
|
||||
removeJob(jobId);
|
||||
})
|
||||
.catch((err)=>{
|
||||
if(err.response?.status == 410) {
|
||||
/* Object not available */
|
||||
removeJob(jobId);
|
||||
} else {
|
||||
Notifier.error(parseApiError(err));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
viewJobDetails(jobId) {
|
||||
showDetails(this.procList.find((p)=>p.id==jobId));
|
||||
}
|
||||
|
||||
updateCloudDetails(jobId) {
|
||||
this.api.put(url_for('bgprocess.update_cloud_details', {
|
||||
pid: jobId,
|
||||
}))
|
||||
.then((res)=>{
|
||||
let _server = res.data?.data?.node;
|
||||
if(!_server) {
|
||||
Notifier.error(gettext('Cloud server information not available'));
|
||||
return;
|
||||
}
|
||||
let _server_path = '/browser/server_group_' + _server.gid + '/' + _server.id,
|
||||
_tree = this.pgBrowser.tree,
|
||||
_item = _tree.findNode(_server_path);
|
||||
|
||||
if (_item) {
|
||||
if(_server.status) {
|
||||
let _dom = _item.domNode;
|
||||
_tree.addIcon(_dom, {icon: _server.icon});
|
||||
let d = _tree.itemData(_dom);
|
||||
d.cloud_status = _server.cloud_status;
|
||||
_tree.update(_dom, d);
|
||||
}
|
||||
else {
|
||||
_tree.remove(_item.domNode);
|
||||
_tree.refresh(_item.domNode.parent);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
if(err.response?.status != 410) {
|
||||
Notifier.error(gettext('Failed Cloud Deployment.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recheckCloudServer(sid) {
|
||||
let self = this;
|
||||
let process = self.procList.find((p)=>p.server_id==sid);
|
||||
if(process) {
|
||||
this.updateCloudDetails(process.id);
|
||||
}
|
||||
}
|
||||
|
||||
openProcessesPanel() {
|
||||
let processPanel = this.pgBrowser.docker.findPanels('processes');
|
||||
if(processPanel.length > 0) {
|
||||
processPanel = processPanel[0];
|
||||
} else {
|
||||
let propertiesPanel = this.pgBrowser.docker.findPanels('properties');
|
||||
processPanel = this.pgBrowser.docker.addPanel('processes', window.wcDocker.DOCK.STACKED, propertiesPanel[0]);
|
||||
}
|
||||
processPanel.focus();
|
||||
}
|
||||
|
||||
registerListener(event, callback) {
|
||||
this._eventManager.registerListener(event, callback);
|
||||
}
|
||||
|
||||
deregisterListener(event, callback) {
|
||||
this._eventManager.deregisterListener(event, callback);
|
||||
}
|
||||
}
|
||||
98
web/pgadmin/misc/bgprocess/static/js/BgProcessNotify.jsx
Normal file
98
web/pgadmin/misc/bgprocess/static/js/BgProcessNotify.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import Notifier from '../../../../static/js/helpers/Notifier';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import { DefaultButton, PgIconButton } from '../../../../static/js/components/Buttons';
|
||||
import clsx from 'clsx';
|
||||
import DescriptionOutlinedIcon from '@material-ui/icons/DescriptionOutlined';
|
||||
import { BgProcessManagerProcessState } from './BgProcessManager';
|
||||
import PropTypes from 'prop-types';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
container: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: '0.25rem 1rem 1rem',
|
||||
minWidth: '325px',
|
||||
...theme.mixins.panelBorder.all,
|
||||
},
|
||||
containerHeader: {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: 'bold',
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: 'inherit',
|
||||
borderTopRightRadius: 'inherit',
|
||||
},
|
||||
containerBody: {
|
||||
marginTop: '1rem',
|
||||
},
|
||||
containerSuccess: {
|
||||
borderColor: theme.palette.success.main,
|
||||
backgroundColor: theme.palette.success.light,
|
||||
},
|
||||
iconSuccess: {
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
containerError: {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.light,
|
||||
},
|
||||
iconError: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
}));
|
||||
|
||||
function ProcessNotifyMessage({title, desc, onClose, onViewProcess, success=true, dataTestSuffix=''}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Box className={clsx(classes.container, (success ? classes.containerSuccess : classes.containerError))} data-test={'process-popup-' + dataTestSuffix}>
|
||||
<Box display="flex" justifyContent="space-between" className={classes.containerHeader}>
|
||||
<Box marginRight={'1rem'}>{title}</Box>
|
||||
<PgIconButton size="xs" noBorder icon={<CloseIcon />} onClick={onClose} title={'Close'} className={success ? classes.iconSuccess : classes.iconError} />
|
||||
</Box>
|
||||
<Box className={classes.containerBody}>
|
||||
<Box>{desc}</Box>
|
||||
<Box marginTop={'1rem'} display="flex">
|
||||
<DefaultButton startIcon={<DescriptionOutlinedIcon />} onClick={onViewProcess}>View Processes</DefaultButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
ProcessNotifyMessage.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
onViewProcess: PropTypes.func,
|
||||
success: PropTypes.bool,
|
||||
dataTestSuffix: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
export function processStarted(desc, onViewProcess) {
|
||||
Notifier.notify(
|
||||
<ProcessNotifyMessage title={gettext('Process started')} desc={desc} onViewProcess={onViewProcess} dataTestSuffix="start"/>,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function processCompleted(desc, process_state, onViewProcess) {
|
||||
let title = gettext('Process completed');
|
||||
let success = true;
|
||||
if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATED) {
|
||||
title = gettext('Process terminated');
|
||||
success = false;
|
||||
} else if(process_state == BgProcessManagerProcessState.PROCESS_FAILED) {
|
||||
title = gettext('Process failed');
|
||||
success = false;
|
||||
}
|
||||
|
||||
Notifier.notify(
|
||||
<ProcessNotifyMessage title={title} desc={desc} onViewProcess={onViewProcess} success={success} dataTestSuffix="end"/>,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
182
web/pgadmin/misc/bgprocess/static/js/ProcessDetails.jsx
Normal file
182
web/pgadmin/misc/bgprocess/static/js/ProcessDetails.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MESSAGE_TYPE, NotifierMessage } from '../../../../static/js/components/FormComponents';
|
||||
import { BgProcessManagerProcessState } from './BgProcessManager';
|
||||
import { DefaultButton, PgIconButton } from '../../../../static/js/components/Buttons';
|
||||
import HighlightOffRoundedIcon from '@material-ui/icons/HighlightOffRounded';
|
||||
import AccessTimeRoundedIcon from '@material-ui/icons/AccessTimeRounded';
|
||||
import { useInterval } from '../../../../static/js/custom_hooks';
|
||||
import getApiInstance from '../../../../static/js/api_instance';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import FolderSharedRoundedIcon from '@material-ui/icons/FolderSharedRounded';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
container: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '8px',
|
||||
userSelect: 'text',
|
||||
},
|
||||
cmd: {
|
||||
...theme.mixins.panelBorder.all,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.otherVars.inputDisabledBg,
|
||||
wordBreak: 'break-word',
|
||||
margin: '8px 0px',
|
||||
padding: '4px',
|
||||
},
|
||||
logs: {
|
||||
flexGrow: 1,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: '4px',
|
||||
overflow: 'auto',
|
||||
textOverflow: 'wrap-text',
|
||||
margin: '8px 0px',
|
||||
...theme.mixins.panelBorder.all,
|
||||
},
|
||||
logErr: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
terminateBtn: {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText,
|
||||
border: 0,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: theme.palette.error.contrastText,
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.error.contrastText + ' !important',
|
||||
border: 0,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
async function getDetailedStatus(api, jobId, out, err) {
|
||||
let res = await api.get(url_for(
|
||||
'bgprocess.detailed_status', {
|
||||
'pid': jobId,
|
||||
'out': out,
|
||||
'err': err,
|
||||
}
|
||||
));
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export default function ProcessDetails({data}) {
|
||||
const classes = useStyles();
|
||||
const api = useMemo(()=>getApiInstance());
|
||||
const [logs, setLogs] = useState(null);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [[outPos, errPos], setOutErrPos] = useState([0, 0]);
|
||||
const [exitCode, setExitCode] = useState(data.exit_code);
|
||||
const [timeTaken, setTimeTaken] = useState(data.execution_time);
|
||||
|
||||
let notifyType = MESSAGE_TYPE.INFO;
|
||||
let notifyText = gettext('Not started');
|
||||
|
||||
const process_state = pgAdmin.Browser.BgProcessManager.evaluateProcessState({
|
||||
...data,
|
||||
exit_code: exitCode,
|
||||
});
|
||||
|
||||
if(process_state == BgProcessManagerProcessState.PROCESS_STARTED) {
|
||||
notifyText = gettext('Running...');
|
||||
} else if(process_state == BgProcessManagerProcessState.PROCESS_FINISHED) {
|
||||
notifyType = MESSAGE_TYPE.SUCCESS;
|
||||
notifyText = gettext('Successfully completed.');
|
||||
} else if(process_state == BgProcessManagerProcessState.PROCESS_FAILED) {
|
||||
notifyType = MESSAGE_TYPE.ERROR;
|
||||
notifyText = gettext('Failed (exit code: %s).', String(exitCode));
|
||||
} else if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATED) {
|
||||
notifyType = MESSAGE_TYPE.ERROR;
|
||||
notifyText = gettext('Terminated by user.');
|
||||
} else if(process_state == BgProcessManagerProcessState.PROCESS_TERMINATING) {
|
||||
notifyText = gettext('Terminating the process...');
|
||||
}
|
||||
|
||||
useInterval(async ()=>{
|
||||
const logsSortComp = (l1, l2)=>{
|
||||
return l1[0].localeCompare(l2[0]);
|
||||
};
|
||||
let resData = await getDetailedStatus(api, data.id, outPos, errPos);
|
||||
resData.out.lines.sort(logsSortComp);
|
||||
resData.err.lines.sort(logsSortComp);
|
||||
if(resData.out?.done && resData.err?.done && resData.exit_code != null) {
|
||||
setExitCode(resData.exit_code);
|
||||
setCompleted(true);
|
||||
}
|
||||
setTimeTaken(resData.execution_time);
|
||||
setOutErrPos([resData.out.pos, resData.err.pos]);
|
||||
setLogs((prevLogs)=>{
|
||||
return [
|
||||
...(prevLogs || []),
|
||||
...resData.out.lines.map((l)=>l[1]),
|
||||
...resData.err.lines.map((l)=>l[1]),
|
||||
];
|
||||
});
|
||||
|
||||
}, completed ? -1 : 1000);
|
||||
|
||||
const errRe = new RegExp(': (' + gettext('error') + '|' + gettext('fatal') + '):', 'i');
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" className={classes.container} data-test="process-details">
|
||||
<Box data-test="process-message">{data.details?.message}</Box>
|
||||
{data.details?.cmd && <>
|
||||
<Box>{gettext('Running command')}:</Box>
|
||||
<Box data-test="process-cmd" className={classes.cmd}>{data.details.cmd}</Box>
|
||||
</>}
|
||||
{data.details?.query && <>
|
||||
<Box>{gettext('Running query')}:</Box>
|
||||
<Box data-test="process-cmd" className={classes.cmd}>{data.details.query}</Box>
|
||||
</>}
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Box><span><AccessTimeRoundedIcon /> {gettext('Start time')}: {new Date(data.stime).toString()}</span></Box>
|
||||
<Box>
|
||||
{pgAdmin.server_mode == 'True' && data.current_storage_dir &&
|
||||
<PgIconButton icon={<FolderSharedRoundedIcon />} title={gettext('Storage Manager')} onClick={()=>{
|
||||
pgAdmin.Tools.FileManager.openStorageManager(data.current_storage_dir);
|
||||
}} style={{marginRight: '4px'}} />}
|
||||
<DefaultButton disabled={process_state != BgProcessManagerProcessState.PROCESS_STARTED || data.server_id != null}
|
||||
startIcon={<HighlightOffRoundedIcon />} className={classes.terminateBtn}>Stop Process</DefaultButton></Box>
|
||||
</Box>
|
||||
<Box flexGrow={1} className={classes.logs}>
|
||||
{logs == null && <span data-test="loading-logs">{gettext('Loading process logs...')}</span>}
|
||||
{logs?.length == 0 && gettext('No logs available.')}
|
||||
{logs?.map((log, i)=>{
|
||||
return <div ref={(el)=>{
|
||||
if(i==logs.length-1) {
|
||||
el?.scrollIntoView();
|
||||
}
|
||||
}} key={i} className={errRe.test(log) ? classes.logErr : ''}>{log}</div>;
|
||||
})}
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center">
|
||||
<NotifierMessage type={notifyType} message={notifyText} closable={false} textCenter={true} style={{flexGrow: 1, marginRight: '8px'}} />
|
||||
<Box>{gettext('Execution time')}: {timeTaken} {gettext('seconds')}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
ProcessDetails.propTypes = {
|
||||
closeModal: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
onOK: PropTypes.func,
|
||||
setHeight: PropTypes.func
|
||||
};
|
||||
290
web/pgadmin/misc/bgprocess/static/js/Processes.jsx
Normal file
290
web/pgadmin/misc/bgprocess/static/js/Processes.jsx
Normal file
@@ -0,0 +1,290 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import PgTable from 'sources/components/PgTable';
|
||||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import { BgProcessManagerEvents, BgProcessManagerProcessState } from './BgProcessManager';
|
||||
import { PgIconButton } from '../../../../static/js/components/Buttons';
|
||||
import CancelIcon from '@material-ui/icons/Cancel';
|
||||
import DescriptionOutlinedIcon from '@material-ui/icons/DescriptionOutlined';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import HelpIcon from '@material-ui/icons/HelpRounded';
|
||||
import url_for from 'sources/url_for';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stopButton: {
|
||||
color: theme.palette.error.main
|
||||
},
|
||||
buttonClick: {
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
},
|
||||
emptyPanel: {
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
background: theme.otherVars.emptySpaceBg,
|
||||
overflow: 'auto',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
},
|
||||
panelIcon: {
|
||||
width: '80%',
|
||||
margin: '0 auto',
|
||||
marginTop: '25px !important',
|
||||
position: 'relative',
|
||||
textAlign: 'center',
|
||||
},
|
||||
panelMessage: {
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
autoResizer: {
|
||||
height: '100% !important',
|
||||
width: '100% !important',
|
||||
background: theme.palette.grey[400],
|
||||
padding: '7.5px',
|
||||
overflow: 'auto !important',
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
},
|
||||
noPadding: {
|
||||
padding: 0,
|
||||
},
|
||||
bgSucess: {
|
||||
backgroundColor: theme.palette.success.light,
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
},
|
||||
bgFailed: {
|
||||
backgroundColor: theme.palette.error.light,
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
},
|
||||
bgTerm: {
|
||||
backgroundColor: theme.palette.warning.light,
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
},
|
||||
bgRunning: {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const ProcessStateTextAndColor = {
|
||||
[BgProcessManagerProcessState.PROCESS_NOT_STARTED]: [gettext('Not started'), 'bgRunning'],
|
||||
[BgProcessManagerProcessState.PROCESS_STARTED]: [gettext('Running'), 'bgRunning'],
|
||||
[BgProcessManagerProcessState.PROCESS_FINISHED]: [gettext('Finished'), 'bgSucess'],
|
||||
[BgProcessManagerProcessState.PROCESS_TERMINATED]: [gettext('Terminated'), 'bgTerm'],
|
||||
[BgProcessManagerProcessState.PROCESS_TERMINATING]: [gettext('Terminating...'), 'bgTerm'],
|
||||
[BgProcessManagerProcessState.PROCESS_FAILED]: [gettext('Failed'), 'bgFailed'],
|
||||
};
|
||||
|
||||
export default function Processes() {
|
||||
const classes = useStyles();
|
||||
const [tableData, setTableData] = React.useState([]);
|
||||
const [selectedRows, setSelectedRows] = React.useState([]);
|
||||
|
||||
let columns = [
|
||||
{
|
||||
accessor: 'stop_process',
|
||||
Header: () => null,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
disableGlobalFilter: true,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
minWidth: 35,
|
||||
id: 'btn-stop',
|
||||
// eslint-disable-next-line react/display-name
|
||||
Cell: ({ row }) => {
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<CancelIcon />}
|
||||
className={classes.stopButton}
|
||||
disabled={row.original.process_state != BgProcessManagerProcessState.PROCESS_STARTED
|
||||
|| row.original.server_id != null}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
pgAdmin.Browser.BgProcessManager.stopProcess(row.original.id);
|
||||
}}
|
||||
aria-label="Stop Process"
|
||||
title={gettext('Stop Process')}
|
||||
></PgIconButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: 'view_details',
|
||||
Header: () => null,
|
||||
sortable: false,
|
||||
resizable: false,
|
||||
disableGlobalFilter: true,
|
||||
width: 35,
|
||||
maxWidth: 35,
|
||||
minWidth: 35,
|
||||
id: 'btn-logs',
|
||||
// eslint-disable-next-line react/display-name
|
||||
Cell: ({ row }) => {
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
icon={<DescriptionOutlinedIcon />}
|
||||
noBorder
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
pgAdmin.Browser.BgProcessManager.viewJobDetails(row.original.id);
|
||||
}}
|
||||
aria-label="View details"
|
||||
title={gettext('View details')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: gettext('PID'),
|
||||
accessor: 'utility_pid',
|
||||
sortable: true,
|
||||
resizable: false,
|
||||
width: 70,
|
||||
minWidth: 70,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Type'),
|
||||
accessor: (row)=>row.details?.type,
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
width: 100,
|
||||
minWidth: 70,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Server'),
|
||||
accessor: (row)=>row.details?.server,
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
width: 200,
|
||||
minWidth: 120,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
Header: gettext('Object'),
|
||||
accessor: (row)=>row.details?.object,
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
width: 200,
|
||||
minWidth: 120,
|
||||
disableGlobalFilter: false,
|
||||
},
|
||||
{
|
||||
id: 'stime',
|
||||
Header: gettext('Start Time'),
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: true,
|
||||
width: 150,
|
||||
minWidth: 150,
|
||||
accessor: (row)=>(new Date(row.stime)),
|
||||
Cell: ({row})=>(new Date(row.original.stime).toLocaleString()),
|
||||
},
|
||||
{
|
||||
Header: gettext('Status'),
|
||||
sortable: true,
|
||||
resizable: false,
|
||||
disableGlobalFilter: false,
|
||||
width: 120,
|
||||
minWidth: 120,
|
||||
accessor: (row)=>ProcessStateTextAndColor[row.process_state][0],
|
||||
dataClassName: classes.noPadding,
|
||||
Cell: ({row})=>{
|
||||
const [text, bgcolor] = ProcessStateTextAndColor[row.original.process_state];
|
||||
return <Box className={classes[bgcolor]}>{text}</Box>;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: gettext('Time Taken'),
|
||||
accessor: 'execution_time',
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
disableGlobalFilter: true,
|
||||
},
|
||||
];
|
||||
|
||||
const updateList = ()=>{
|
||||
if(pgAdmin.Browser.BgProcessManager.procList) {
|
||||
setTableData([...pgAdmin.Browser.BgProcessManager.procList]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateList();
|
||||
pgAdmin.Browser.BgProcessManager.registerListener(BgProcessManagerEvents.LIST_UPDATED, updateList);
|
||||
return ()=>{
|
||||
pgAdmin.Browser.BgProcessManager.deregisterListener(BgProcessManagerEvents.LIST_UPDATED, updateList);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PgTable
|
||||
data-test="processes"
|
||||
className={classes.autoResizer}
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
sortOptions={[{id: 'stime', desc: true}]}
|
||||
getSelectedRows={(rows)=>{setSelectedRows(rows);}}
|
||||
isSelectRow={true}
|
||||
CustomHeader={()=>{
|
||||
return (
|
||||
<Box>
|
||||
<PgIconButton
|
||||
className={classes.dropButton}
|
||||
icon={<DeleteIcon/>}
|
||||
aria-label="Acknowledge and Remove"
|
||||
title={gettext('Acknowledge and Remove')}
|
||||
onClick={() => {
|
||||
pgAdmin.Browser.BgProcessManager.acknowledge(selectedRows.map((p)=>p.original.id));
|
||||
}}
|
||||
disabled={selectedRows.length <= 0}
|
||||
></PgIconButton>
|
||||
<PgIconButton
|
||||
icon={<HelpIcon/>}
|
||||
aria-label="Help"
|
||||
title={gettext('Help')}
|
||||
style={{marginLeft: '8px'}}
|
||||
onClick={() => {
|
||||
window.open(url_for('help.static', {'filename': 'processes.html'}));
|
||||
}}
|
||||
></PgIconButton>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
></PgTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Processes.propTypes = {
|
||||
res: PropTypes.array,
|
||||
nodeData: PropTypes.object,
|
||||
treeNodeInfo: PropTypes.object,
|
||||
node: PropTypes.func,
|
||||
item: PropTypes.object,
|
||||
row: PropTypes.object,
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
22
web/pgadmin/misc/bgprocess/static/js/index.js
Normal file
22
web/pgadmin/misc/bgprocess/static/js/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import pgBrowser from 'top/browser/static/js/browser';
|
||||
import BgProcessManager from './BgProcessManager';
|
||||
|
||||
if (!pgAdmin.Browser) {
|
||||
pgAdmin.Browser = {};
|
||||
}
|
||||
|
||||
pgAdmin.Browser.BgProcessManager = BgProcessManager.getInstance(pgBrowser);
|
||||
|
||||
module.exports = {
|
||||
BgProcessManager: BgProcessManager,
|
||||
};
|
||||
|
||||
31
web/pgadmin/misc/bgprocess/static/js/showDetails.jsx
Normal file
31
web/pgadmin/misc/bgprocess/static/js/showDetails.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import Theme from '../../../../static/js/Theme';
|
||||
import ProcessDetails from './ProcessDetails';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
export default function showDetails(p) {
|
||||
let pgBrowser = pgAdmin.Browser;
|
||||
|
||||
// Register dialog panel
|
||||
pgBrowser.Node.registerUtilityPanel();
|
||||
let panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
|
||||
j = panel.$container.find('.obj_properties').first();
|
||||
panel.title(gettext('Process Watcher - %s', p.type_desc));
|
||||
panel.focus();
|
||||
|
||||
panel.on(window.wcDocker.EVENT.CLOSED, ()=>{
|
||||
ReactDOM.unmountComponentAtNode(j[0]);
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<Theme>
|
||||
<ProcessDetails
|
||||
data={p}
|
||||
closeModal={()=>{
|
||||
panel.close();
|
||||
}}
|
||||
/>
|
||||
</Theme>, j[0]);
|
||||
}
|
||||
Reference in New Issue
Block a user