- Fix few SonarQube issues.

- Cleanup NW.js related stuff.
This commit is contained in:
Aditya Toshniwal 2024-07-02 10:34:30 +05:30 committed by GitHub
parent 3bb9f0ba8c
commit f8fa1cf6d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 785 additions and 634 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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/>

View File

@ -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.

View File

@ -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);
});
}

View File

@ -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),

View File

@ -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}

View File

@ -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]),

View File

@ -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>
);
}

View File

@ -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'}

View File

@ -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));
}, []);

View File

@ -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`));

View File

@ -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 (

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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() {

View File

@ -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();

View File

@ -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);

View File

@ -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}),
},
];

View File

@ -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>