parent
3bb9f0ba8c
commit
f8fa1cf6d6
10
README.md
10
README.md
|
@ -14,7 +14,7 @@ and ReactJS, HTML5 with CSS for the client side processing and UI.
|
|||
|
||||
Although developed using web technologies, pgAdmin 4 can be deployed either on
|
||||
a web server using a browser, or standalone on a workstation. The runtime/
|
||||
subdirectory contains an NWjs based runtime application intended to allow this,
|
||||
subdirectory contains an Electron based runtime application intended to allow this,
|
||||
which will execute the Python server and display the UI.
|
||||
|
||||
## Building the Runtime
|
||||
|
@ -35,13 +35,7 @@ paths it would expect to find in the standard package for your platform.
|
|||
You can then execute the runtime by running something like:
|
||||
|
||||
```bash
|
||||
node_modules/nw/nwjs/nw .
|
||||
```
|
||||
|
||||
or on macOS:
|
||||
|
||||
```bash
|
||||
node_modules/nw/nwjs/nwjs.app/Contents/MacOS/nwjs .
|
||||
yarn run start
|
||||
```
|
||||
|
||||
# Configuring the Python Environment
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
The bulk of pgAdmin is a Python web application written using the Flask framework
|
||||
on the backend, and HTML5 with CSS3,ReactJS on the front end. A
|
||||
desktop runtime is also included for users that prefer a desktop application to
|
||||
a web application, which is written using NWjs (Node Webkit).
|
||||
a web application, which is written using Electron.
|
||||
|
||||
Runtime
|
||||
*******
|
||||
|
||||
The runtime is based on NWjs which integrates a browser and the Python server
|
||||
The runtime is based on Electron which integrates a browser and the Python server
|
||||
creating a standalone application. The source code can be found in the
|
||||
**/runtime** directory in the source tree.
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ See :ref:`config_py` for more information on configuration settings.
|
|||
Desktop Runtime Standalone Application
|
||||
======================================
|
||||
|
||||
The Desktop Runtime is based on `NWjs <https://nwjs.io/>`_ which integrates a
|
||||
The Desktop Runtime is based on `Electron <https://www.electronjs.org/>`_ which integrates a
|
||||
browser and the Python server creating a standalone application.
|
||||
|
||||
.. image:: images/runtime_standalone.png
|
||||
|
|
|
@ -153,8 +153,6 @@ _build_runtime() {
|
|||
# Copy electron into the staging directory
|
||||
mkdir -p "${DESKTOPROOT}/usr/${APP_NAME}/bin"
|
||||
|
||||
# The chmod command below is needed to fix the permission issue of
|
||||
# the NWjs binaries and files.
|
||||
# Change the permission for others and group the same as the owner
|
||||
chmod -R og=u "${BUILDROOT}/electron-v${ELECTRON_VERSION}-linux-${ELECTRON_ARCH}"/*
|
||||
# Explicitly remove write permissions for others and group
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<dict>
|
||||
<!--
|
||||
Disable Sandboxing. This must be enabled for the app store, but it will
|
||||
cause NWjs to fail to start with an error like:
|
||||
cause Electron to fail to start with an error like:
|
||||
|
||||
[1004/170922.238911:ERROR:directory_reader_posix.cc(42)] opendir /dev/fd: Operation not permitted (1)
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
|||
<string>%TEAMID%.org.pgadmin.pgadmin4</string>
|
||||
|
||||
<!--
|
||||
We have no need for JIT on x86_64, but NWJS won't start without it
|
||||
We have no need for JIT on x86_64, but Electron won't start without it
|
||||
on Apple Silicon.
|
||||
-->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
|
@ -50,7 +50,7 @@
|
|||
|
||||
<!--
|
||||
We need to enable this, even though we don't modify our own executables.
|
||||
Otherwise, NWjs just bombs out.
|
||||
Otherwise, Electron just bombs out.
|
||||
-->
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
|
|
|
@ -199,9 +199,9 @@ def dump_header():
|
|||
def dump_runtime():
|
||||
print_title("Runtime Dependencies")
|
||||
print_table_header()
|
||||
print_row("Python", "3.6+", "PSF", "https://www.python.org/")
|
||||
print_row("nw", "0.50.2", "MIT",
|
||||
"git://github.com/nwjs/npm-installer.git")
|
||||
print_row("Python", "3.8+", "PSF", "https://www.python.org/")
|
||||
print_row("Electron", "30.0.5", "MIT",
|
||||
"https://www.npmjs.com/package/electron")
|
||||
|
||||
# Make sure to change the count of hardcoded_deps if we will
|
||||
# manually add some more dependencies in future.
|
||||
|
|
|
@ -107,7 +107,7 @@ export default class MainMenuFactory {
|
|||
static refreshMainMenuItems(menu, menuItems) {
|
||||
menu.setMenuItems(menuItems);
|
||||
window.electronUI?.setMenus(MainMenuFactory.toElectron());
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:nw-refresh-menu-item', pgAdmin.Browser.MainMenus);
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:refresh-menu-item', pgAdmin.Browser.MainMenus);
|
||||
}
|
||||
|
||||
static createMenuItem(options) {
|
||||
|
@ -130,10 +130,8 @@ export default class MainMenuFactory {
|
|||
});
|
||||
}
|
||||
}}, (menu, item)=> {
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:nw-enable-disable-menu-items', menu, item);
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:enable-disable-menu-items', menu, item);
|
||||
window.electronUI?.enableDisableMenuItems(menu?.serialize(), item?.serialize());
|
||||
}, (item) => {
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:nw-update-checked-menu-item', item);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -920,7 +920,7 @@ define('pgadmin.browser', [
|
|||
|
||||
this.success = function() {
|
||||
addItemNode();
|
||||
}.bind(this);
|
||||
};
|
||||
// We can refresh the collection node, but - let's not bother about
|
||||
// it right now.
|
||||
this.notFound = errorOut;
|
||||
|
@ -961,7 +961,7 @@ define('pgadmin.browser', [
|
|||
|
||||
this.success = function() {
|
||||
addItemNode();
|
||||
}.bind(this);
|
||||
};
|
||||
// We can refresh the collection node, but - let's not bother about
|
||||
// it right now.
|
||||
this.notFound = errorOut;
|
||||
|
@ -1021,7 +1021,7 @@ define('pgadmin.browser', [
|
|||
this.load = true;
|
||||
this.success = function() {
|
||||
addItemNode();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
if (_d._type == this.old._type) {
|
||||
// We were already searching the old object under the parent.
|
||||
|
@ -1423,7 +1423,7 @@ define('pgadmin.browser', [
|
|||
});
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
if (n?.collection_node) {
|
||||
let p = ctx.i = this.tree.parent(_i),
|
||||
|
|
|
@ -25,7 +25,7 @@ import _ from 'lodash';
|
|||
import EmptyPanelMessage from '../../../static/js/components/EmptyPanelMessage';
|
||||
import TabPanel from '../../../static/js/components/TabPanel';
|
||||
import Summary from './SystemStats/Summary';
|
||||
import CPU from './SystemStats/CPU';
|
||||
import CpuDetails from './SystemStats/CpuDetails';
|
||||
import Memory from './SystemStats/Memory';
|
||||
import Storage from './SystemStats/Storage';
|
||||
import withStandardTabInfo from '../../../static/js/helpers/withStandardTabInfo';
|
||||
|
@ -86,6 +86,160 @@ const Root = styled('div')(({theme}) => ({
|
|||
|
||||
let activeQSchemaObj = new ActiveQuery();
|
||||
|
||||
const cellPropTypes = {
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
function getTerminateCell(pgAdmin, sid, did, canTakeAction, onSuccess) {
|
||||
function TerminateCell({row}) {
|
||||
let terminate_session_url =
|
||||
url_for('dashboard.index') + 'terminate_session' + '/' + sid,
|
||||
title = gettext('Terminate Session?'),
|
||||
txtConfirm = gettext(
|
||||
'Are you sure you wish to terminate the session?'
|
||||
),
|
||||
txtSuccess = gettext('Session terminated successfully.'),
|
||||
txtError = gettext(
|
||||
'An error occurred whilst terminating the active query.'
|
||||
);
|
||||
const action_url = did
|
||||
? terminate_session_url + '/' + did
|
||||
: terminate_session_url;
|
||||
|
||||
const api = getApiInstance();
|
||||
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<CancelIcon />}
|
||||
className='Dashboard-terminateButton'
|
||||
onClick={() => {
|
||||
if (
|
||||
!canTakeAction(row, 'terminate')
|
||||
)
|
||||
return;
|
||||
let url = action_url + '/' + row.original.pid;
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
title,
|
||||
txtConfirm,
|
||||
function () {
|
||||
api
|
||||
.delete(url)
|
||||
.then(function (res) {
|
||||
if (res.data == gettext('Success')) {
|
||||
pgAdmin.Browser.notifier.success(txtSuccess);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.error(txtError);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to perform the operation.'),
|
||||
parseApiError(error)
|
||||
);
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}}
|
||||
aria-label="Terminate Session?"
|
||||
title={gettext('Terminate Session?')}
|
||||
></PgIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
TerminateCell.propTypes = cellPropTypes;
|
||||
return TerminateCell;
|
||||
}
|
||||
|
||||
function getCancelCell(pgAdmin, sid, did, canTakeAction, onSuccess) {
|
||||
function CancelCell({ row }) {
|
||||
let cancel_query_url =
|
||||
url_for('dashboard.index') + 'cancel_query' + '/' + sid,
|
||||
title = gettext('Cancel Active Query?'),
|
||||
txtConfirm = gettext(
|
||||
'Are you sure you wish to cancel the active query?'
|
||||
),
|
||||
txtSuccess = gettext('Active query cancelled successfully.'),
|
||||
txtError = gettext(
|
||||
'An error occurred whilst cancelling the active query.'
|
||||
);
|
||||
|
||||
const action_url = did ? cancel_query_url + '/' + did : cancel_query_url;
|
||||
|
||||
const api = getApiInstance();
|
||||
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<StopSharpIcon/>}
|
||||
onClick={() => {
|
||||
if (!canTakeAction(row, 'cancel'))
|
||||
return;
|
||||
let url = action_url + '/' + row.original.pid;
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
title,
|
||||
txtConfirm,
|
||||
function () {
|
||||
api
|
||||
.delete(url)
|
||||
.then(function (res) {
|
||||
if (res.data == gettext('Success')) {
|
||||
pgAdmin.Browser.notifier.success(txtSuccess);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.error(txtError);
|
||||
onSuccess?.();
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to perform the operation.'),
|
||||
parseApiError(error)
|
||||
);
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}}
|
||||
aria-label="Cancel the query"
|
||||
title={gettext('Cancel the active query')}
|
||||
></PgIconButton>
|
||||
);
|
||||
}
|
||||
CancelCell.propTypes = cellPropTypes;
|
||||
return CancelCell;
|
||||
}
|
||||
|
||||
function ActiveOnlyHeader({activeOnly, setActiveOnly}) {
|
||||
return (
|
||||
<InputCheckbox
|
||||
label={gettext('Active sessions only')}
|
||||
labelPlacement="end"
|
||||
className='Dashboard-searchInput'
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveOnly(e.target.checked);
|
||||
}}
|
||||
value={activeOnly}
|
||||
controlProps={{
|
||||
label: gettext('Active sessions only'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
ActiveOnlyHeader.propTypes = {
|
||||
activeOnly: PropTypes.bool,
|
||||
setActiveOnly: PropTypes.func,
|
||||
};
|
||||
|
||||
function Dashboard({
|
||||
nodeItem, nodeData, node, treeNodeInfo,
|
||||
...props
|
||||
|
@ -134,6 +288,62 @@ function Dashboard({
|
|||
setMainTabVal(tabVal);
|
||||
};
|
||||
|
||||
const canTakeAction = (row, cellAction) => {
|
||||
// We will validate if user is allowed to cancel the active query
|
||||
// If there is only one active session means it probably our main
|
||||
// connection session
|
||||
cellAction = cellAction || null;
|
||||
let pg_version = treeNodeInfo.server.version || null,
|
||||
is_cancel_session = cellAction === 'cancel',
|
||||
txtMessage,
|
||||
maintenance_database = treeNodeInfo.server.db;
|
||||
|
||||
let maintenanceActiveSessions = dashData.filter((data) => data.state === 'active'&&
|
||||
maintenance_database === data.datname);
|
||||
|
||||
// With PG10, We have background process showing on dashboard
|
||||
// We will not allow user to cancel them as they will fail with error
|
||||
// anyway, so better usability we will throw our on notification
|
||||
|
||||
// Background processes do not have database field populated
|
||||
if (pg_version && pg_version >= 100000 && !row.original.datname) {
|
||||
if (is_cancel_session) {
|
||||
txtMessage = gettext('You cannot cancel background worker processes.');
|
||||
} else {
|
||||
txtMessage = gettext(
|
||||
'You cannot terminate background worker processes.'
|
||||
);
|
||||
}
|
||||
pgAdmin.Browser.notifier.info(txtMessage);
|
||||
return false;
|
||||
// If it is the last active connection on maintenance db then error out
|
||||
} else if (
|
||||
maintenance_database == row.original.datname &&
|
||||
row.original.state == 'active' &&
|
||||
maintenanceActiveSessions.length === 1
|
||||
) {
|
||||
if (is_cancel_session) {
|
||||
txtMessage = gettext(
|
||||
'You are not allowed to cancel the main active session.'
|
||||
);
|
||||
} else {
|
||||
txtMessage = gettext(
|
||||
'You are not allowed to terminate the main active session.'
|
||||
);
|
||||
}
|
||||
pgAdmin.Browser.notifier.error(txtMessage);
|
||||
return false;
|
||||
} else if (is_cancel_session && row.original.state == 'idle') {
|
||||
// If this session is already idle then do nothing
|
||||
pgAdmin.Browser.notifier.info(gettext('The session is already in idle state.'));
|
||||
return false;
|
||||
} else {
|
||||
// Will return true and let the backend handle all the cases.
|
||||
// Added as fix of #7217
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const serverConfigColumns = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
|
@ -190,67 +400,7 @@ function Dashboard({
|
|||
maxSize: 35,
|
||||
minSize: 35,
|
||||
id: 'btn-terminate',
|
||||
// eslint-disable-next-line react/display-name
|
||||
cell: ({ row }) => {
|
||||
let terminate_session_url =
|
||||
url_for('dashboard.index') + 'terminate_session' + '/' + sid,
|
||||
title = gettext('Terminate Session?'),
|
||||
txtConfirm = gettext(
|
||||
'Are you sure you wish to terminate the session?'
|
||||
),
|
||||
txtSuccess = gettext('Session terminated successfully.'),
|
||||
txtError = gettext(
|
||||
'An error occurred whilst terminating the active query.'
|
||||
);
|
||||
const action_url = did
|
||||
? terminate_session_url + '/' + did
|
||||
: terminate_session_url;
|
||||
|
||||
const api = getApiInstance();
|
||||
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<CancelIcon />}
|
||||
className='Dashboard-terminateButton'
|
||||
onClick={() => {
|
||||
if (
|
||||
!canTakeAction(row, 'terminate')
|
||||
)
|
||||
return;
|
||||
let url = action_url + '/' + row.original.pid;
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
title,
|
||||
txtConfirm,
|
||||
function () {
|
||||
api
|
||||
.delete(url)
|
||||
.then(function (res) {
|
||||
if (res.data == gettext('Success')) {
|
||||
pgAdmin.Browser.notifier.success(txtSuccess);
|
||||
setRefresh(!refresh);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.error(txtError);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to perform the operation.'),
|
||||
parseApiError(error)
|
||||
);
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}}
|
||||
aria-label="Terminate Session?"
|
||||
title={gettext('Terminate Session?')}
|
||||
></PgIconButton>
|
||||
);
|
||||
},
|
||||
cell: getTerminateCell(pgAdmin, sid, did, canTakeAction, setRefresh, ()=>setRefresh(!refresh)),
|
||||
},
|
||||
{
|
||||
header: () => null,
|
||||
|
@ -261,64 +411,7 @@ function Dashboard({
|
|||
maxSize: 35,
|
||||
minSize: 35,
|
||||
id: 'btn-cancel',
|
||||
cell: ({ row }) => {
|
||||
let cancel_query_url =
|
||||
url_for('dashboard.index') + 'cancel_query' + '/' + sid,
|
||||
title = gettext('Cancel Active Query?'),
|
||||
txtConfirm = gettext(
|
||||
'Are you sure you wish to cancel the active query?'
|
||||
),
|
||||
txtSuccess = gettext('Active query cancelled successfully.'),
|
||||
txtError = gettext(
|
||||
'An error occurred whilst cancelling the active query.'
|
||||
);
|
||||
|
||||
const action_url = did ? cancel_query_url + '/' + did : cancel_query_url;
|
||||
|
||||
const api = getApiInstance();
|
||||
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<StopSharpIcon/>}
|
||||
onClick={() => {
|
||||
if (!canTakeAction(row, 'cancel'))
|
||||
return;
|
||||
let url = action_url + '/' + row.original.pid;
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
title,
|
||||
txtConfirm,
|
||||
function () {
|
||||
api
|
||||
.delete(url)
|
||||
.then(function (res) {
|
||||
if (res.data == gettext('Success')) {
|
||||
pgAdmin.Browser.notifier.success(txtSuccess);
|
||||
setRefresh(!refresh);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.error(txtError);
|
||||
setRefresh(!refresh);
|
||||
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
pgAdmin.Browser.notifier.alert(
|
||||
gettext('Failed to perform the operation.'),
|
||||
parseApiError(error)
|
||||
);
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}}
|
||||
aria-label="Cancel the query"
|
||||
title={gettext('Cancel the active query')}
|
||||
></PgIconButton>
|
||||
);
|
||||
},
|
||||
cell: getCancelCell(pgAdmin, sid, did, canTakeAction, setRefresh, ()=>setRefresh(!refresh)),
|
||||
},
|
||||
{
|
||||
header: () => null,
|
||||
|
@ -590,61 +683,6 @@ function Dashboard({
|
|||
},
|
||||
];
|
||||
|
||||
const canTakeAction = (row, cellAction) => {
|
||||
// We will validate if user is allowed to cancel the active query
|
||||
// If there is only one active session means it probably our main
|
||||
// connection session
|
||||
cellAction = cellAction || null;
|
||||
let pg_version = treeNodeInfo.server.version || null,
|
||||
is_cancel_session = cellAction === 'cancel',
|
||||
txtMessage,
|
||||
maintenance_database = treeNodeInfo.server.db;
|
||||
|
||||
let maintenanceActiveSessions = dashData.filter((data) => data.state === 'active'&&
|
||||
maintenance_database === data.datname);
|
||||
|
||||
// With PG10, We have background process showing on dashboard
|
||||
// We will not allow user to cancel them as they will fail with error
|
||||
// anyway, so better usability we will throw our on notification
|
||||
|
||||
// Background processes do not have database field populated
|
||||
if (pg_version && pg_version >= 100000 && !row.original.datname) {
|
||||
if (is_cancel_session) {
|
||||
txtMessage = gettext('You cannot cancel background worker processes.');
|
||||
} else {
|
||||
txtMessage = gettext(
|
||||
'You cannot terminate background worker processes.'
|
||||
);
|
||||
}
|
||||
pgAdmin.Browser.notifier.info(txtMessage);
|
||||
return false;
|
||||
// If it is the last active connection on maintenance db then error out
|
||||
} else if (
|
||||
maintenance_database == row.original.datname &&
|
||||
row.original.state == 'active' &&
|
||||
maintenanceActiveSessions.length === 1
|
||||
) {
|
||||
if (is_cancel_session) {
|
||||
txtMessage = gettext(
|
||||
'You are not allowed to cancel the main active session.'
|
||||
);
|
||||
} else {
|
||||
txtMessage = gettext(
|
||||
'You are not allowed to terminate the main active session.'
|
||||
);
|
||||
}
|
||||
pgAdmin.Browser.notifier.error(txtMessage);
|
||||
return false;
|
||||
} else if (is_cancel_session && row.original.state == 'idle') {
|
||||
// If this session is already idle then do nothing
|
||||
pgAdmin.Browser.notifier.info(gettext('The session is already in idle state.'));
|
||||
return false;
|
||||
} else {
|
||||
// Will return true and let the backend handle all the cases.
|
||||
// Added as fix of #7217
|
||||
return true;
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
// Reset Tab values to 0, so that it will select "Sessions" on node changed.
|
||||
nodeData?._type === 'database' && setTabVal(0);
|
||||
|
@ -766,25 +804,6 @@ function Dashboard({
|
|||
);
|
||||
};
|
||||
|
||||
const CustomActiveOnlyHeaderLabel =
|
||||
{
|
||||
label: gettext('Active sessions only'),
|
||||
};
|
||||
const CustomActiveOnlyHeader = () => {
|
||||
return (
|
||||
<InputCheckbox
|
||||
label={gettext('Active sessions only')}
|
||||
labelPlacement="end"
|
||||
className='Dashboard-searchInput'
|
||||
onChange={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveOnly(e.target.checked);
|
||||
}}
|
||||
value={activeOnly}
|
||||
controlProps={CustomActiveOnlyHeaderLabel}
|
||||
></InputCheckbox>);
|
||||
};
|
||||
|
||||
return (
|
||||
(<Root>
|
||||
{sid && serverConnected ? (
|
||||
|
@ -832,7 +851,7 @@ function Dashboard({
|
|||
<PgTable
|
||||
caveTable={false}
|
||||
tableNoBorder={false}
|
||||
CustomHeader={CustomActiveOnlyHeader}
|
||||
customHeader={<ActiveOnlyHeader activeOnly={activeOnly} setActiveOnly={setActiveOnly} />}
|
||||
columns={activityColumns}
|
||||
data={filteredDashData}
|
||||
schema={activeQSchemaObj}
|
||||
|
@ -891,7 +910,7 @@ function Dashboard({
|
|||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={systemStatsTabVal} index={1} classNameRoot='Dashboard-tabPanel'>
|
||||
<CPU
|
||||
<CpuDetails
|
||||
key={sid + did}
|
||||
preferences={preferences}
|
||||
sid={sid}
|
||||
|
|
|
@ -12,14 +12,14 @@ import PgTable from 'sources/components/PgTable';
|
|||
import gettext from 'sources/gettext';
|
||||
import PropTypes from 'prop-types';
|
||||
import {getGCD, getEpoch} from 'sources/utils';
|
||||
import ChartContainer from '../components/ChartContainer';
|
||||
import ChartContainer from '../components/ChartContainer.jsx';
|
||||
import { Box, Grid } from '@mui/material';
|
||||
import { DATA_POINT_SIZE } from 'sources/chartjs';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart';
|
||||
import StreamingChart from '../../../../static/js/components/PgChart/StreamingChart.jsx';
|
||||
import {useInterval, usePrevious} from 'sources/custom_hooks';
|
||||
import axios from 'axios';
|
||||
import { getStatsUrl, transformData, statsReducer, X_AXIS_LENGTH } from './utility.js';
|
||||
import { toPrettySize } from '../../../../static/js/utils';
|
||||
import { toPrettySize } from '../../../../static/js/utils.js';
|
||||
import SectionContainer from '../components/SectionContainer.jsx';
|
||||
|
||||
const chartsDefault = {
|
||||
|
@ -28,7 +28,7 @@ const chartsDefault = {
|
|||
'pcpu_stats': {},
|
||||
};
|
||||
|
||||
export default function CPU({preferences, sid, did, pageVisible, enablePoll=true}) {
|
||||
export default function CpuDetails({preferences, sid, did, pageVisible, enablePoll=true}) {
|
||||
const refreshOn = useRef(null);
|
||||
const prevPrefernces = usePrevious(preferences);
|
||||
|
||||
|
@ -202,7 +202,7 @@ export default function CPU({preferences, sid, did, pageVisible, enablePoll=true
|
|||
);
|
||||
}
|
||||
|
||||
CPU.propTypes = {
|
||||
CpuDetails.propTypes = {
|
||||
preferences: PropTypes.object.isRequired,
|
||||
sid: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
|
||||
did: PropTypes.oneOfType([PropTypes.string.isRequired, PropTypes.number.isRequired]),
|
|
@ -64,6 +64,95 @@ const ProcessStateTextAndColor = {
|
|||
[BgProcessManagerProcessState.PROCESS_TERMINATING]: [gettext('Terminating...'), 'bgTerm'],
|
||||
[BgProcessManagerProcessState.PROCESS_FAILED]: [gettext('Failed'), 'bgFailed'],
|
||||
};
|
||||
|
||||
const cellPropTypes = {
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
function CancelCell({row}) {
|
||||
const pgAdmin = usePgAdmin();
|
||||
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<CancelIcon />}
|
||||
className='Processes-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>
|
||||
);
|
||||
}
|
||||
CancelCell.propTypes = cellPropTypes;
|
||||
|
||||
function getLogsCell(pgAdmin, onViewDetailsClick) {
|
||||
function LogsCell({ row }) {
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
icon={<DescriptionOutlinedIcon />}
|
||||
noBorder
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onViewDetailsClick(row.original);
|
||||
}}
|
||||
aria-label="View details"
|
||||
title={gettext('View details')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
LogsCell.propTypes = cellPropTypes;
|
||||
|
||||
return LogsCell;
|
||||
}
|
||||
|
||||
function StatusCell({row}) {
|
||||
const [text, bgcolor] = ProcessStateTextAndColor[row.original.process_state];
|
||||
return <Box className={'Processes-'+bgcolor}>{text}</Box>;
|
||||
}
|
||||
StatusCell.propTypes = cellPropTypes;
|
||||
|
||||
function CustomHeader({selectedRowIDs, setSelectedRows}) {
|
||||
const pgAdmin = usePgAdmin();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PgButtonGroup>
|
||||
<PgIconButton
|
||||
icon={<DeleteIcon style={{height: '1.4rem'}}/>}
|
||||
aria-label="Acknowledge and Remove"
|
||||
title={gettext('Acknowledge and Remove')}
|
||||
onClick={() => {
|
||||
pgAdmin.Browser.notifier.confirm(gettext('Remove Processes'), gettext('Are you sure you want to remove the selected processes?'), ()=>{
|
||||
pgAdmin.Browser.BgProcessManager.acknowledge(selectedRowIDs);
|
||||
setSelectedRows({});
|
||||
});
|
||||
}}
|
||||
disabled={selectedRowIDs.length <= 0}
|
||||
></PgIconButton>
|
||||
<PgIconButton
|
||||
icon={<HelpIcon style={{height: '1.4rem'}}/>}
|
||||
aria-label="Help"
|
||||
title={gettext('Help')}
|
||||
onClick={() => {
|
||||
window.open(url_for('help.static', {'filename': 'processes.html'}));
|
||||
}}
|
||||
></PgIconButton>
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
CustomHeader.propTypes = {
|
||||
selectedRowIDs: PropTypes.array,
|
||||
setSelectedRows: PropTypes.func,
|
||||
};
|
||||
|
||||
export default function Processes() {
|
||||
|
||||
const pgAdmin = usePgAdmin();
|
||||
|
@ -89,56 +178,6 @@ export default function Processes() {
|
|||
|
||||
|
||||
const columns = useMemo(()=>{
|
||||
const cellPropTypes = {
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
const CancelCell = ({row}) => {
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
noBorder
|
||||
icon={<CancelIcon />}
|
||||
className='Processes-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>
|
||||
);
|
||||
};
|
||||
CancelCell.displayName = 'CancelCell';
|
||||
CancelCell.propTypes = cellPropTypes;
|
||||
|
||||
const LogsCell = ({ row }) => {
|
||||
return (
|
||||
<PgIconButton
|
||||
size="xs"
|
||||
icon={<DescriptionOutlinedIcon />}
|
||||
noBorder
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onViewDetailsClick(row.original);
|
||||
}}
|
||||
aria-label="View details"
|
||||
title={gettext('View details')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
LogsCell.displayName = 'LogsCell';
|
||||
LogsCell.propTypes = cellPropTypes;
|
||||
|
||||
const StatusCell = ({row})=>{
|
||||
const [text, bgcolor] = ProcessStateTextAndColor[row.original.process_state];
|
||||
return <Box className={'Processes-'+bgcolor}>{text}</Box>;
|
||||
};
|
||||
StatusCell.displayName = 'StatusCell';
|
||||
StatusCell.propTypes = cellPropTypes;
|
||||
|
||||
return [{
|
||||
header: () => null,
|
||||
enableSorting: false,
|
||||
|
@ -159,7 +198,7 @@ export default function Processes() {
|
|||
maxSize: 35,
|
||||
minSize: 35,
|
||||
id: 'btn-logs',
|
||||
cell: LogsCell,
|
||||
cell: getLogsCell(pgAdmin, onViewDetailsClick),
|
||||
},
|
||||
{
|
||||
header: gettext('PID'),
|
||||
|
@ -257,34 +296,7 @@ export default function Processes() {
|
|||
return row.id;
|
||||
}
|
||||
}}
|
||||
CustomHeader={()=>{
|
||||
return (
|
||||
<Box>
|
||||
<PgButtonGroup>
|
||||
<PgIconButton
|
||||
icon={<DeleteIcon style={{height: '1.4rem'}}/>}
|
||||
aria-label="Acknowledge and Remove"
|
||||
title={gettext('Acknowledge and Remove')}
|
||||
onClick={() => {
|
||||
pgAdmin.Browser.notifier.confirm(gettext('Remove Processes'), gettext('Are you sure you want to remove the selected processes?'), ()=>{
|
||||
pgAdmin.Browser.BgProcessManager.acknowledge(selectedRowIDs);
|
||||
setSelectedRows({});
|
||||
});
|
||||
}}
|
||||
disabled={selectedRowIDs.length <= 0}
|
||||
></PgIconButton>
|
||||
<PgIconButton
|
||||
icon={<HelpIcon style={{height: '1.4rem'}}/>}
|
||||
aria-label="Help"
|
||||
title={gettext('Help')}
|
||||
onClick={() => {
|
||||
window.open(url_for('help.static', {'filename': 'processes.html'}));
|
||||
}}
|
||||
></PgIconButton>
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
customHeader={<CustomHeader selectedRowIDs={selectedRowIDs} setSelectedRows={setSelectedRows} />}
|
||||
></PgTable></Root>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,66 @@ const StyledBox = styled(Box)(({theme}) => ({
|
|||
}
|
||||
}));
|
||||
|
||||
function CustomHeader({node, nodeData, nodeItem, treeNodeInfo, selectedObject, onDrop}) {
|
||||
const canDrop = evalFunc(node, node.canDrop, nodeData, nodeItem, treeNodeInfo);
|
||||
const canDropCascade = evalFunc(node, node.canDropCascade, nodeData, nodeItem, treeNodeInfo);
|
||||
const canDropForce = evalFunc(node, node.canDropForce, nodeData, nodeItem, treeNodeInfo);
|
||||
return (
|
||||
<Box >
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton
|
||||
icon={<DeleteIcon style={{height: '1.35rem'}}/>}
|
||||
aria-label="Delete"
|
||||
title={gettext('Delete')}
|
||||
onClick={() => {
|
||||
onDrop('drop');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDrop
|
||||
: true
|
||||
}
|
||||
></PgIconButton>
|
||||
{node.type !== 'coll-database' ? <PgIconButton
|
||||
icon={<DeleteSweepIcon style={{height: '1.5rem'}} />}
|
||||
aria-label="Delete Cascade"
|
||||
title={gettext('Delete (Cascade)')}
|
||||
onClick={() => {
|
||||
onDrop('dropCascade');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDropCascade
|
||||
: true
|
||||
}
|
||||
></PgIconButton> :
|
||||
<PgIconButton
|
||||
icon={<DeleteForeverIcon style={{height: '1.4rem'}} />}
|
||||
aria-label="Delete Force"
|
||||
title={gettext('Delete (Force)')}
|
||||
onClick={() => {
|
||||
onDrop('dropForce');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDropForce
|
||||
: true
|
||||
}
|
||||
></PgIconButton>}
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
CustomHeader.propTypes = {
|
||||
node: PropTypes.func,
|
||||
nodeData: PropTypes.object,
|
||||
treeNodeInfo: PropTypes.object,
|
||||
nodeItem: PropTypes.object,
|
||||
selectedObject: PropTypes.object,
|
||||
onDrop: PropTypes.func,
|
||||
};
|
||||
|
||||
export default function CollectionNodeProperties({
|
||||
node,
|
||||
treeNodeInfo,
|
||||
|
@ -221,56 +281,6 @@ export default function CollectionNodeProperties({
|
|||
}
|
||||
}, [nodeData, node, nodeItem, isStale, isActive]);
|
||||
|
||||
const CustomHeader = () => {
|
||||
const canDrop = evalFunc(node, node.canDrop, nodeData, nodeItem, treeNodeInfo);
|
||||
const canDropCascade = evalFunc(node, node.canDropCascade, nodeData, nodeItem, treeNodeInfo);
|
||||
const canDropForce = evalFunc(node, node.canDropForce, nodeData, nodeItem, treeNodeInfo);
|
||||
return (
|
||||
<Box >
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton
|
||||
icon={<DeleteIcon style={{height: '1.35rem'}}/>}
|
||||
aria-label="Delete"
|
||||
title={gettext('Delete')}
|
||||
onClick={() => {
|
||||
onDrop('drop');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDrop
|
||||
: true
|
||||
}
|
||||
></PgIconButton>
|
||||
{node.type !== 'coll-database' ? <PgIconButton
|
||||
icon={<DeleteSweepIcon style={{height: '1.5rem'}} />}
|
||||
aria-label="Delete Cascade"
|
||||
title={gettext('Delete (Cascade)')}
|
||||
onClick={() => {
|
||||
onDrop('dropCascade');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDropCascade
|
||||
: true
|
||||
}
|
||||
></PgIconButton> :
|
||||
<PgIconButton
|
||||
icon={<DeleteForeverIcon style={{height: '1.4rem'}} />}
|
||||
aria-label="Delete Force"
|
||||
title={gettext('Delete (Force)')}
|
||||
onClick={() => {
|
||||
onDrop('dropForce');
|
||||
}}
|
||||
disabled={
|
||||
(Object.keys(selectedObject).length > 0)
|
||||
? !canDropForce
|
||||
: true
|
||||
}
|
||||
></PgIconButton>}
|
||||
</PgButtonGroup>
|
||||
</Box>);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Loader message={loaderText}/>
|
||||
|
@ -279,7 +289,7 @@ export default function CollectionNodeProperties({
|
|||
(
|
||||
<PgTable
|
||||
hasSelectRow={!('catalog' in treeNodeInfo) && (nodeData.label !== 'Catalogs') && _.isUndefined(node?.canSelect)}
|
||||
CustomHeader={CustomHeader}
|
||||
customHeader={<CustomHeader node={node} nodeData={nodeData} nodeItem={nodeItem} treeNodeInfo={treeNodeInfo} selectedObject={selectedObject} onDrop={onDrop} />}
|
||||
columns={pgTableColumns}
|
||||
data={data}
|
||||
type={'panel'}
|
||||
|
|
|
@ -64,10 +64,10 @@ export default function AppMenuBar() {
|
|||
const pgAdmin = usePgAdmin();
|
||||
|
||||
useEffect(()=>{
|
||||
pgAdmin.Browser.Events.on('pgadmin:nw-enable-disable-menu-items', _.debounce(()=>{
|
||||
pgAdmin.Browser.Events.on('pgadmin:enable-disable-menu-items', _.debounce(()=>{
|
||||
forceUpdate();
|
||||
}, 100));
|
||||
pgAdmin.Browser.Events.on('pgadmin:nw-refresh-menu-item', _.debounce(()=>{
|
||||
pgAdmin.Browser.Events.on('pgadmin:refresh-menu-item', _.debounce(()=>{
|
||||
forceUpdate();
|
||||
}, 100));
|
||||
}, []);
|
||||
|
|
|
@ -15,9 +15,6 @@ import { Box } from '@mui/material';
|
|||
import { PgIconButton } from '../components/Buttons';
|
||||
import AddIcon from '@mui/icons-material/AddOutlined';
|
||||
import { MappedCellControl } from './MappedControl';
|
||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
||||
|
||||
import {
|
||||
useReactTable,
|
||||
|
@ -42,7 +39,9 @@ import { useIsMounted } from '../custom_hooks';
|
|||
import { InputText } from '../components/FormComponents';
|
||||
import { usePgAdmin } from '../BrowserComponent';
|
||||
import { requestAnimationAndFocus } from '../utils';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent } from '../components/PgReactTableStyled';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader,
|
||||
PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent,
|
||||
getDeleteCell, getEditCell, getReorderCell } from '../components/PgReactTableStyled';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
|
@ -82,16 +81,6 @@ const StyledBox = styled(Box)(({theme}) => ({
|
|||
'&.btn-cell, &.expanded-icon-cell': {
|
||||
padding: '2px 0px'
|
||||
},
|
||||
'& .DataGridView-gridRowButton': {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
backgroundColor: 'inherit',
|
||||
'&.Mui-disabled': {
|
||||
border: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -108,10 +97,6 @@ const StyledBox = styled(Box)(({theme}) => ({
|
|||
opacity: 0.75,
|
||||
}
|
||||
},
|
||||
'& .DataGridView-btnReorder': {
|
||||
cursor: 'move',
|
||||
padding: '4px 2px',
|
||||
},
|
||||
'& .DataGridView-resizer': {
|
||||
display: 'inline-block',
|
||||
width: '5px',
|
||||
|
@ -298,6 +283,58 @@ DataGridHeader.propTypes = {
|
|||
onSearchTextChange: PropTypes.func,
|
||||
};
|
||||
|
||||
function getMappedCell({
|
||||
field,
|
||||
schemaRef,
|
||||
viewHelperProps,
|
||||
accessPath,
|
||||
dataDispatch
|
||||
}) {
|
||||
const Cell = ({row, ...other}) => {
|
||||
const value = other.getValue();
|
||||
/* Make sure to take the latest field info from schema */
|
||||
field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field;
|
||||
|
||||
let {editable, disabled, modeSupported} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);
|
||||
|
||||
if(_.isUndefined(field.cell)) {
|
||||
console.error('cell is required ', field);
|
||||
}
|
||||
|
||||
return modeSupported && <MappedCellControl rowIndex={row.index} value={value}
|
||||
row={row} {...field}
|
||||
readonly={!editable}
|
||||
disabled={disabled}
|
||||
visible={true}
|
||||
onCellChange={(changeValue)=>{
|
||||
if(field.radioType) {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,
|
||||
path: accessPath,
|
||||
value: changeValue,
|
||||
id: field.id
|
||||
});
|
||||
}
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat([row.index, field.id]),
|
||||
value: changeValue,
|
||||
});
|
||||
}}
|
||||
reRenderRow={other.reRenderRow}
|
||||
/>;
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
onCellChange: PropTypes.func,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export default function DataGridView({
|
||||
value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,
|
||||
fixedRows, ...props}) {
|
||||
|
@ -325,13 +362,8 @@ export default function DataGridView({
|
|||
size: 36,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: ()=>{
|
||||
return <div className='DataGridView-btnReorder'>
|
||||
<DragIndicatorRoundedIcon fontSize="small" />
|
||||
</div>;
|
||||
}
|
||||
cell: getReorderCell(),
|
||||
};
|
||||
colInfo.cell.displayName = 'Cell';
|
||||
cols.push(colInfo);
|
||||
}
|
||||
if(props.canEdit) {
|
||||
|
@ -345,21 +377,16 @@ export default function DataGridView({
|
|||
size: 26,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: ({row})=>{
|
||||
let canEditRow = true;
|
||||
if(props.canEditRow) {
|
||||
canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});
|
||||
}
|
||||
return <PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon fontSize="small" />} className='DataGridView-gridRowButton'
|
||||
onClick={()=>{
|
||||
row.toggleExpanded();
|
||||
}} disabled={!canEditRow}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
colInfo.cell.displayName = 'Cell';
|
||||
colInfo.cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
cell: getEditCell({
|
||||
isDisabled: (row)=>{
|
||||
let canEditRow = true;
|
||||
if(props.canEditRow) {
|
||||
canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});
|
||||
}
|
||||
return !canEditRow;
|
||||
},
|
||||
title: gettext('Edit row'),
|
||||
})
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
|
@ -374,43 +401,39 @@ export default function DataGridView({
|
|||
size: 26,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: ({row}) => {
|
||||
let canDeleteRow = true;
|
||||
if(props.canDeleteRow) {
|
||||
canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
|
||||
}
|
||||
cell: getDeleteCell({
|
||||
title: gettext('Delete row'),
|
||||
isDisabled: (row)=>{
|
||||
let canDeleteRow = true;
|
||||
if(props.canDeleteRow) {
|
||||
canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
|
||||
}
|
||||
return !canDeleteRow;
|
||||
},
|
||||
onClick: (row)=>{
|
||||
const deleteRow = ()=> {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||
path: accessPath,
|
||||
value: row.index,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}
|
||||
onClick={()=>{
|
||||
const deleteRow = ()=> {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||
path: accessPath,
|
||||
value: row.index,
|
||||
});
|
||||
if (props.onDelete){
|
||||
props.onDelete(row.original || {}, deleteRow);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
props.customDeleteTitle || gettext('Delete Row'),
|
||||
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
|
||||
deleteRow,
|
||||
function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
if (props.onDelete){
|
||||
props.onDelete(row.original || {}, deleteRow);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
props.customDeleteTitle || gettext('Delete Row'),
|
||||
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
|
||||
deleteRow,
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
}} className='DataGridView-gridRowButton' disabled={!canDeleteRow} />
|
||||
);
|
||||
}
|
||||
};
|
||||
colInfo.cell.displayName = 'Cell';
|
||||
colInfo.cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
|
@ -447,47 +470,15 @@ export default function DataGridView({
|
|||
enableResizing: true,
|
||||
enableSorting: false,
|
||||
...widthParms,
|
||||
cell: ({row, ...other}) => {
|
||||
const value = other.getValue();
|
||||
/* Make sure to take the latest field info from schema */
|
||||
field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field;
|
||||
|
||||
let {editable, disabled, modeSupported} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);
|
||||
|
||||
if(_.isUndefined(field.cell)) {
|
||||
console.error('cell is required ', field);
|
||||
}
|
||||
|
||||
return modeSupported && <MappedCellControl rowIndex={row.index} value={value}
|
||||
row={row} {...field}
|
||||
readonly={!editable}
|
||||
disabled={disabled}
|
||||
visible={true}
|
||||
onCellChange={(changeValue)=>{
|
||||
if(field.radioType) {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,
|
||||
path: accessPath,
|
||||
value: changeValue,
|
||||
id: field.id
|
||||
});
|
||||
}
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat([row.index, field.id]),
|
||||
value: changeValue,
|
||||
});
|
||||
}}
|
||||
reRenderRow={other.reRenderRow}
|
||||
/>;
|
||||
},
|
||||
};
|
||||
colInfo.cell.displayName = 'Cell';
|
||||
colInfo.cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
onCellChange: PropTypes.func,
|
||||
cell: getMappedCell({
|
||||
field: field,
|
||||
schemaRef: schemaRef,
|
||||
viewHelperProps: viewHelperProps,
|
||||
accessPath: accessPath,
|
||||
dataDispatch: dataDispatch,
|
||||
}),
|
||||
};
|
||||
|
||||
return colInfo;
|
||||
})
|
||||
);
|
||||
|
@ -568,7 +559,7 @@ export default function DataGridView({
|
|||
|
||||
// Try autofocus on newly added row.
|
||||
setTimeout(()=>{
|
||||
const rowInput = tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`);
|
||||
const rowInput = tableRef.current?.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`);
|
||||
if(!rowInput) return;
|
||||
|
||||
requestAnimationAndFocus(tableRef.current.querySelector(`.pgrt-row[data-index="${newRowIndex.current}"] input`));
|
||||
|
|
|
@ -61,17 +61,19 @@ export function SecurityButton({...props}) {
|
|||
export default function BasePage({pageImage, title, children, messages}) {
|
||||
const snackbar = useSnackbar();
|
||||
useEffect(()=>{
|
||||
messages?.forEach((m)=>{
|
||||
snackbar.enqueueSnackbar({
|
||||
messages?.forEach((message)=>{
|
||||
let options = {
|
||||
autoHideDuration: null,
|
||||
content: (key)=>{
|
||||
if(Array.isArray(m[0])) m[0] = m[0][0];
|
||||
const type = Object.values(MESSAGE_TYPE).includes(m[0]) ? m[0] : MESSAGE_TYPE.INFO;
|
||||
content:(key)=>{
|
||||
if(Array.isArray(message[0])) message[0] = message[0][0];
|
||||
const type = Object.values(MESSAGE_TYPE).includes(message[0]) ? message[0] : MESSAGE_TYPE.INFO;
|
||||
return <FinalNotifyContent>
|
||||
<NotifierMessage type={type} message={m[1]} closable={true} onClose={()=>{snackbar.closeSnackbar(key);}} style={{maxWidth: '400px'}} />
|
||||
<NotifierMessage type={type} message={message[1]} closable={true} onClose={()=>{snackbar.closeSnackbar(key);}} style={{maxWidth: '400px'}} />
|
||||
</FinalNotifyContent>;
|
||||
}
|
||||
});
|
||||
};
|
||||
options.content.displayName = 'content';
|
||||
snackbar.enqueueSnackbar(options);
|
||||
});
|
||||
}, [messages]);
|
||||
return (
|
||||
|
|
|
@ -49,10 +49,6 @@ export default function ToolView() {
|
|||
ReactDOM.render(
|
||||
<ToolForm actionUrl={window.location.origin+toolUrl} params={formParams}/>, div
|
||||
);
|
||||
// Send the signal to runtime, so that proper zoom level will be set.
|
||||
setTimeout(function () {
|
||||
pgAdmin.Browser.Events.trigger('pgadmin:nw-set-new-window-open-size');
|
||||
}, 500);
|
||||
} else {
|
||||
window.open(toolUrl);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,13 @@ import PropTypes from 'prop-types';
|
|||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
||||
import { PgIconButton } from './Buttons';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import { InputSwitch } from './FormComponents';
|
||||
import { Checkbox } from '@mui/material';
|
||||
|
||||
|
||||
const StyledDiv = styled('div')(({theme})=>({
|
||||
|
@ -129,6 +133,21 @@ const StyledDiv = styled('div')(({theme})=>({
|
|||
whiteSpace: 'nowrap',
|
||||
userSelect: 'text',
|
||||
width: '100%',
|
||||
},
|
||||
|
||||
'& .reorder-cell': {
|
||||
cursor: 'move',
|
||||
padding: '4px 2px',
|
||||
},
|
||||
'& .pgrt-cell-button': {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
backgroundColor: 'inherit',
|
||||
'&.Mui-disabled': {
|
||||
border: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -139,7 +158,7 @@ const StyledDiv = styled('div')(({theme})=>({
|
|||
flexGrow: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -329,7 +348,7 @@ PgReactTable.propTypes = {
|
|||
children: CustomPropTypes.children,
|
||||
};
|
||||
|
||||
export function getExpandCell({ onClick, ...props }) {
|
||||
export function getExpandCell({ onClick, title }) {
|
||||
const Cell = ({ row }) => {
|
||||
const onClickFinal = (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -347,16 +366,14 @@ export function getExpandCell({ onClick, ...props }) {
|
|||
)
|
||||
}
|
||||
noBorder
|
||||
{...props}
|
||||
onClick={onClickFinal}
|
||||
aria-label={props.title}
|
||||
aria-label={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'ExpandCell';
|
||||
Cell.propTypes = {
|
||||
title: PropTypes.string,
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
|
@ -375,3 +392,89 @@ export function getSwitchCell() {
|
|||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function getCheckboxCell({title}) {
|
||||
const Cell = ({ table }) => {
|
||||
return (
|
||||
<div style={{textAlign: 'center', minWidth: 20}}>
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
inputProps={{ 'aria-label': title }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'CheckboxCell';
|
||||
Cell.propTypes = {
|
||||
table: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCheckboxHeaderCell({title}) {
|
||||
const Cell = ({ row }) => {
|
||||
return (
|
||||
<div style={{textAlign: 'center', minWidth: 20}}>
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={row.getIsSelected()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
inputProps={{ 'aria-label': title }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'CheckboxHeaderCell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object,
|
||||
};
|
||||
}
|
||||
|
||||
export function getReorderCell() {
|
||||
const Cell = () => {
|
||||
return <div className='reorder-cell'>
|
||||
<DragIndicatorRoundedIcon fontSize="small" />
|
||||
</div>;
|
||||
};
|
||||
|
||||
Cell.displayName = 'ReorderCell';
|
||||
}
|
||||
|
||||
export function getEditCell({isDisabled, title}) {
|
||||
const Cell = ({ row }) => {
|
||||
return <PgIconButton data-test="expand-row" title={title} icon={<EditRoundedIcon fontSize="small" />} className='pgrt-cell-button'
|
||||
onClick={()=>{
|
||||
row.toggleExpanded();
|
||||
}} disabled={isDisabled?.(row)}
|
||||
/>;
|
||||
};
|
||||
|
||||
Cell.displayName = 'EditCell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function getDeleteCell({isDisabled, title, onClick}) {
|
||||
const Cell = ({ row }) => (
|
||||
<PgIconButton data-test="delete-row" title={title} icon={<DeleteRoundedIcon fontSize="small" />}
|
||||
onClick={()=>onClick?.(row)}
|
||||
className='pgrt-cell-button' disabled={isDisabled?.(row)}
|
||||
/>
|
||||
);
|
||||
|
||||
Cell.displayName = 'DeleteCell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.any,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ import {
|
|||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Checkbox, Box } from '@mui/material';
|
||||
import { InputText } from './FormComponents';
|
||||
import _ from 'lodash';
|
||||
import gettext from 'sources/gettext';
|
||||
import SchemaView from '../SchemaView';
|
||||
import EmptyPanelMessage from './EmptyPanelMessage';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent } from './PgReactTableStyled';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, getCheckboxCell, getCheckboxHeaderCell } from './PgReactTableStyled';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
const ROW_HEIGHT = 30;
|
||||
function TableRow({ index, style, schema, row, measureElement}) {
|
||||
|
@ -86,31 +86,12 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
|
|||
|
||||
const finalColumns = useMemo(() => (hasSelectRow ? [{
|
||||
id: 'selection',
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<div style={{textAlign: 'center', minWidth: 20}}>
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
inputProps={{ 'aria-label': gettext('Select All Rows') }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div style={{textAlign: 'center', minWidth: 20}}>
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={row.getIsSelected()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
inputProps={{ 'aria-label': gettext('Select Row') }}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
header: getCheckboxCell({
|
||||
title: gettext('Select All Rows'),
|
||||
}),
|
||||
cell: getCheckboxHeaderCell({
|
||||
title: gettext('Select Row'),
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
maxSize: 35,
|
||||
|
@ -239,7 +220,7 @@ export default function PgTable({ caveTable = true, tableNoBorder = true, ...pro
|
|||
return (
|
||||
<StyledPgTableRoot className={[tableNoBorder ? '' : 'pgtable-pgrt-border', caveTable ? 'pgtable-pgrt-cave' : ''].join(' ')} data-test={props['data-test']}>
|
||||
<Box className='pgtable-header'>
|
||||
{props.CustomHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}> <props.CustomHeader /></Box>)}
|
||||
{props.customHeader && (<Box className={['pgtable-custom-header-section', props['className']].join(' ')}>{props.customHeader}</Box>)}
|
||||
<Box marginLeft="auto">
|
||||
<InputText
|
||||
placeholder={gettext('Search')}
|
||||
|
@ -260,7 +241,7 @@ export default function PgTable({ caveTable = true, tableNoBorder = true, ...pro
|
|||
}
|
||||
|
||||
PgTable.propTypes = {
|
||||
CustomHeader: PropTypes.func,
|
||||
customHeader: PropTypes.element,
|
||||
caveTable: PropTypes.bool,
|
||||
tableNoBorder: PropTypes.bool,
|
||||
'data-test': PropTypes.string,
|
||||
|
|
|
@ -116,7 +116,7 @@ export default class Menu {
|
|||
|
||||
|
||||
export class MenuItem {
|
||||
constructor(options, onDisableChange, onChangeChecked) {
|
||||
constructor(options, onDisableChange) {
|
||||
let menu_opts = [
|
||||
'name', 'label', 'priority', 'module', 'callback', 'data', 'enable',
|
||||
'category', 'target', 'url', 'node',
|
||||
|
@ -136,7 +136,6 @@ export class MenuItem {
|
|||
};
|
||||
}
|
||||
this.onDisableChange = onDisableChange;
|
||||
this.changeChecked = onChangeChecked;
|
||||
this._isDisabled = true;
|
||||
this.checkAndSetDisabled();
|
||||
}
|
||||
|
@ -158,7 +157,6 @@ export class MenuItem {
|
|||
|
||||
change_checked(isChecked) {
|
||||
this.checked = isChecked;
|
||||
this.changeChecked?.(this);
|
||||
}
|
||||
|
||||
getMenuItems() {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default function ObjectExplorerToolbar() {
|
|||
};
|
||||
|
||||
useEffect(()=>{
|
||||
const deregister = pgAdmin.Browser.Events.on('pgadmin:nw-enable-disable-menu-items', _.debounce(checkMenuState, 100));
|
||||
const deregister = pgAdmin.Browser.Events.on('pgadmin:enable-disable-menu-items', _.debounce(checkMenuState, 100));
|
||||
checkMenuState();
|
||||
return ()=>{
|
||||
deregister();
|
||||
|
|
|
@ -758,7 +758,7 @@ export default class ERDTool extends React.Component {
|
|||
height = 32766;
|
||||
isCut = true;
|
||||
}
|
||||
toPng(this.canvasEle)
|
||||
toPng(this.canvasEle, {width, height})
|
||||
.then((dataUrl)=>{
|
||||
let link = document.createElement('a');
|
||||
link.setAttribute('href', dataUrl);
|
||||
|
|
|
@ -432,6 +432,124 @@ function reducer(rows, { type, id, filterParams, gridData }) {
|
|||
}
|
||||
}
|
||||
|
||||
function selectHeaderRenderer({selectedRows, setSelectedRows, rootSelection, setRootSelection, allRowIds, selectedRowIds}) {
|
||||
const Cell = ()=>(
|
||||
<InputCheckbox
|
||||
cid={_.uniqueId('rgc')}
|
||||
className='ResultGridComponent-headerSelectCell'
|
||||
value={selectedRows.length == allRowIds.length ? rootSelection : false}
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setRootSelection(true);
|
||||
setSelectedRows([...allRowIds]);
|
||||
selectedRowIds([...allRowIds]);
|
||||
} else {
|
||||
setRootSelection(false);
|
||||
setSelectedRows([]);
|
||||
selectedRowIds([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
></InputCheckbox>
|
||||
);
|
||||
Cell.displayName = 'Cell';
|
||||
return Cell;
|
||||
}
|
||||
function selectFormatter({selectedRows, setSelectedRows, setRootSelection, activeRow, setActiveRow, allRowIds, selectedRowIds, selectedResultRows, deselectResultRows, getStyleClassName}) {
|
||||
const Cell = ({ row, isCellSelected }) => {
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
return (
|
||||
<Box className={!row?.children && getStyleClassName(row, selectedRows, isCellSelected, activeRow, true) + ' ResultGridComponent-selChBox'}>
|
||||
<InputCheckbox
|
||||
className='ResultGridComponent-selectCell'
|
||||
cid={`${row.id}`}
|
||||
value={selectedRows.includes(`${row.id}`)}
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setSelectedRows((prev) => {
|
||||
let tempSelectedRows = [...prev];
|
||||
if (!prev.includes(e.target.id)) {
|
||||
selectedResultRows(row, tempSelectedRows);
|
||||
tempSelectedRows.length === allRowIds.length && setRootSelection(true);
|
||||
} else {
|
||||
deselectResultRows(row, tempSelectedRows);
|
||||
}
|
||||
tempSelectedRows = new Set(tempSelectedRows);
|
||||
selectedRowIds([...tempSelectedRows]);
|
||||
return [...tempSelectedRows];
|
||||
});
|
||||
}
|
||||
}
|
||||
></InputCheckbox>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object,
|
||||
isCellSelected: PropTypes.bool,
|
||||
};
|
||||
return Cell;
|
||||
}
|
||||
|
||||
function expandFormatter({activeRow, setActiveRow, filterParams, gridData, selectedRows, dispatch, getStyleClassName}) {
|
||||
const Cell = ({ row, isCellSelected })=>{
|
||||
const hasChildren = row.children !== undefined;
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
return (
|
||||
<>
|
||||
{hasChildren && (
|
||||
|
||||
<CellExpanderFormatter
|
||||
row={row}
|
||||
isCellSelected={isCellSelected}
|
||||
expanded={row.isExpanded === true}
|
||||
filterParams={filterParams}
|
||||
onCellExpand={() => dispatch({ id: row.id, type: 'toggleSubRow', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows })}
|
||||
/>
|
||||
)}
|
||||
<div className="rdg-cell-value">
|
||||
|
||||
{!hasChildren && (
|
||||
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
|
||||
<span className={'ResultGridComponent-recordRow ' + row.icon}></span>
|
||||
{row.label}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object,
|
||||
isCellSelected: PropTypes.bool,
|
||||
};
|
||||
return Cell;
|
||||
}
|
||||
|
||||
function resultFormatter({selectedRows, activeRow, setActiveRow, getStyleClassName}) {
|
||||
const Cell = ({ row, isCellSelected })=>{
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
|
||||
return (
|
||||
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
|
||||
{row.status}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object,
|
||||
isCellSelected: PropTypes.bool,
|
||||
};
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function ResultGridComponent({ gridData, allRowIds, filterParams, selectedRowIds, transId, sourceData, targetData }) {
|
||||
|
||||
const [rows, dispatch] = useReducer(reducer, [...gridData]);
|
||||
|
@ -553,56 +671,15 @@ export function ResultGridComponent({ gridData, allRowIds, filterParams, selecte
|
|||
...SelectColumn,
|
||||
minWidth: 30,
|
||||
width: 30,
|
||||
headerRenderer() {
|
||||
return (
|
||||
<InputCheckbox
|
||||
cid={_.uniqueId('rgc')}
|
||||
className='ResultGridComponent-headerSelectCell'
|
||||
value={selectedRows.length == allRowIds.length ? rootSelection : false}
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setRootSelection(true);
|
||||
setSelectedRows([...allRowIds]);
|
||||
selectedRowIds([...allRowIds]);
|
||||
} else {
|
||||
setRootSelection(false);
|
||||
setSelectedRows([]);
|
||||
selectedRowIds([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
></InputCheckbox>
|
||||
);
|
||||
},
|
||||
formatter({ row, isCellSelected }) {
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
return (
|
||||
<Box className={!row?.children && getStyleClassName(row, selectedRows, isCellSelected, activeRow, true) + ' ResultGridComponent-selChBox'}>
|
||||
<InputCheckbox
|
||||
className='ResultGridComponent-selectCell'
|
||||
cid={`${row.id}`}
|
||||
value={selectedRows.includes(`${row.id}`)}
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setSelectedRows((prev) => {
|
||||
let tempSelectedRows = [...prev];
|
||||
if (!prev.includes(e.target.id)) {
|
||||
selectedResultRows(row, tempSelectedRows);
|
||||
tempSelectedRows.length === allRowIds.length && setRootSelection(true);
|
||||
} else {
|
||||
deselectResultRows(row, tempSelectedRows);
|
||||
}
|
||||
tempSelectedRows = new Set(tempSelectedRows);
|
||||
selectedRowIds([...tempSelectedRows]);
|
||||
return [...tempSelectedRows];
|
||||
});
|
||||
}
|
||||
}
|
||||
></InputCheckbox>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
headerRenderer: selectHeaderRenderer({
|
||||
selectedRows, setSelectedRows, rootSelection,
|
||||
setRootSelection, allRowIds, selectedRowIds
|
||||
}),
|
||||
formatter: selectFormatter({
|
||||
selectedRows, setSelectedRows, setRootSelection,
|
||||
activeRow, setActiveRow, allRowIds, selectedRowIds,
|
||||
selectedResultRows, deselectResultRows, getStyleClassName
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'label',
|
||||
|
@ -615,46 +692,15 @@ export function ResultGridComponent({ gridData, allRowIds, filterParams, selecte
|
|||
|
||||
return 1;
|
||||
},
|
||||
formatter({ row, isCellSelected }) {
|
||||
const hasChildren = row.children !== undefined;
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
return (
|
||||
<>
|
||||
{hasChildren && (
|
||||
|
||||
<CellExpanderFormatter
|
||||
row={row}
|
||||
isCellSelected={isCellSelected}
|
||||
expanded={row.isExpanded === true}
|
||||
filterParams={filterParams}
|
||||
onCellExpand={() => dispatch({ id: row.id, type: 'toggleSubRow', filterParams: filterParams, gridData: gridData, selectedRows: selectedRows })}
|
||||
/>
|
||||
)}
|
||||
<div className="rdg-cell-value">
|
||||
|
||||
{!hasChildren && (
|
||||
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
|
||||
<span className={'ResultGridComponent-recordRow ' + row.icon}></span>
|
||||
{row.label}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
formatter: expandFormatter({
|
||||
activeRow, setActiveRow, filterParams, gridData,
|
||||
selectedRows, dispatch, getStyleClassName
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
name: 'Comparison Result',
|
||||
formatter({ row, isCellSelected }) {
|
||||
isCellSelected && setActiveRow(row.id);
|
||||
|
||||
return (
|
||||
<Box className={getStyleClassName(row, selectedRows, isCellSelected, activeRow)}>
|
||||
{row.status}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
formatter: resultFormatter({selectedRows, activeRow, setActiveRow, getStyleClassName}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -51,6 +51,31 @@ const StyledEditor = styled('div')(({theme})=>({
|
|||
}
|
||||
}));
|
||||
|
||||
function ShowDataOutputQueryPopup({query}) {
|
||||
function suppressEnterKey(e) {
|
||||
if(e.keyCode == 13) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal container={document.body}>
|
||||
<StyledEditor ref={(ele)=>{
|
||||
setEditorPosition(document.getElementById('sql-query'), ele, '.MuiBox-root', 29);
|
||||
}} onKeyDown={suppressEnterKey}>
|
||||
<CodeMirror
|
||||
value={query || ''}
|
||||
className={'textarea'}
|
||||
readonly={true}
|
||||
/>
|
||||
</StyledEditor>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
ShowDataOutputQueryPopup.propTypes = {
|
||||
query: PropTypes.string,
|
||||
};
|
||||
|
||||
export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
||||
const eventBus = useContext(QueryToolEventsContext);
|
||||
const queryToolCtx = useContext(QueryToolContext);
|
||||
|
@ -182,28 +207,6 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
|||
},
|
||||
], queryToolCtx.mainContainerRef);
|
||||
|
||||
function suppressEnterKey(e) {
|
||||
if(e.keyCode == 13) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
const ShowDataOutputQueryPopup =()=> {
|
||||
return (
|
||||
<Portal container={document.body}>
|
||||
<StyledEditor ref={(ele)=>{
|
||||
setEditorPosition(document.getElementById('sql-query'), ele, '.MuiBox-root', 29);
|
||||
}} onKeyDown={suppressEnterKey}>
|
||||
<CodeMirror
|
||||
value={query || ''}
|
||||
className={'textarea'}
|
||||
readonly={true}
|
||||
/>
|
||||
</StyledEditor>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDiv>
|
||||
|
@ -234,13 +237,13 @@ export function ResultSetToolbar({query,canEdit, totalRowCount}) {
|
|||
<PgIconButton title={gettext('Graph Visualiser')} icon={<TimelineRoundedIcon />}
|
||||
onClick={showGraphVisualiser} disabled={buttonsDisabled['save-result']} />
|
||||
</PgButtonGroup>
|
||||
{query &&
|
||||
{query &&
|
||||
<>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton title={gettext('SQL query of data')} icon={<SQLQueryIcon />}
|
||||
onClick={()=>{setDataOutputQueryBtn(prev=>!prev);}} onBlur={()=>{setDataOutputQueryBtn(false);}} disabled={!query} id='sql-query'/>
|
||||
</PgButtonGroup>
|
||||
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup />}
|
||||
{ dataOutputQueryBtn && <ShowDataOutputQueryPopup query={query} />}
|
||||
</>
|
||||
}
|
||||
</StyledDiv>
|
||||
|
|
Loading…
Reference in New Issue