Port the remaining components of the ERD Tool to React. Fixes #7343

1. Make use of MUI styles and remove SCSS.
2. Use the new common components for buttons and tooltips, so that they are consistent.
3. UI design should be aligned with the query tool.
4. Remove tippyjs and Alertify dependencies.
This commit is contained in:
Aditya Toshniwal
2022-09-06 18:09:13 +05:30
committed by Akshay Joshi
parent 1d0ac0f7dc
commit 0f46f070ed
64 changed files with 1451 additions and 1757 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -19,6 +19,7 @@ New features
Housekeeping
************
| `Issue #7343 <https://redmine.postgresql.org/issues/7343>`_ - Port the remaining components of the ERD Tool to React.
| `Issue #7622 <https://redmine.postgresql.org/issues/7622>`_ - Port search object dialog to React.
Bug fixes

View File

@@ -88,7 +88,6 @@
"@projectstorm/react-diagrams": "^6.6.1",
"@simonwep/pickr": "^1.5.1",
"@szhsin/react-menu": "^2.2.0",
"@tippyjs/react": "^4.2.0",
"@types/classnames": "^2.2.6",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",

View File

@@ -279,7 +279,7 @@ define('pgadmin.node.database', [
t = pgBrowser.tree,
i = input.item || t.selected(),
d = i ? t.itemData(i) : undefined;
pgBrowser.erd.showErdTool(d, i, true);
pgAdmin.Tools.ERD.showErdTool(d, i, true);
},
/* Connect the database (if not connected), before opening this node */

View File

@@ -0,0 +1 @@
<svg width="18" height="18" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M224 96l16-32 32-16-32-16-16-32-16 32-32 16 32 16 16 32zM80 160l26.66-53.33L160 80l-53.34-26.67L80 0 53.34 53.33 0 80l53.34 26.67L80 160zm352 128l-26.66 53.33L352 368l53.34 26.67L432 448l26.66-53.33L512 368l-53.34-26.67L432 288zm70.62-193.77L417.77 9.38C411.53 3.12 403.34 0 395.15 0c-8.19 0-16.38 3.12-22.63 9.38L9.38 372.52c-12.5 12.5-12.5 32.76 0 45.25l84.85 84.85c6.25 6.25 14.44 9.37 22.62 9.37 8.19 0 16.38-3.12 22.63-9.37l363.14-363.15c12.5-12.48 12.5-32.75 0-45.24zM359.45 203.46l-50.91-50.91 86.6-86.6 50.91 50.91-86.6 86.6z"></path></svg>

After

Width:  |  Height:  |  Size: 642 B

View File

@@ -0,0 +1,54 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="18" height="18" viewBox="0 0 548.29 548.291" style="enable-background:new 0 0 548.29 548.291;" xml:space="preserve">
<g>
<g>
<path d="M276.043,244.216c-24.575,0-38.741,24.087-38.741,53.862c-0.241,30.228,14.407,53.382,38.5,53.382
c24.323,0,38.512-22.92,38.512-54.091C314.313,268.303,300.604,244.216,276.043,244.216z"></path>
<path d="M486.2,196.116h-13.164V132.59c0-0.399-0.064-0.795-0.116-1.2c-0.021-2.52-0.824-4.997-2.551-6.96L364.656,3.677
c-0.031-0.031-0.064-0.044-0.085-0.075c-0.629-0.704-1.364-1.29-2.141-1.796c-0.231-0.154-0.462-0.283-0.704-0.419
c-0.672-0.365-1.386-0.672-2.121-0.893c-0.199-0.052-0.377-0.134-0.576-0.186C358.229,0.118,357.4,0,356.562,0H96.757
C84.893,0,75.256,9.649,75.256,21.502v174.613H62.093c-16.967,0-30.733,13.756-30.733,30.733v159.812
c0,16.961,13.766,30.731,30.733,30.731h13.163V526.79c0,11.854,9.637,21.501,21.501,21.501h354.777
c11.853,0,21.502-9.647,21.502-21.501V417.392H486.2c16.977,0,30.729-13.771,30.729-30.731V226.849
C516.93,209.872,503.177,196.116,486.2,196.116z M96.757,21.502h249.053v110.006c0,5.943,4.818,10.751,10.751,10.751h94.973
v53.861H96.757V21.502z M353.033,376.96l-10.394,27.884c-22.666-6.619-41.565-13.479-62.828-22.445
c-3.527-1.418-7.317-2.132-11.094-2.362c-35.909-2.352-69.449-28.819-69.449-80.778c0-47.711,30.236-83.623,77.71-83.623
c48.675,0,75.351,36.854,75.351,80.317c0,36.142-16.766,61.638-37.785,71.091v0.945
C326.828,371.528,340.519,374.367,353.033,376.96z M72.912,370.116l7.328-29.764c9.69,4.96,24.554,9.915,39.917,9.915
c16.525,0,25.271-6.84,25.271-17.228c0-9.928-7.56-15.597-26.691-22.442c-26.457-9.217-43.696-23.858-43.696-47.014
c0-27.163,22.68-47.948,60.231-47.948c17.954,0,31.184,3.791,40.623,8.03l-8.021,29.061c-6.375-3.076-17.711-7.564-33.3-7.564
c-15.599,0-23.163,7.079-23.163,15.357c0,10.15,8.977,14.646,29.533,22.447c28.108,10.394,41.332,25.023,41.332,47.464
c0,26.699-20.557,49.365-64.253,49.365C99.844,379.785,81.899,375.06,72.912,370.116z M451.534,520.962H96.757v-103.57h354.777
V520.962z M475.387,377.428h-99.455V218.231h36.158v128.97h63.297V377.428z"></path>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { useModalStyles } from '../../../../../../static/js/helpers/ModalProvider';
import { useModalStyles } from '../helpers/ModalProvider';
import gettext from 'sources/gettext';
import { Box } from '@material-ui/core';
import { DefaultButton, PrimaryButton } from '../../../../../../static/js/components/Buttons';
import { DefaultButton, PrimaryButton } from '../components/Buttons';
import CloseIcon from '@material-ui/icons/CloseRounded';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';

View File

@@ -508,7 +508,7 @@ export default function DataGridView({
setGlobalFilter(value || undefined);
}}
/>}
<div {...getTableProps()} className={classes.table}>
<div {...getTableProps(()=>({style: {minWidth: 'unset'}}))} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()} className={classes.tableContentWidth}>
{rows.map((row, i) => {

View File

@@ -100,7 +100,9 @@ export default function(basicSettings) {
cardHeaderBg: '#424242',
colorFg: '#FFFFFF',
emptySpaceBg: '#212121',
textMuted: '#8A8A8A'
textMuted: '#8A8A8A',
erdCanvasBg: '#303030',
erdGridColor: '#444952',
}
});
}

View File

@@ -98,7 +98,9 @@ export default function(basicSettings) {
cardHeaderBg: '#062F57',
colorFg: '#FFFFFF',
emptySpaceBg: '#010B15',
textMuted: '#8b9cad'
textMuted: '#8b9cad',
erdCanvasBg: '#010B15',
erdGridColor: '#1F2932',
}
});
}

View File

@@ -106,6 +106,8 @@ export default function(basicSettings) {
cardHeaderBg: '#fff',
emptySpaceBg: '#ebeef3',
textMuted: '#646B82',
erdCanvasBg: '#fff',
erdGridColor: '#bac1cd',
explain: {
sev2: {
color: '#222222',

View File

@@ -17,6 +17,8 @@ import Collapse from '../../img/fonticon/close_fullscreen.svg?svgr';
import AWS from '../../img/aws.svg?svgr';
import BigAnimal from '../../img/biganimal.svg?svgr';
import Azure from '../../img/azure.svg?svgr';
import SQLFileSvg from '../../img/sql_file.svg?svgr';
import MagicSvg from '../../img/magic.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className={'MuiSvgIcon-root'} {...props} />;
@@ -75,4 +77,10 @@ export const BigAnimalIcon = ({style})=><ExternalIcon Icon={BigAnimal} style={{h
BigAnimalIcon.propTypes = {style: PropTypes.object};
export const AzureIcon = ({style})=><ExternalIcon Icon={Azure} style={{height: '1.4rem', ...style}} data-label="AzureIcon" />;
AzureIcon.propTypes = {style: PropTypes.object};
AzureIcon.propTypes = {style: PropTypes.object};
export const SQLFileIcon = ({style})=><ExternalIcon Icon={SQLFileSvg} style={{height: '1rem', ...style}} data-label="SQLFileIcon" />;
SQLFileIcon.propTypes = {style: PropTypes.object};
export const MagicIcon = ({style})=><ExternalIcon Icon={MagicSvg} style={{height: '1rem', ...style}} data-label="MagicIcon" />;
MagicIcon.propTypes = {style: PropTypes.object};

View File

@@ -40,7 +40,7 @@ const useStyles = makeStyles((theme)=>({
}
}));
export default function Loader({message, style, ...props}) {
export default function Loader({message, style, autoEllipsis=false, ...props}) {
const classes = useStyles();
if(!message) {
return <></>;
@@ -49,7 +49,7 @@ export default function Loader({message, style, ...props}) {
<Box className={classes.root} style={style} data-label="loader" {...props}>
<Box className={classes.loaderRoot}>
<CircularProgress className={classes.loader} />
<Typography className={classes.message}>{message}</Typography>
<Typography className={classes.message}>{message}{autoEllipsis ? '...':''}</Typography>
</Box>
</Box>
);
@@ -58,4 +58,5 @@ export default function Loader({message, style, ...props}) {
Loader.propTypes = {
message: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
autoEllipsis: PropTypes.bool,
};

View File

@@ -98,7 +98,7 @@ export default function ModalProvider({ children }) {
const [modals, setModals] = React.useState([]);
const showModal = (title, content, modalOptions) => {
let id = getEpoch().toString() + crypto.getRandomValues(new Uint8Array(1));
let id = getEpoch().toString() + crypto.getRandomValues(new Uint8Array(4));
setModals((prev) => [...prev, {
id: id,
title: title,

View File

@@ -1,9 +0,0 @@
@import "node_modules/tippy.js/dist/tippy.css";
.tippy-box {
background-color: $popover-bg;
color: $popover-body-color;
.tippy-arrow {
color: $popover-bg;
}
}

View File

@@ -31,7 +31,6 @@ $theme-colors: (
@import 'pgadmin.style';
@import 'bootstrap4-toggle.overrides';
@import 'pickr.overrides';
@import 'tippy.overrides';
@import 'jsoneditor.overrides';
@import 'pgadmin4-tree.overrides';
@import 'pgadmin4-tree/src/css/styles';

View File

@@ -399,8 +399,6 @@ def panel(trans_id):
if 'gen' in params:
params['gen'] = True if params['gen'] == 'true' else False
close_url = request.form['close_url']
# We need client OS information to render correct Keyboard shortcuts
user_agent = UserAgent(request.headers.get('User-Agent'))
@@ -439,7 +437,6 @@ def panel(trans_id):
return render_template(
"erd/index.html",
title=underscore_unescape(params['title']),
close_url=close_url,
requirejs=True,
basejs=True,
params=json.dumps(params),

View File

@@ -0,0 +1,240 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgWindow from 'sources/window';
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
import {getRandomInt, registerDetachEvent} from 'sources/utils';
import Notify from '../../../../static/js/helpers/Notifier';
import url_for from 'sources/url_for';
import gettext from 'sources/gettext';
import React from 'react';
import ReactDOM from 'react-dom';
import ERDTool from './erd_tool/components/ERDTool';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme';
import $ from 'jquery';
const wcDocker = window.wcDocker;
export function setPanelTitle(erdToolPanel, panelTitle) {
erdToolPanel?.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
}
export default class ERDModule {
static instance;
static getInstance(...args) {
if(!ERDModule.instance) {
ERDModule.instance = new ERDModule(...args);
}
return ERDModule.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
}
init() {
if (this.initialized)
return;
this.initialized = true;
// Define the nodes on which the menus to be appear
this.pgBrowser.add_menus([{
name: 'erd',
module: this,
applies: ['tools'],
callback: 'showErdTool',
priority: 1,
label: gettext('ERD Tool'),
enable: this.erdToolEnabled,
data: {
data_disabled: gettext('The selected tree node does not support this option.'),
},
}]);
// Creating a new this.pgBrowser frame to show the data.
const erdFrameType = new this.pgBrowser.Frame({
name: 'frm_erdtool',
showTitle: true,
isCloseable: true,
isPrivate: true,
url: 'about:blank',
});
// Load the newly created frame
erdFrameType.load(this.pgBrowser.docker);
return this;
}
erdToolEnabled(obj) {
/* Same as query tool */
return (() => {
if (!_.isUndefined(obj) && !_.isNull(obj)) {
if (_.indexOf(this.pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
}
// Callback to draw ERD Tool for objects
showErdTool(_data, treeIdentifier, gen=false) {
if (treeIdentifier === undefined) {
Notify.alert(
gettext('ERD Error'),
gettext('No object selected.')
);
return;
}
const parentData = this.pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
if(_.isUndefined(parentData.database)) {
Notify.alert(
gettext('ERD Error'),
gettext('Please select a database/database object.')
);
return;
}
const transId = getRandomInt(1, 9999999);
const panelTitle = getPanelTitle(this.pgBrowser, treeIdentifier);
const panelUrl = this.getPanelUrl(transId, parentData, gen);
let erdToolForm = `
<form id="erdToolForm" action="${panelUrl}" method="post">
<input id="title" name="title" hidden />
</form>
<script>
document.getElementById("title").value = "${_.escape(panelTitle)}";
document.getElementById("erdToolForm").submit();
</script>
`;
var open_new_tab = this.pgBrowser.get_preferences_for_module('browser').new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('erd_tool')) {
var newWin = window.open('', '_blank');
newWin.document.write(erdToolForm);
newWin.document.title = panelTitle;
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
this.pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
/* On successfully initialization find the dashboard panel,
* create new panel and add it to the dashboard panel.
*/
var propertiesPanel = this.pgBrowser.docker.findPanels('properties');
var erdToolPanel = this.pgBrowser.docker.addPanel('frm_erdtool', wcDocker.DOCK.STACKED, propertiesPanel[0]);
// Set panel title and icon
setPanelTitle(erdToolPanel, 'Untitled');
erdToolPanel.icon('fa fa-sitemap');
erdToolPanel.focus();
// Register detach event.
registerDetachEvent(erdToolPanel);
var openErdToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).appendTo($(j).data('embeddedFrame').$container);
let init_poller_id = setInterval(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
clearInterval(init_poller_id);
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.onLoaded(()=>{
$spinner_el.remove();
});
frame.openHTML(erdToolForm);
}
}
}, 100);
};
openErdToolURL(erdToolPanel);
}
}
getPanelUrl(transId, parentData, gen) {
let openUrl = url_for('erd.panel', {
trans_id: transId,
});
openUrl += `?sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`
+`&server_type=${parentData.server.server_type}`
+`&did=${parentData.database._id}`
+`&gen=${gen}`;
return openUrl;
}
setupPreferencesWorker() {
if (window.location == window.parent?.location) {
/* Sync the local preferences with the main window if in new tab */
setInterval(()=>{
if(pgWindow?.pgAdmin) {
if(this.pgAdmin.Browser.preference_version() < pgWindow.pgAdmin.Browser.preference_version()){
this.pgAdmin.Browser.preferences_cache = pgWindow.pgAdmin.Browser.preferences_cache;
this.pgAdmin.Browser.preference_version(pgWindow.pgAdmin.Browser.preference_version());
this.pgAdmin.Browser.triggerPreferencesChange('browser');
this.pgAdmin.Browser.triggerPreferencesChange('erd');
}
}
}, 1000);
}
}
loadComponent(container, params) {
let panel = null;
/* Mount the React ERD tool to the container */
_.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
if (p.isVisible()) {
panel = p;
}
});
this.setupPreferencesWorker();
ReactDOM.render(
<Theme>
<ModalProvider>
<ERDTool
params={params}
pgWindow={pgWindow}
pgAdmin={this.pgAdmin}
panel={panel}
/>
</ModalProvider>
</Theme>,
container
);
}
}

View File

@@ -1,217 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
import {getRandomInt, registerDetachEvent} from 'sources/utils';
import Notify from '../../../../static/js/helpers/Notifier';
export function setPanelTitle(erdToolPanel, panelTitle) {
erdToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
}
export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser, wcDocker) {
/* Return back, this has been called more than once */
if (pgBrowser.erd)
return pgBrowser.erd;
pgBrowser.erd = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
// Define the nodes on which the menus to be appear
var menus = [{
name: 'erd',
module: this,
applies: ['tools'],
callback: 'showErdTool',
priority: 1,
label: gettext('ERD Tool'),
enable: this.erdToolEnabled,
data: {
data_disabled: gettext('The selected tree node does not support this option.'),
},
}];
pgBrowser.add_menus(menus);
// Creating a new pgBrowser frame to show the data.
var erdFrameType = new pgBrowser.Frame({
name: 'frm_erdtool',
showTitle: true,
isCloseable: true,
isPrivate: true,
url: 'about:blank',
});
let self = this;
/* Cache may take time to load for the first time
* Keep trying till available
*/
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('erd');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('erd', function() {
self.preferences = pgBrowser.get_preferences_for_module('erd');
});
// Load the newly created frame
erdFrameType.load(pgBrowser.docker);
return this;
},
erdToolEnabled: function(obj) {
/* Same as query tool */
return (() => {
if (!_.isUndefined(obj) && !_.isNull(obj)) {
if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) {
if (obj._type == 'database' && obj.allowConn) {
return true;
} else if (obj._type != 'database') {
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
})();
},
// Callback to draw ERD Tool for objects
showErdTool: function(data, treeIdentifier, gen=false) {
if (treeIdentifier === undefined) {
Notify.alert(
gettext('ERD Error'),
gettext('No object selected.')
);
return;
}
const parentData = pgBrowser.tree.getTreeNodeHierarchy(treeIdentifier);
if(_.isUndefined(parentData.database)) {
Notify.alert(
gettext('ERD Error'),
gettext('Please select a database/database object.')
);
return;
}
const transId = getRandomInt(1, 9999999);
const panelTitle = getPanelTitle(pgBrowser, treeIdentifier);
const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
let erdToolForm = `
<form id="erdToolForm" action="${panelUrl}" method="post">
<input id="title" name="title" hidden />
<input name="close_url" value="${panelCloseUrl}" hidden />
</form>
<script>
document.getElementById("title").value = "${_.escape(panelTitle)}";
document.getElementById("erdToolForm").submit();
</script>
`;
var open_new_tab = pgBrowser.get_preferences_for_module('browser').new_browser_tab_open;
if (open_new_tab && open_new_tab.includes('erd_tool')) {
var newWin = window.open('', '_blank');
newWin.document.write(erdToolForm);
newWin.document.title = panelTitle;
// Send the signal to runtime, so that proper zoom level will be set.
setTimeout(function() {
pgBrowser.send_signal_to_runtime('Runtime new window opened');
}, 500);
} else {
/* On successfully initialization find the dashboard panel,
* create new panel and add it to the dashboard panel.
*/
var propertiesPanel = pgBrowser.docker.findPanels('properties');
var erdToolPanel = pgBrowser.docker.addPanel('frm_erdtool', wcDocker.DOCK.STACKED, propertiesPanel[0]);
// Set panel title and icon
setPanelTitle(erdToolPanel, 'Untitled');
erdToolPanel.icon('fa fa-sitemap');
erdToolPanel.focus();
// Listen on the panel closed event.
erdToolPanel.on(wcDocker.EVENT.CLOSED, function() {
$.ajax({
url: panelCloseUrl,
method: 'DELETE',
});
});
// Register detach event.
registerDetachEvent(erdToolPanel);
var openErdToolURL = function(j) {
// add spinner element
let $spinner_el =
$(`<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>`).appendTo($(j).data('embeddedFrame').$container);
let init_poller_id = setInterval(function() {
var frameInitialized = $(j).data('frameInitialized');
if (frameInitialized) {
clearInterval(init_poller_id);
var frame = $(j).data('embeddedFrame');
if (frame) {
frame.onLoaded(()=>{
$spinner_el.remove();
});
frame.openHTML(erdToolForm);
}
}
}, 100);
};
openErdToolURL(erdToolPanel);
}
},
getPanelUrls: function(transId, panelTitle, parentData, gen) {
let openUrl = url_for('erd.panel', {
trans_id: transId,
});
openUrl += `?sgid=${parentData.server_group._id}`
+`&sid=${parentData.server._id}`
+`&server_type=${parentData.server.server_type}`
+`&did=${parentData.database._id}`
+`&gen=${gen}`;
let closeUrl = url_for('erd.close', {
trans_id: transId,
sgid: parentData.server_group._id,
sid: parentData.server._id,
did: parentData.database._id,
});
return [openUrl, closeUrl];
},
};
return pgBrowser.erd;
}

View File

@@ -0,0 +1,21 @@
export const ERD_EVENTS = {
LOAD_DIAGRAM: 'LOAD_DIAGRAM',
SAVE_DIAGRAM: 'SAVE_DIAGRAM',
SHOW_SQL: 'SHOW_SQL',
DOWNLOAD_IMAGE: 'DOWNLOAD_IMAGE',
ADD_NODE: 'ADD_NODE',
EDIT_NODE: 'EDIT_NODE',
CLONE_NODE: 'CLONE_NODE',
DELETE_NODE: 'DELETE_NODE',
SHOW_NOTE: 'SHOW_NOTE',
ONE_TO_MANY: 'ONE_TO_MANY',
MANY_TO_MANY: 'MANY_TO_MANY',
AUTO_DISTRIBUTE: 'AUTO_DISTRIBUTE',
TOGGLE_DETAILS: 'TOGGLE_DETAILS',
ZOOM_FIT: 'ZOOM_FIT',
ZOOM_IN: 'ZOOM_IN',
ZOOM_OUT: 'ZOOM_OUT',
SINGLE_NODE_SELECTED: 'SINGLE_NODE_SELECTED',
ANY_ITEM_SELECTED: 'ANY_ITEM_SELECTED',
DIRTY: 'DIRTY',
};

View File

@@ -0,0 +1,100 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useMemo } from 'react';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import { DefaultButton, PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import { Box, makeStyles, Tooltip, CircularProgress } from '@material-ui/core';
import { ConnectedIcon, DisonnectedIcon } from '../../../../../../static/js/components/ExternalIcon';
export const STATUS = {
CONNECTED: 1,
DISCONNECTED: 2,
CONNECTING: 3,
FAILED: 4,
};
function ConnectionStatusIcon({status}) {
if(status == STATUS.CONNECTING) {
return <CircularProgress style={{height: '18px', width: '18px'}} />;
} else if(status == STATUS.CONNECTED || status == STATUS.FAILED) {
return <ConnectedIcon />;
} else {
return <DisonnectedIcon />;
}
}
ConnectionStatusIcon.propTypes = {
status: PropTypes.oneOf(Object.values(STATUS)).isRequired,
};
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
flexWrap: 'wrap',
},
connectionButton: {
display: 'flex',
width: '450px',
backgroundColor: theme.palette.default.main,
color: theme.palette.default.contrastText,
border: '1px solid ' + theme.palette.default.borderColor,
justifyContent: 'flex-start',
},
}));
/* The connection bar component */
export default function ConnectionBar({status, bgcolor, fgcolor, title}) {
const classes = useStyles();
const connTitle = useMemo(()=>{
if(status == STATUS.CONNECTED) {
return gettext('Connected');
} else if(status == STATUS.CONNECTING) {
return gettext('Connecting');
} else if(status == STATUS.DISCONNECTED) {
return gettext('Disconnected');
} else if(status == STATUS.FAILED) {
return gettext('Failed');
}
}, [status]);
return (
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton
title={connTitle}
icon={<ConnectionStatusIcon status={status} />}
data-test="btn-conn-status"
/>
<DefaultButton className={classes.connectionButton} style={{backgroundColor: bgcolor, color: fgcolor}} data-test="btn-conn-title">
<Tooltip title={title}>
<Box display="flex" width="100%">
<Box textOverflow="ellipsis" overflow="hidden" marginRight="auto">
{status == STATUS.CONNECTING && (gettext('(Obtaining connection...)')+' ')}
{status == STATUS.FAILED && (gettext('(Connection failed)')+' ')}
{title}
</Box>
</Box>
</Tooltip>
</DefaultButton>
</PgButtonGroup>
</Box>
);
}
ConnectionBar.propTypes = {
status: PropTypes.oneOf(Object.values(STATUS)).isRequired,
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
title: PropTypes.string.isRequired,
};

View File

@@ -9,25 +9,29 @@
import * as React from 'react';
import { CanvasWidget, Action, InputType } from '@projectstorm/react-canvas-core';
import axios from 'axios';
import PropTypes from 'prop-types';
import _ from 'lodash';
import html2canvas from 'html2canvas';
import ERDCore from '../ERDCore';
import ToolBar, {IconButton, DetailsToggleButton, ButtonGroup} from './ToolBar';
import ConnectionBar, { STATUS as CONNECT_STATUS } from './ConnectionBar';
import Loader from './Loader';
import FloatingNote from './FloatingNote';
import {setPanelTitle} from '../../erd_module';
import {setPanelTitle} from '../../ERDModule';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import {showERDSqlTool} from 'tools/sqleditor/static/js/show_query_tool';
import 'wcdocker';
import Theme from '../../../../../../static/js/Theme';
import TableSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
import Notify from '../../../../../../static/js/helpers/Notifier';
import { ModalContext } from '../../../../../../static/js/helpers/ModalProvider';
import ERDDialogs from '../dialogs';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import Loader from '../../../../../../static/js/components/Loader';
import { MainToolBar } from './MainToolBar';
import { Box, withStyles } from '@material-ui/core';
import EventBus from '../../../../../../static/js/helpers/EventBus';
import { ERD_EVENTS } from '../ERDConstants';
import getApiInstance, { parseApiError } from '../../../../../../static/js/api_instance';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@@ -60,11 +64,41 @@ export class KeyboardShortcutAction extends Action {
}
}
const getCanvasGrid = (theme)=>{
let erdCanvasBg = encodeURIComponent(theme.otherVars.erdCanvasBg);
let erdGridColor = encodeURIComponent(theme.otherVars.erdGridColor);
return `url("data:image/svg+xml, %3Csvg width='100%25' viewBox='0 0 45 45' style='background-color:${erdCanvasBg}' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='15' height='15' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 15 0 L 0 0 0 15' fill='none' stroke='${erdGridColor}' stroke-width='0.5'/%3E%3C/pattern%3E%3Cpattern id='grid' width='45' height='45' patternUnits='userSpaceOnUse'%3E%3Crect width='100' height='100' fill='url(%23smallGrid)'/%3E%3Cpath d='M 100 0 L 0 0 0 100' fill='none' stroke='${erdGridColor}' stroke-width='1'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)' /%3E%3C/svg%3E%0A")`;
};
const styles = ((theme)=>({
diagramContainer: {
position: 'relative',
width: '100%',
height: '100%',
minHeight: 0,
},
diagramCanvas: {
width: '100%',
height: '100%',
color: theme.palette.text.primary,
fontFamily: 'sans-serif',
backgroundColor: theme.otherVars.erdCanvasBg,
backgroundImage: getCanvasGrid(theme),
cursor: 'unset',
flexGrow: 1,
},
html2canvasReset: {
backgroundImage: 'none !important',
overflow: 'auto !important',
}
}));
/* The main body container for the ERD */
export default class BodyWidget extends React.Component {
class ERDTool extends React.Component {
static contextType = ModalContext;
constructor() {
super();
constructor(props) {
super(props);
this.state = {
conn_status: CONNECT_STATUS.DISCONNECTED,
server_version: null,
@@ -89,11 +123,16 @@ export default class BodyWidget extends React.Component {
/* Flag for checking if user has opted for save before close */
this.closeOnSave = React.createRef();
this.fileInputRef = React.createRef();
this.containerRef = React.createRef();
this.diagramContainerRef = React.createRef();
this.canvasEle = null;
this.noteRefEle = null;
this.noteNode = null;
this.keyboardActionObj = null;
this.erdDialogs = new ERDDialogs(this.context);
this.apiObj = getApiInstance();
this.eventBus = new EventBus();
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSaveAsDiagram', 'onSQLClick',
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
@@ -125,22 +164,29 @@ export default class BodyWidget extends React.Component {
singleNodeSelected = true;
}
}
const anyItemSelected = this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0;
this.setState({
single_node_selected: singleNodeSelected,
any_item_selected: this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0,
any_item_selected: anyItemSelected,
});
this.eventBus.fireEvent(ERD_EVENTS.SINGLE_NODE_SELECTED, singleNodeSelected);
this.eventBus.fireEvent(ERD_EVENTS.ANY_ITEM_SELECTED, anyItemSelected);
},
'linksSelectionChanged': ()=>{
const anyItemSelected = this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0;
this.setState({
single_link_selected: this.diagram.getSelectedLinks().length == 1,
any_item_selected: this.diagram.getSelectedNodes().length > 0 || this.diagram.getSelectedLinks().length > 0,
});
this.eventBus.fireEvent(ERD_EVENTS.ANY_ITEM_SELECTED, anyItemSelected);
},
'linksUpdated': () => {
this.setState({dirty: true});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true);
},
'nodesUpdated': ()=>{
this.setState({dirty: true});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, true);
},
'showNote': (event)=>{
this.showNote(event.node);
@@ -154,53 +200,94 @@ export default class BodyWidget extends React.Component {
});
}
registerEvents() {
this.eventBus.registerListener(ERD_EVENTS.LOAD_DIAGRAM, this.onLoadDiagram);
this.eventBus.registerListener(ERD_EVENTS.SAVE_DIAGRAM, this.onSaveDiagram);
this.eventBus.registerListener(ERD_EVENTS.SHOW_SQL, this.onSQLClick);
this.eventBus.registerListener(ERD_EVENTS.DOWNLOAD_IMAGE, this.onImageClick);
this.eventBus.registerListener(ERD_EVENTS.ADD_NODE, this.onAddNewNode);
this.eventBus.registerListener(ERD_EVENTS.EDIT_NODE, this.onEditTable);
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
this.eventBus.registerListener(ERD_EVENTS.DELETE_NODE, this.onDeleteNode);
this.eventBus.registerListener(ERD_EVENTS.SHOW_NOTE, this.onNoteClick);
this.eventBus.registerListener(ERD_EVENTS.ONE_TO_MANY, this.onOneToManyClick);
this.eventBus.registerListener(ERD_EVENTS.MANY_TO_MANY, this.onManyToManyClick);
this.eventBus.registerListener(ERD_EVENTS.AUTO_DISTRIBUTE, this.onAutoDistribute);
this.eventBus.registerListener(ERD_EVENTS.TOGGLE_DETAILS, this.onDetailsToggle);
this.eventBus.registerListener(ERD_EVENTS.ZOOM_FIT, this.diagram.zoomToFit);
this.eventBus.registerListener(ERD_EVENTS.ZOOM_IN, this.diagram.zoomIn);
this.eventBus.registerListener(ERD_EVENTS.ZOOM_OUT, this.diagram.zoomOut);
}
registerKeyboardShortcuts() {
/* First deregister to avoid double events */
this.keyboardActionObj && this.diagram.deregisterKeyAction(this.keyboardActionObj);
this.keyboardActionObj = new KeyboardShortcutAction([
[this.state.preferences.open_project, this.onLoadDiagram],
[this.state.preferences.save_project, this.onSaveDiagram],
[this.state.preferences.save_project_as, this.onSaveAsDiagram],
[this.state.preferences.generate_sql, this.onSQLClick],
[this.state.preferences.download_image, this.onImageClick],
[this.state.preferences.add_table, this.onAddNewNode],
[this.state.preferences.edit_table, this.onEditTable],
[this.state.preferences.clone_table, this.onCloneNode],
[this.state.preferences.drop_table, this.onDeleteNode],
[this.state.preferences.add_edit_note, this.onNoteClick],
[this.state.preferences.one_to_many, this.onOneToManyClick],
[this.state.preferences.many_to_many, this.onManyToManyClick],
[this.state.preferences.auto_align, this.onAutoDistribute],
[this.state.preferences.show_details, this.onDetailsToggle],
[this.state.preferences.zoom_to_fit, this.diagram.zoomToFit],
[this.state.preferences.zoom_in, this.diagram.zoomIn],
[this.state.preferences.zoom_out, this.diagram.zoomOut],
[this.state.preferences.open_project, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.LOAD_DIAGRAM);
}],
[this.state.preferences.save_project, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.SAVE_DIAGRAM);
}],
[this.state.preferences.save_project_as, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.SAVE_DIAGRAM, true);
}],
[this.state.preferences.generate_sql, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.SHOW_SQL);
}],
[this.state.preferences.download_image, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
}],
[this.state.preferences.add_table, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ADD_NODE);
}],
[this.state.preferences.edit_table, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.EDIT_NODE);
}],
[this.state.preferences.clone_table, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.CLONE_NODE);
}],
[this.state.preferences.drop_table, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.DELETE_NODE);
}],
[this.state.preferences.add_edit_note, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.SHOW_NOTE);
}],
[this.state.preferences.one_to_many, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
}],
[this.state.preferences.many_to_many, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.MANY_TO_MANY);
}],
[this.state.preferences.auto_align, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.AUTO_DISTRIBUTE);
}],
[this.state.preferences.show_details, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.TOGGLE_DETAILS);
}],
[this.state.preferences.zoom_to_fit, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ZOOM_FIT);
}],
[this.state.preferences.zoom_in, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ZOOM_IN);
}],
[this.state.preferences.zoom_out, ()=>{
this.eventBus.fireEvent(ERD_EVENTS.ZOOM_OUT);
}],
]);
this.diagram.registerKeyAction(this.keyboardActionObj);
}
handleAxiosCatch(err) {
if (err.response) {
// client received an error response (5xx, 4xx)
Notify.alert(
gettext('Error'),
`${err.response.statusText} - ${err.response.data.errormsg}`
);
console.error('response error', err.response);
} else if (err.request) {
// client never received a response, or request never left
Notify.alert(gettext('Error'), gettext('Client error') + ':' + err);
console.error('client eror', err);
} else {
Notify.alert(gettext('Error'), err.message);
console.error('other error', err);
}
console.error(err);
Notify.alert(gettext('Error'), parseApiError(err));
}
async componentDidMount() {
this.setLoading(gettext('Preparing...'));
this.registerEvents();
this.setState({
preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
@@ -216,7 +303,7 @@ export default class BodyWidget extends React.Component {
backgroundPosition: '0px 0px',
});
this.props.pgAdmin.Browser.onPreferencesChange('erd', () => {
this.props.pgWindow.pgAdmin.Browser.onPreferencesChange('erd', () => {
this.setState({
preferences: this.props.pgWindow.pgAdmin.Browser.get_preferences_for_module('erd'),
}, ()=>this.registerKeyboardShortcuts());
@@ -232,6 +319,15 @@ export default class BodyWidget extends React.Component {
return true;
});
window.addEventListener('unload', ()=>{
this.apiObj.delete(url_for('erd.close', {
trans_id: this.props.params.trans_id,
sgid: this.props.params.sgid,
sid: this.props.params.sid,
did: this.props.params.did
}));
});
let done = await this.initConnection();
if(!done) return;
@@ -257,52 +353,18 @@ export default class BodyWidget extends React.Component {
confirmBeforeClose() {
let bodyObj = this;
this.props.alertify.confirmSave || this.props.alertify.dialog('confirmSave', function() {
return {
main: function(title, message) {
this.setHeader(title);
this.setContent(message);
},
setup: function() {
return {
buttons: [{
text: gettext('Cancel'),
key: 27, // ESC
invokeOnClose: true,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
}, {
text: gettext('Don\'t save'),
className: 'btn btn-secondary fa fa-lg fa-trash-alt pg-alertify-button',
}, {
text: gettext('Save'),
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
}],
focus: {
element: 0,
select: false,
},
options: {
maximizable: false,
resizable: false,
},
};
},
callback: function(closeEvent) {
switch (closeEvent.index) {
case 0: // Cancel
//Do nothing.
break;
case 1: // Don't Save
bodyObj.closePanel();
break;
case 2: //Save
bodyObj.onSaveDiagram(false, true);
break;
}
},
};
});
this.props.alertify.confirmSave(gettext('Save changes?'), gettext('The diagram has changed. Do you want to save changes?'));
this.context.showModal(gettext('Save changes?'), (closeModal)=>(
<ConfirmSaveContent
closeModal={closeModal}
text={gettext('The diagram has changed. Do you want to save changes?')}
onDontSave={()=>{
bodyObj.closePanel();
}}
onSave={()=>{
bodyObj.onSaveDiagram(false, true);
}}
/>
));
return false;
}
@@ -318,15 +380,18 @@ export default class BodyWidget extends React.Component {
};
if(dialogName === 'table_dialog') {
return (title, attributes, isNew, callback)=>{
this.props.getDialog(dialogName).show(
title, attributes, isNew, this.diagram.getModel().getNodesDict(), this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), serverInfo, callback
);
this.erdDialogs.showTableDialog({
title, attributes, isNew, tableNodes: this.diagram.getModel().getNodesDict(),
colTypes: this.diagram.getCache('colTypes'), schemas: this.diagram.getCache('schemas'),
serverInfo, callback
});
};
} else if(dialogName === 'onetomany_dialog' || dialogName === 'manytomany_dialog') {
return (title, attributes, callback)=>{
this.props.getDialog(dialogName).show(
title, attributes, this.diagram.getModel().getNodesDict(), serverInfo, callback
);
this.erdDialogs.showRelationDialog(dialogName, {
title, attributes, tableNodes: this.diagram.getModel().getNodesDict(),
serverInfo, callback
});
};
}
}
@@ -386,7 +451,7 @@ export default class BodyWidget extends React.Component {
Notify.error(gettext('Cannot drop table from outside of the current database.'));
} else {
let dataPromise = new Promise((resolve, reject)=>{
axios.get(nodeDropData.objUrl)
this.apiObj.get(nodeDropData.objUrl)
.then((res)=>{
resolve(this.diagram.cloneTableData(TableSchema.getErdSupportedData(res.data)));
})
@@ -467,7 +532,7 @@ export default class BodyWidget extends React.Component {
}
onLoadDiagram() {
var params = {
const params = {
'supported_types': ['*','pgerd'], // file types allowed
'dialog_type': 'select_file', // open select file dialog
};
@@ -476,13 +541,14 @@ export default class BodyWidget extends React.Component {
openFile(fileName) {
this.setLoading(gettext('Loading project...'));
axios.post(url_for('sqleditor.load_file'), {
this.apiObj.post(url_for('sqleditor.load_file'), {
'file_name': decodeURI(fileName),
}).then((res)=>{
this.setState({
current_file: fileName,
dirty: false,
});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, false);
this.setTitle(fileName);
this.diagram.deserialize(res.data);
this.diagram.clearSelection();
@@ -515,7 +581,7 @@ export default class BodyWidget extends React.Component {
saveFile(fileName) {
this.setLoading(gettext('Saving...'));
axios.post(url_for('sqleditor.save_file'), {
this.apiObj.post(url_for('sqleditor.save_file'), {
'file_name': decodeURI(fileName),
'file_content': JSON.stringify(this.diagram.serialize(this.props.pgAdmin.Browser.utils.app_version_int)),
}).then(()=>{
@@ -524,6 +590,7 @@ export default class BodyWidget extends React.Component {
current_file: fileName,
dirty: false,
});
this.eventBus.fireEvent(ERD_EVENTS.DIRTY, false);
this.setTitle(fileName);
this.setLoading(null);
if(this.closeOnSave) {
@@ -564,7 +631,7 @@ export default class BodyWidget extends React.Component {
});
this.setLoading(gettext('Preparing the SQL...'));
axios.post(url, this.diagram.serializeData())
this.apiObj.post(url, this.diagram.serializeData())
.then((resp)=>{
let sqlScript = resp.data.data;
sqlScript = scriptHeader + 'BEGIN;\n' + sqlScript + '\nEND;';
@@ -579,7 +646,7 @@ export default class BodyWidget extends React.Component {
let sqlId = `erd${this.props.params.trans_id}`;
localStorage.setItem(sqlId, sqlScript);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.Tools.SQLEditor, this.props.alertify);
showERDSqlTool(parentData, sqlId, this.props.params.title, this.props.pgWindow.pgAdmin.Tools.SQLEditor);
})
.catch((error)=>{
this.handleAxiosCatch(error);
@@ -617,7 +684,7 @@ export default class BodyWidget extends React.Component {
});
/* Change the styles for suiting html2canvas */
this.canvasEle.classList.add('html2canvas-reset');
this.canvasEle.classList.add(this.props.classes.html2canvasReset);
this.canvasEle.style.width = this.canvasEle.scrollWidth + 'px';
this.canvasEle.style.height = this.canvasEle.scrollHeight + 'px';
@@ -628,7 +695,8 @@ export default class BodyWidget extends React.Component {
'color',
'font-size',
'stroke',
'font'
'font',
'display',
];
let svgElems = Array.from(targetElem.getElementsByTagName('svg'));
for (let svgEle of svgElems) {
@@ -638,6 +706,7 @@ export default class BodyWidget extends React.Component {
let wrap = document.createElement('div');
wrap.setAttribute('style', svgEle.getAttribute('style'));
svgEle.setAttribute('style', null);
svgEle.style.display = 'block';
svgEle.parentNode.insertBefore(wrap, svgEle);
wrap.appendChild(svgEle);
recurseElementChildren(svgEle);
@@ -679,7 +748,7 @@ export default class BodyWidget extends React.Component {
allowTaint: true,
backgroundColor: window.getComputedStyle(this.canvasEle).backgroundColor,
onclone: (clonedEle)=>{
setSvgInlineStyles(clonedEle);
setSvgInlineStyles(clonedEle.body.querySelector('div[data-test="diagram-container"]'));
return clonedEle;
},
}).then((canvas)=>{
@@ -697,7 +766,7 @@ export default class BodyWidget extends React.Component {
Notify.alert(gettext('Error'), msg);
}).then(()=>{
/* Revert back to the original CSS styles */
this.canvasEle.classList.remove('html2canvas-reset');
this.canvasEle.classList.remove(this.props.classes.html2canvasReset);
this.canvasEle.style.width = '';
this.canvasEle.style.height = '';
this.canvasEle.childNodes.forEach((ele)=>{
@@ -760,7 +829,7 @@ export default class BodyWidget extends React.Component {
});
try {
let response = await axios.post(initUrl);
let response = await this.apiObj.post(initUrl);
this.setState({
conn_status: CONNECT_STATUS.CONNECTED,
server_version: response.data.data.serverVersion,
@@ -789,7 +858,7 @@ export default class BodyWidget extends React.Component {
});
try {
let response = await axios.get(url);
let response = await this.apiObj.get(url);
let data = response.data.data;
this.diagram.setCache('colTypes', data['col_types']);
this.diagram.setCache('schemas', data['schemas']);
@@ -812,7 +881,7 @@ export default class BodyWidget extends React.Component {
});
try {
let response = await axios.get(url);
let response = await this.apiObj.get(url);
this.diagram.deserializeData(response.data.data);
return true;
} catch (error) {
@@ -824,74 +893,27 @@ export default class BodyWidget extends React.Component {
}
render() {
this.erdDialogs.modal = this.context;
return (
<Theme>
<ToolBar id="btn-toolbar">
<ButtonGroup>
<IconButton id="open-file" icon="fa fa-folder-open" onClick={this.onLoadDiagram} title={gettext('Load from file')}
shortcut={this.state.preferences.open_project}/>
<IconButton id="save-erd" icon="fa fa-save" onClick={()=>{this.onSaveDiagram();}} title={gettext('Save project')}
shortcut={this.state.preferences.save_project} disabled={!this.state.dirty}/>
<IconButton id="save-as-erd" icon="fa fa-share-square" onClick={this.onSaveAsDiagram} title={gettext('Save as')}
shortcut={this.state.preferences.save_project_as}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="save-sql" icon="fa fa-file-code" onClick={this.onSQLClick} title={gettext('Generate SQL')}
shortcut={this.state.preferences.generate_sql}/>
<IconButton id="save-image" icon="fa fa-file-image" onClick={this.onImageClick} title={gettext('Download image')}
shortcut={this.state.preferences.download_image}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="add-node" icon="fa fa-plus-square" onClick={this.onAddNewNode} title={gettext('Add table')}
shortcut={this.state.preferences.add_table}/>
<IconButton id="edit-node" icon="fa fa-pencil-alt" onClick={this.onEditTable} title={gettext('Edit table')}
shortcut={this.state.preferences.edit_table} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
<IconButton id="clone-node" icon="fa fa-clone" onClick={this.onCloneNode} title={gettext('Clone table')}
shortcut={this.state.preferences.clone_table} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
<IconButton id="delete-node" icon="fa fa-trash-alt" onClick={this.onDeleteNode} title={gettext('Drop table/link')}
shortcut={this.state.preferences.drop_table} disabled={!this.state.any_item_selected}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="add-onetomany" text="1M" onClick={this.onOneToManyClick} title={gettext('One-to-Many link')}
shortcut={this.state.preferences.one_to_many} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
<IconButton id="add-manytomany" text="MM" onClick={this.onManyToManyClick} title={gettext('Many-to-Many link')}
shortcut={this.state.preferences.many_to_many} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="add-note" icon="fa fa-sticky-note" onClick={this.onNoteClick} title={gettext('Add/Edit note')}
shortcut={this.state.preferences.add_edit_note} disabled={!this.state.single_node_selected || this.state.single_link_selected}/>
<IconButton id="auto-align" icon="fa fa-magic" onClick={this.onAutoDistribute} title={gettext('Auto align')}
shortcut={this.state.preferences.auto_align} />
<DetailsToggleButton id="more-details" onClick={this.onDetailsToggle} showDetails={this.state.show_details}
shortcut={this.state.preferences.show_details} />
</ButtonGroup>
<ButtonGroup>
<IconButton id="zoom-to-fit" icon="fa fa-compress" onClick={this.diagram.zoomToFit} title={gettext('Zoom to fit')}
shortcut={this.state.preferences.zoom_to_fit}/>
<IconButton id="zoom-in" icon="fa fa-search-plus" onClick={this.diagram.zoomIn} title={gettext('Zoom in')}
shortcut={this.state.preferences.zoom_in}/>
<IconButton id="zoom-out" icon="fa fa-search-minus" onClick={this.diagram.zoomOut} title={gettext('Zoom out')}
shortcut={this.state.preferences.zoom_out}/>
</ButtonGroup>
<ButtonGroup>
<IconButton id="help" icon="fa fa-question" onClick={this.onHelpClick} title={gettext('Help')} />
</ButtonGroup>
</ToolBar>
<ConnectionBar statusId="btn-conn-status" status={this.state.conn_status} bgcolor={this.props.params.bgcolor}
<Box ref={this.containerRef} height="100%">
<ConnectionBar status={this.state.conn_status} bgcolor={this.props.params.bgcolor}
fgcolor={this.props.params.fgcolor} title={this.props.params.title}/>
<MainToolBar containerRef={this.containerRef} preferences={this.state.preferences} connStatus={this.state.conn_status} params={this.props.params} eventBus={this.eventBus} />
<FloatingNote open={this.state.note_open} onClose={this.onNoteClose}
reference={this.noteRefEle} noteNode={this.state.note_node} appendTo={this.diagramContainerRef.current} rows={8}/>
<div className="diagram-container" ref={this.diagramContainerRef} onDrop={this.onDropNode} onDragOver={e => {e.preventDefault();}}>
anchorEl={this.noteRefEle} noteNode={this.state.note_node} appendTo={this.diagramContainerRef.current} rows={8}/>
<div className={this.props.classes.diagramContainer} data-test="diagram-container" ref={this.diagramContainerRef} onDrop={this.onDropNode} onDragOver={e => {e.preventDefault();}}>
<Loader message={this.state.loading_msg} autoEllipsis={true}/>
<CanvasWidget className="diagram-canvas flex-grow-1" ref={(ele)=>{this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
<CanvasWidget className={this.props.classes.diagramCanvas} ref={(ele)=>{this.canvasEle = ele?.ref?.current;}} engine={this.diagram.getEngine()} />
</div>
</Theme>
</Box>
);
}
}
export default withStyles(styles)(ERDTool);
BodyWidget.propTypes = {
ERDTool.propTypes = {
params:PropTypes.shape({
trans_id: PropTypes.number.isRequired,
sgid: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
@@ -903,9 +925,8 @@ BodyWidget.propTypes = {
fgcolor: PropTypes.string,
gen: PropTypes.bool.isRequired,
}),
getDialog: PropTypes.func.isRequired,
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,
alertify: PropTypes.object.isRequired,
panel: PropTypes.object,
classes: PropTypes.object,
};

View File

@@ -0,0 +1,99 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState, useMemo } from 'react';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import CustomPropTypes from 'sources/custom_prop_types';
import { Box, makeStyles, Popper } from '@material-ui/core';
import { DefaultButton } from '../../../../../../static/js/components/Buttons';
import CheckIcon from '@material-ui/icons/Check';
const useStyles = makeStyles((theme)=>({
root: {
width: '250px',
marginLeft: '8px',
...theme.mixins.panelBorder.all,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
},
note: {
padding: '4px',
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
borderTopLeftRadius: 'inherit',
borderTopRightRadius: 'inherit',
},
header: {
padding: '4px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
...theme.mixins.panelBorder.bottom,
},
textarea: {
width: '100%',
border: 0,
display: 'block',
},
buttons: {
padding: '4px',
...theme.mixins.panelBorder.top,
textAlign: 'right',
},
}));
export default function FloatingNote({open, onClose, anchorEl, rows, noteNode}) {
const [text, setText] = useState('');
const classes = useStyles();
useEffect(()=>{
if(noteNode) {
setText(noteNode.getNote());
}
}, [noteNode]);
const header = useMemo(()=>{
if(noteNode) {
let [schema, name] = noteNode.getSchemaTableName();
return `${name} (${schema})`;
}
return '';
}, [open]);
return (
<Popper
open={open}
anchorEl={anchorEl}
placement="right-start"
>
<Box className={classes.root}>
<Box className={classes.note}>{gettext('Note')}:</Box>
<Box className={classes.header}>{header}</Box>
<textarea className={classes.textarea} autoFocus value={text} rows={rows} onChange={(e)=>setText(e.target.value)}/>
<Box className={classes.buttons}>
<DefaultButton startIcon={<CheckIcon />} onClick={()=>{
let updated = (noteNode.getNote() != text);
noteNode.setNote(text);
if(onClose) onClose(updated);
}}>{gettext('OK')}</DefaultButton>
</Box>
</Box>
</Popper>
);
}
FloatingNote.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
anchorEl: CustomPropTypes.ref,
rows: PropTypes.number,
noteNode: PropTypes.object,
};

View File

@@ -0,0 +1,249 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, {useCallback, useEffect, useState} from 'react';
import { makeStyles } from '@material-ui/styles';
import { Box } from '@material-ui/core';
import { PgButtonGroup, PgIconButton } from '../../../../../../static/js/components/Buttons';
import FolderRoundedIcon from '@material-ui/icons/FolderRounded';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import SaveRoundedIcon from '@material-ui/icons/SaveRounded';
import HelpIcon from '@material-ui/icons/HelpRounded';
import ZoomInIcon from '@material-ui/icons/ZoomIn';
import ZoomOutIcon from '@material-ui/icons/ZoomOut';
import ZoomOutMapIcon from '@material-ui/icons/ZoomOutMap';
import AddBoxIcon from '@material-ui/icons/AddBox';
import EditRoundedIcon from '@material-ui/icons/EditRounded';
import FileCopyRoundedIcon from '@material-ui/icons/FileCopyRounded';
import DeleteIcon from '@material-ui/icons/Delete';
import NoteRoundedIcon from '@material-ui/icons/NoteRounded';
import VisibilityRoundedIcon from '@material-ui/icons/VisibilityRounded';
import VisibilityOffRoundedIcon from '@material-ui/icons/VisibilityOffRounded';
import ImageRoundedIcon from '@material-ui/icons/ImageRounded';
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import PropTypes from 'prop-types';
import { ERD_EVENTS } from '../ERDConstants';
import { MagicIcon, SQLFileIcon } from '../../../../../../static/js/components/ExternalIcon';
import { useModal } from '../../../../../../static/js/helpers/ModalProvider';
const useStyles = makeStyles((theme)=>({
root: {
padding: '2px 4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
backgroundColor: theme.otherVars.editorToolbarBg,
flexWrap: 'wrap',
...theme.mixins.panelBorder.bottom,
},
connectionButton: {
display: 'flex',
width: '450px',
backgroundColor: theme.palette.default.main,
color: theme.palette.default.contrastText,
border: '1px solid ' + theme.palette.default.borderColor,
justifyContent: 'flex-start',
},
}));
export function MainToolBar({preferences, eventBus}) {
const classes = useStyles();
const [buttonsDisabled, setButtonsDisabled] = useState({
'save': true,
'edit-table': true,
'clone-table': true,
'one-to-many': true,
'many-to-many': true,
'show-note': true,
'drop-table': true,
});
const [showDetails, setShowDetails] = useState(true);
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
const saveAsMenuRef = React.useRef(null);
const isDirtyRef = React.useRef(null);
const modal = useModal();
const setDisableButton = useCallback((name, disable=true)=>{
setButtonsDisabled((prev)=>({...prev, [name]: disable}));
}, []);
const onHelpClick=()=>{
let url = url_for('help.static', {'filename': 'erd_tool.html'});
window.open(url, 'pgadmin_help');
};
const confirmDiscard=(callback, checkSaved=false)=>{
if(checkSaved && buttonsDisabled['save']) {
/* No need to check */
callback();
return;
}
modal.confirm(
gettext('Unsaved changes'),
gettext('Are you sure you wish to discard the current changes?'),
function() {
callback();
},
function() {
return true;
}
);
};
useEffect(()=>{
const events = [
[ERD_EVENTS.SINGLE_NODE_SELECTED, (selected)=>{
setDisableButton('edit-table', !selected);
setDisableButton('clone-table', !selected);
setDisableButton('one-to-many', !selected);
setDisableButton('many-to-many', !selected);
setDisableButton('show-note', !selected);
}],
[ERD_EVENTS.ANY_ITEM_SELECTED, (selected)=>{
setDisableButton('drop-table', !selected);
}],
[ERD_EVENTS.DIRTY, (isDirty)=>{
isDirtyRef.current = isDirty;
setDisableButton('save', !isDirty);
}],
];
events.forEach((e)=>{
eventBus.registerListener(e[0], e[1]);
});
return ()=>{
events.forEach((e)=>{
eventBus.deregisterListener(e[0], e[1]);
});
};
}, []);
return (
<>
<Box className={classes.root}>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Load Project')} icon={<FolderRoundedIcon />}
shortcut={preferences.open_project} onClick={()=>{
confirmDiscard(()=>{
eventBus.fireEvent(ERD_EVENTS.LOAD_DIAGRAM);
}, true);
}} />
<PgIconButton title={gettext('Save Project')} icon={<SaveRoundedIcon />}
shortcut={preferences.save_project} disabled={buttonsDisabled['save']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.SAVE_DIAGRAM);
}} />
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-saveas" ref={saveAsMenuRef} onClick={toggleMenu}
/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Generate SQL')} icon={<SQLFileIcon />}
shortcut={preferences.generate_sql}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.SHOW_SQL);
}} />
<PgIconButton title={gettext('Download image')} icon={<ImageRoundedIcon />}
shortcut={preferences.download_image}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
}} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Add Table')} icon={<AddBoxIcon />}
shortcut={preferences.add_table}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ADD_NODE);
}} />
<PgIconButton title={gettext('Edit Table')} icon={<EditRoundedIcon />}
shortcut={preferences.edit_table} disabled={buttonsDisabled['edit-table']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.EDIT_NODE);
}} />
<PgIconButton title={gettext('Clone Table')} icon={<FileCopyRoundedIcon />}
shortcut={preferences.clone_table} disabled={buttonsDisabled['clone-table']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.CLONE_NODE);
}} />
<PgIconButton title={gettext('Drop Table/Relation')} icon={<DeleteIcon />}
shortcut={preferences.drop_table} disabled={buttonsDisabled['drop-table']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.DELETE_NODE);
}} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('One-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>1M</span>}
shortcut={preferences.one_to_many} disabled={buttonsDisabled['one-to-many']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ONE_TO_MANY);
}} />
<PgIconButton title={gettext('Many-to-Many Relation')} icon={<span style={{letterSpacing: '-1px'}}>MM</span>}
shortcut={preferences.many_to_many} disabled={buttonsDisabled['many-to-many']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.MANY_TO_MANY);
}} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Add/Edit Note')} icon={<NoteRoundedIcon />}
shortcut={preferences.add_edit_note} disabled={buttonsDisabled['show-note']}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.SHOW_NOTE);
}} />
<PgIconButton title={gettext('Auto Align')} icon={<MagicIcon />}
shortcut={preferences.auto_align}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.AUTO_DISTRIBUTE);
}} />
<PgIconButton title={gettext('Show Details')} icon={showDetails ? <VisibilityRoundedIcon /> : <VisibilityOffRoundedIcon />}
shortcut={preferences.show_details}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.TOGGLE_DETAILS);
setShowDetails((prev)=>!prev);
}} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Zoom In')} icon={<ZoomInIcon />}
shortcut={preferences.zoom_in}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ZOOM_IN);
}} />
<PgIconButton title={gettext('Zoom to Fit')} icon={<ZoomOutMapIcon />}
shortcut={preferences.zoom_to_fit}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ZOOM_FIT);
}} />
<PgIconButton title={gettext('Zoom Out')} icon={<ZoomOutIcon />}
shortcut={preferences.zoom_out}
onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.ZOOM_OUT);
}}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
</PgButtonGroup>
</Box>
<PgMenu
anchorRef={saveAsMenuRef}
open={openMenuName=='menu-saveas'}
onClose={onMenuClose}
label={gettext('File Menu')}
>
<PgMenuItem onClick={()=>{
eventBus.fireEvent(ERD_EVENTS.SAVE_DIAGRAM, true);
}}>{gettext('Save as')}</PgMenuItem>
</PgMenu>
</>
);
}
MainToolBar.propTypes = {
preferences: PropTypes.object,
eventBus: PropTypes.object,
};

View File

@@ -1,111 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import SchemaView from '../../../../../../static/js/SchemaView';
import Theme from '../../../../../../static/js/Theme';
export default class DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog, alertify, serverInfo) {
this.dialogContainerSelector = dialogContainerSelector;
this.dialogTitle = dialogTitle;
this.alertify = alertify;
this.typeOfDialog = typeOfDialog;
this.serverInfo = serverInfo;
let self = this;
this.hooks = {
onshow: ()=>{
self.createDialog(self.elements.content);
},
onclose: ()=>{
self.cleanupDialog(self.elements.content);
}
};
}
main(title, dialogSchema, okCallback) {
this.set('title', title);
this.dialogSchema = dialogSchema;
this.okCallback = okCallback;
}
build() {
this.elements.dialog.classList.add('erd-dialog');
}
prepare() {
/* If tooltip is mounted after alertify in dom and button is click,
alertify re-positions itself on DOM to come in focus. This makes it lose
the button click events. Making it modal along with following fixes things. */
this.elements.modal.style.maxHeight=0;
this.elements.modal.style.maxWidth='none';
this.elements.modal.style.overflow='visible';
this.elements.dimmer.style.display='none';
}
setup() {
return {
buttons: [],
// Set options for dialog
options: {
title: this.dialogTitle,
//disable both padding and overflow control.
padding: !1,
overflow: !1,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
modal: true,
autoReset: false,
},
};
}
onSaveClick(_isNew, data) {
return new Promise((resolve, reject)=>{
let errorMsg = this.okCallback(data);
if(errorMsg) {
reject(errorMsg);
} else {
this.close();
resolve();
}
});
}
createDialog(container) {
let self = this;
ReactDOM.render(
<Theme>
<SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={this.dialogSchema}
viewHelperProps={{
mode: 'create',
keepCid: true,
serverInfo: this.serverInfo,
}}
onSave={this.onSaveClick.bind(this)}
onClose={()=>self.close()}
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
/>
</Theme>, container);
}
cleanupDialog(container) {
ReactDOM.unmountComponentAtNode(container);
}
}

View File

@@ -8,10 +8,7 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DialogWrapper from './DialogWrapper';
import _ from 'lodash';
class ManyToManySchema extends BaseUISchema {
@@ -50,59 +47,27 @@ class ManyToManySchema extends BaseUISchema {
}
}
export default class ManyToManyDialog {
constructor(pgBrowser) {
this.pgBrowser = pgBrowser;
}
export function getManyToManyDialogSchema(attributes, tableNodesDict) {
let tablesData = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
});
dialogName() {
return 'manytomany_dialog';
}
getUISchema(attributes, tableNodesDict) {
let tablesData = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
});
return new ManyToManySchema({
left_table_uid: tablesData,
left_table_column_attnum: tableNodesDict[attributes.left_table_uid].getColumns().map((col)=>{
return new ManyToManySchema({
left_table_uid: tablesData,
left_table_column_attnum: tableNodesDict[attributes.left_table_uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
}),
right_table_uid: tablesData,
getRefColumns: (uid)=>{
return tableNodesDict[uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
}),
right_table_uid: tablesData,
getRefColumns: (uid)=>{
return tableNodesDict[uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
});
},
}, attributes);
}
createOrGetDialog(title) {
const dialogName = this.dialogName();
if (!Alertify[dialogName]) {
Alertify.dialog(dialogName, () => {
return new DialogWrapper(
`<div class="${dialogName}"></div>`,
title,
null,
Alertify,
);
});
}
return Alertify[dialogName];
}
show(title, attributes, tablesData, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('manytomany_dialog', serverInfo);
dialog(dialogTitle, this.getUISchema(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
}
},
}, attributes);
}

View File

@@ -8,10 +8,7 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DialogWrapper from './DialogWrapper';
import _ from 'lodash';
class OneToManySchema extends BaseUISchema {
@@ -50,60 +47,27 @@ class OneToManySchema extends BaseUISchema {
}
}
export default class OneToManyDialog {
constructor(pgBrowser) {
this.pgBrowser = pgBrowser;
}
export function getOneToManyDialogSchema(attributes, tableNodesDict) {
let tablesData = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
});
dialogName() {
return 'onetomany_dialog';
}
getUISchema(attributes, tableNodesDict) {
let tablesData = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
tablesData.push({value: uid, label: `(${schema}) ${name}`, image: 'icon-table'});
});
return new OneToManySchema({
local_table_uid: tablesData,
local_column_attnum: tableNodesDict[attributes.local_table_uid].getColumns().map((col)=>{
return new OneToManySchema({
local_table_uid: tablesData,
local_column_attnum: tableNodesDict[attributes.local_table_uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
}),
referenced_table_uid: tablesData,
getRefColumns: (uid)=>{
return tableNodesDict[uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
}),
referenced_table_uid: tablesData,
getRefColumns: (uid)=>{
return tableNodesDict[uid].getColumns().map((col)=>{
return {
value: col.attnum, label: col.name, 'image': 'icon-column',
};
});
},
}, attributes);
}
createOrGetDialog(title, sVersion) {
const dialogName = this.dialogName();
if (!Alertify[dialogName]) {
Alertify.dialog(dialogName, () => {
return new DialogWrapper(
`<div class="${dialogName}"></div>`,
title,
null,
Alertify,
sVersion
);
});
}
return Alertify[dialogName];
}
show(title, attributes, tablesData, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('onetomany_dialog', serverInfo);
dialog(dialogTitle, this.getUISchema(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
}
},
}, attributes);
}

View File

@@ -7,12 +7,9 @@
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
import _ from 'lodash';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DialogWrapper from './DialogWrapper';
import TableSchema, { ConstraintsSchema } from '../../../../../../browser/server_groups/servers/databases/schemas/tables/static/js/table.ui';
import ColumnSchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui';
import ForeignKeySchema from '../../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui';
@@ -26,106 +23,73 @@ class EmptySchema extends BaseUISchema {
}
}
export default class TableDialog {
constructor(pgBrowser) {
this.pgBrowser = pgBrowser;
}
export function getTableDialogSchema(attributes, isNew, tableNodesDict, colTypes, schemas) {
let treeNodeInfo = undefined;
dialogName() {
return 'table_dialog';
}
let columnSchema = new ColumnSchema(
()=>{/*This is intentional (SonarQube)*/},
treeNodeInfo,
()=>colTypes,
()=>[],
()=>[],
true,
);
getUISchema(attributes, isNew, tableNodesDict, colTypes, schemas) {
let treeNodeInfo = undefined;
let columnSchema = new ColumnSchema(
()=>{/*This is intentional (SonarQube)*/},
treeNodeInfo,
()=>colTypes,
()=>[],
()=>[],
true,
);
return new TableSchema(
{
relowner: [],
schema: schemas.map((schema)=>{
return {
'value': schema['name'],
'image': 'icon-schema',
'label': schema['name'],
};
}),
spcname: [],
coll_inherits: [],
typname: [],
like_relation: [],
},
treeNodeInfo,
{
columns: ()=>columnSchema,
vacuum_settings: ()=>new EmptySchema(),
constraints: ()=>new ConstraintsSchema(
treeNodeInfo,
()=>new ForeignKeySchema({
local_column: [],
references: ()=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
}
},
treeNodeInfo,
(params)=>{
if(params.tid) {
return tableNodesDict[params.tid].getColumns().map((col)=>{
return {
value: col.name, label: col.name, 'image': 'icon-column',
};
});
}
}, {autoindex: false}, true),
()=>new EmptySchema(),
{spcname: []},
true
),
},
()=>new EmptySchema(),
()=>[],
()=>[],
()=>[],
()=>[],
isNew ? {
schema: schemas[0]?.name,
} : attributes,
true
);
}
createOrGetDialog(type, sVersion) {
const dialogName = this.dialogName();
if (!Alertify[dialogName]) {
Alertify.dialog(dialogName, () => {
return new DialogWrapper(
`<div class="${dialogName}"></div>`,
null,
type,
Alertify,
sVersion
);
});
}
return Alertify[dialogName];
}
show(title, attributes, isNew, tableNodesDict, colTypes, schemas, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('table_dialog', serverInfo);
dialog(dialogTitle, this.getUISchema(attributes, isNew, tableNodesDict, colTypes, schemas, serverInfo), callback).resizeTo(this.pgBrowser.stdW.lg, this.pgBrowser.stdH.md);
}
return new TableSchema(
{
relowner: [],
schema: schemas.map((schema)=>{
return {
'value': schema['name'],
'image': 'icon-schema',
'label': schema['name'],
};
}),
spcname: [],
coll_inherits: [],
typname: [],
like_relation: [],
},
treeNodeInfo,
{
columns: ()=>columnSchema,
vacuum_settings: ()=>new EmptySchema(),
constraints: ()=>new ConstraintsSchema(
treeNodeInfo,
()=>new ForeignKeySchema({
local_column: [],
references: ()=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
}
},
treeNodeInfo,
(params)=>{
if(params.tid) {
return tableNodesDict[params.tid].getColumns().map((col)=>{
return {
value: col.name, label: col.name, 'image': 'icon-column',
};
});
}
}, {autoindex: false}, true),
()=>new EmptySchema(),
{spcname: []},
true
),
},
()=>new EmptySchema(),
()=>[],
()=>[],
()=>[],
()=>[],
isNew ? {
schema: schemas[0]?.name,
} : attributes,
true
);
}

View File

@@ -1,25 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import TableDialog from './TableDialog';
import OneToManyDialog from './OneToManyDialog';
import ManyToManyDialog from './ManyToManyDialog';
import pgBrowser from 'top/browser/static/js/browser';
import 'sources/backgrid.pgadmin';
import 'sources/backform.pgadmin';
export default function getDialog(dialogName) {
if(dialogName === 'table_dialog') {
return new TableDialog(pgBrowser);
} else if(dialogName === 'onetomany_dialog') {
return new OneToManyDialog(pgBrowser);
} else if(dialogName === 'manytomany_dialog') {
return new ManyToManyDialog(pgBrowser);
}
}

View File

@@ -0,0 +1,95 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {getTableDialogSchema} from './TableDialog';
import {getOneToManyDialogSchema} from './OneToManyDialog';
import {getManyToManyDialogSchema} from './ManyToManyDialog';
import pgAdmin from 'sources/pgadmin';
import SchemaView from '../../../../../../static/js/SchemaView';
import React from 'react';
export default class ERDDialogs {
constructor(modalProvider) {
this.modal = modalProvider;
}
onSaveClick(_isNew, data, closeModal, okCallback) {
return new Promise((resolve, reject)=>{
let errorMsg = okCallback(data);
if(errorMsg) {
reject(errorMsg);
} else {
closeModal();
resolve();
}
});
}
showTableDialog(params) {
let schema = getTableDialogSchema(
params.attributes, params.isNew, params.tableNodes,
params.colTypes, params.schemas);
this.modal.showModal(params.title, (closeModal)=>{
return (
<SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={schema}
viewHelperProps={{
mode: 'create',
keepCid: true,
serverInfo: params.serverInfo,
}}
onSave={(...args)=>this.onSaveClick(...args, closeModal, params.callback)}
onClose={closeModal}
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
/>
);
}, {
isResizeable: true,
dialogWidth: pgAdmin.Browser?.stdW?.lg, dialogHeight: pgAdmin.Browser?.stdH?.md,
});
}
showRelationDialog(dialogName, params) {
let schema;
if(dialogName === 'onetomany_dialog') {
schema = getOneToManyDialogSchema(params.attributes, params.tableNodes);
} else if(dialogName === 'manytomany_dialog') {
schema = getManyToManyDialogSchema(params.attributes, params.tableNodes);
}
this.modal.showModal(params.title, (closeModal)=>{
return (
<SchemaView
formType={'dialog'}
getInitData={()=>Promise.resolve({})}
schema={schema}
viewHelperProps={{
mode: 'create',
keepCid: true,
serverInfo: params.serverInfo,
}}
onSave={(...args)=>this.onSaveClick(...args, closeModal, params.callback)}
onClose={closeModal}
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
/>
);
}, {
isResizeable: true,
dialogWidth: pgAdmin.Browser?.stdW?.lg, dialogHeight: pgAdmin.Browser?.stdH?.md,
});
}
}

View File

@@ -1,57 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import BodyWidget from './ui_components/BodyWidget';
import getDialog from './dialogs';
import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import ModalProvider from '../../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../../static/js/Theme';
export default class ERDTool {
constructor(container, params) {
this.container = document.querySelector(container);
this.params = params;
}
getPreferencesForModule() {
/*This is intentional (SonarQube)*/
}
render() {
/* Mount the React ERD tool to the container */
let panel = null;
_.each(pgWindow.pgAdmin.Browser.docker.findPanels('frm_erdtool'), function(p) {
if (p.isVisible()) {
panel = p;
}
});
ReactDOM.render(
<Theme>
<ModalProvider>
<BodyWidget
params={this.params}
getDialog={getDialog}
pgWindow={pgWindow}
pgAdmin={pgAdmin}
panel={panel}
alertify={Alertify} />
</ModalProvider>
</Theme>,
this.container
);
}
}

View File

@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import React from 'react';
import React, { forwardRef } from 'react';
import {
RightAngleLinkModel,
RightAngleLinkWidget,
@@ -19,6 +19,8 @@ import {
import {Point} from '@projectstorm/geometry';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
export const OneToManyModel = {
local_table_uid: undefined,
@@ -74,20 +76,42 @@ export class OneToManyLinkModel extends RightAngleLinkModel {
}
}
const useStyles = makeStyles((theme)=>({
svgLink: {
stroke: theme.palette.text.primary,
},
'@keyframes svgLinkSelected': {
'from': { strokeDashoffset: 24},
'to': { strokeDashoffset: 0 }
},
svgLinkSelected: {
strokeDasharray: '10, 2',
animation: '$svgLinkSelected 1s linear infinite'
},
svgLinkCircle: {
fill: theme.palette.text.primary,
},
svgLinkPath: {
pointerEvents: 'all',
cursor: 'move',
}
}));
const CustomLinkEndWidget = props => {
const { point, rotation, tx, ty, type } = props;
const classes = useStyles();
const svgForType = (itype) => {
if(itype == 'many') {
return (
<>
<circle className="svg-link-ele svg-otom-circle" cx="0" cy="16" r={props.width*1.75} strokeWidth={props.width} />
<polyline className="svg-link-ele" points="-8,0 0,15 0,0 0,30 0,15 8,0" fill="none" strokeWidth={props.width} />
<circle className={clsx(classes.svgLink, classes.svgLinkCircle)} cx="0" cy="16" r={props.width*1.75} strokeWidth={props.width} />
<polyline className={classes.svgLink} points="-8,0 0,15 0,0 0,30 0,15 8,0" fill="none" strokeWidth={props.width} />
</>
);
} else if (itype == 'one') {
return (
<polyline className="svg-link-ele" points="-8,15 0,15 0,0 0,30 0,15 8,15" fill="none" strokeWidth={props.width} />
<polyline className={classes.svgLink} points="-8,15 0,15 0,0 0,30 0,15 8,15" fill="none" strokeWidth={props.width} />
);
}
};
@@ -270,6 +294,29 @@ export class OneToManyLinkWidget extends RightAngleLinkWidget {
}
}
const LinkSegment = forwardRef(({model, selected, path, ...props}, ref)=>{
const classes = useStyles();
return (
<path
ref={ref}
className={clsx(classes.svgLink, classes.svgLinkPath, (selected ? classes.svgLinkSelected : ''))}
stroke={model.getOptions().color}
strokeWidth={model.getOptions().width}
selected={selected}
d={path}
{...props}
>
</path>
);
});
LinkSegment.displayName = 'LinkSegment';
LinkSegment.propTypes = {
model: PropTypes.object,
selected: PropTypes.bool,
path: PropTypes.any,
};
export class OneToManyLinkFactory extends DefaultLinkFactory {
constructor() {
super('onetomany');
@@ -280,19 +327,10 @@ export class OneToManyLinkFactory extends DefaultLinkFactory {
}
generateReactWidget(event) {
return <OneToManyLinkWidget color='#fff' width={1} smooth={true} link={event.model} diagramEngine={this.engine} factory={this} />;
return <OneToManyLinkWidget width={1} smooth={true} link={event.model} diagramEngine={this.engine} factory={this} />;
}
generateLinkSegment(model, selected, path) {
return (
<path
className={'svg-link-ele path ' + (selected ? 'selected' : '')}
stroke={model.getOptions().color}
selected={selected}
strokeWidth={model.getOptions().width}
d={path}
>
</path>
);
return <LinkSegment model={model} selected={selected} path={path} />;
}
}

View File

@@ -11,7 +11,6 @@ import React from 'react';
import { DefaultNodeModel, DiagramEngine, PortWidget } from '@projectstorm/react-diagrams';
import { AbstractReactFactory } from '@projectstorm/react-canvas-core';
import _ from 'lodash';
import { IconButton, DetailsToggleButton } from '../ui_components/ToolBar';
import SchemaIcon from 'top/browser/server_groups/servers/databases/schemas/static/img/schema.svg';
import TableIcon from 'top/browser/server_groups/servers/databases/schemas/tables/static/img/table.svg';
import PrimaryKeyIcon from 'top/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/img/primary_key.svg';
@@ -20,6 +19,14 @@ import ColumnIcon from 'top/browser/server_groups/servers/databases/schemas/tabl
import UniqueKeyIcon from 'top/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/img/unique_constraint.svg';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import { PgIconButton } from '../../../../../../static/js/components/Buttons';
import NoteRoundedIcon from '@material-ui/icons/NoteRounded';
import VisibilityRoundedIcon from '@material-ui/icons/VisibilityRounded';
import VisibilityOffRoundedIcon from '@material-ui/icons/VisibilityOffRounded';
import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import { Box } from '@material-ui/core';
const TYPE = 'table';
@@ -132,7 +139,7 @@ export class TableNodeModel extends DefaultNodeModel {
function RowIcon({icon}) {
return (
<div className="table-icon">
<div style={{padding: '0rem 0.125rem'}}>
<img src={icon} crossOrigin="anonymous"/>
</div>
);
@@ -142,8 +149,48 @@ RowIcon.propTypes = {
icon: PropTypes.any.isRequired,
};
const styles = (theme)=>({
tableNode: {
backgroundColor: theme.palette.background.default,
...theme.mixins.panelBorder.all,
borderRadius: theme.shape.borderRadius,
position: 'relative',
width: '175px',
fontSize: '0.8em',
'& div:last-child': {
borderBottomLeftRadius: 'inherit',
borderBottomRightRadius: 'inherit',
}
},
tableNodeSelected: {
borderColor: theme.palette.primary.main,
},
tableSection: {
...theme.mixins.panelBorder.bottom,
padding: '0.125rem 0.25rem',
display: 'flex',
},
tableToolbar: {
background: theme.otherVars.editorToolbarBg,
borderTopLeftRadius: 'inherit',
borderTopRightRadius: 'inherit',
},
tableNameText: {
fontWeight: 'bold',
wordBreak: 'break-all',
margin: 'auto 0',
},
error: {
color: theme.palette.error.main,
},
noteBtn: {
marginLeft: 'auto',
backgroundColor: theme.palette.warning.main,
color: theme.palette.warning.contrastText,
}
});
export class TableNodeWidget extends React.Component {
class TableNodeWidgetRaw extends React.Component {
constructor(props) {
super(props);
@@ -179,17 +226,21 @@ export class TableNodeWidget extends React.Component {
if(col.attlen) {
cltype += '('+ col.attlen + (col.attprecision ? ',' + col.attprecision : '') +')';
}
const {classes} = this.props;
return (
<div className='d-flex col-row' key={col.attnum}>
<div className='d-flex col-row-data'>
<div className={classes.tableSection} key={col.attnum} data-test="column-row">
<Box display="flex" width="100%" style={{wordBreak: 'break-all'}}>
<RowIcon icon={icon} />
<div className="my-auto">
<span className='col-name'>{col.name}</span>&nbsp;
<Box margin="auto 0">
<span data-test="column-name">{col.name}</span>&nbsp;
{this.state.show_details &&
<span className='col-datatype'>{cltype}</span>}
</div>
</div>
<div className="ml-auto col-row-port">{this.generatePort(port)}</div>
<span data-test="column-type">{cltype}</span>}
</Box>
</Box>
<Box marginLeft="auto" padding="0" minHeight="0" display="flex" alignItems="center">
{this.generatePort(port)}
</Box>
</div>
);
}
@@ -219,44 +270,50 @@ export class TableNodeWidget extends React.Component {
(tableData.unique_constraint||[]).forEach((uk)=>{
localUkCols.push(...uk.columns.map((c)=>c.column));
});
const {classes} = this.props;
return (
<div className={'table-node ' + (this.props.node.isSelected() ? 'selected': '') } onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}}>
<div className="table-toolbar">
<DetailsToggleButton className='btn-xs' showDetails={this.state.show_details}
onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}}
disabled={tableMetaData.is_promise} />
<div className={clsx(classes.tableNode, (this.props.node.isSelected() ? classes.tableNodeSelected: ''))} onDoubleClick={()=>{this.props.node.fireEvent({}, 'editTable');}}>
<div className={clsx(classes.tableSection, classes.tableToolbar)}>
<PgIconButton size="xs" title={gettext('Show Details')} icon={this.state.show_details ? <VisibilityRoundedIcon /> : <VisibilityOffRoundedIcon />}
onClick={this.toggleShowDetails} onDoubleClick={(e)=>{e.stopPropagation();}} />
{this.props.node.getNote() &&
<IconButton icon="far fa-sticky-note" className="btn-xs btn-warning ml-auto" onClick={()=>{
this.props.node.fireEvent({}, 'showNote');
}} title="Check note"/>}
<PgIconButton size="xs" className={classes.noteBtn}
title={gettext('Check Note')} icon={<NoteRoundedIcon />}
onClick={()=>{
this.props.node.fireEvent({}, 'showNote');
}}
/>}
</div>
{tableMetaData.is_promise && <>
<div className="d-flex table-name-data">
{!tableMetaData.data_failed && <div className="table-name my-auto">{gettext('Fetching...')}</div>}
{tableMetaData.data_failed && <div className="table-name my-auto fetch-error">{gettext('Failed to get data. Please delete this table.')}</div>}
<div className={classes.tableSection}>
{!tableMetaData.data_failed && <div className={classes.tableNameText}>{gettext('Fetching...')}</div>}
{tableMetaData.data_failed && <div className={clsx(classes.tableNameText, classes.error)}>{gettext('Failed to get data. Please delete this table.')}</div>}
</div>
</>}
{!tableMetaData.is_promise && <>
<div className="d-flex table-schema-data">
<div className={classes.tableSection}>
<RowIcon icon={SchemaIcon}/>
<div className="table-schema my-auto">{tableData.schema}</div>
<div className={classes.tableNameText} data-test="schema-name">{tableData.schema}</div>
</div>
<div className="d-flex table-name-data">
<div className={classes.tableSection}>
<RowIcon icon={TableIcon} />
<div className="table-name my-auto">{tableData.name}</div>
<div className={classes.tableNameText} data-test="table-name">{tableData.name}</div>
</div>
<div className="table-cols">
{tableData.columns.length > 0 && <div>
{_.map(tableData.columns, (col)=>this.generateColumn(col, localFkCols, localUkCols))}
</div>
</div>}
</>}
</div>
);
}
}
TableNodeWidget.propTypes = {
export const TableNodeWidget = withStyles(styles)(TableNodeWidgetRaw);
TableNodeWidgetRaw.propTypes = {
node: PropTypes.instanceOf(TableNodeModel),
engine: PropTypes.instanceOf(DiagramEngine),
classes: PropTypes.object,
};
export class TableNodeFactory extends AbstractReactFactory {

View File

@@ -1,57 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
export const STATUS = {
CONNECTED: 1,
DISCONNECTED: 2,
CONNECTING: 3,
FAILED: 4,
};
/* The connection bar component */
export default function ConnectionBar({statusId, status, bgcolor, fgcolor, title}) {
return (
<div className="connection_status_wrapper d-flex">
<div id={statusId}
role="status"
className="connection_status d-flex justify-content-center align-items-center" data-container="body"
data-toggle="popover" data-placement="bottom"
data-content=""
data-panel-visible="visible"
tabIndex="0">
<span className={'pg-font-icon d-flex m-auto '
+ (status == STATUS.CONNECTED ? 'icon-connected' : '')
+ (status == (STATUS.DISCONNECTED || STATUS.FAILED) ? 'icon-disconnected ' : '')
+ (status == STATUS.CONNECTING ? 'obtaining-conn' : '')}
aria-hidden="true" title="" role="img">
</span>
</div>
<div className="connection-info btn-group" role="group" aria-label="">
<div className="editor-title"
style={{backgroundColor: bgcolor, color: fgcolor}}>
{status == STATUS.CONNECTING ? '(' + gettext('Obtaining connection...') + ') ' : ''}
{status == STATUS.FAILED ? '(' + gettext('Connection failed') + ') ' : ''}
{title}
</div>
</div>
</div>
);
}
ConnectionBar.propTypes = {
statusId: PropTypes.string.isRequired,
status: PropTypes.oneOf(Object.values(STATUS)).isRequired,
bgcolor: PropTypes.string,
fgcolor: PropTypes.string,
title: PropTypes.string.isRequired,
};

View File

@@ -1,70 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useState } from 'react';
import Tippy from '@tippyjs/react';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import CustomPropTypes from 'sources/custom_prop_types';
/* The note component of ERD. It uses tippy to create the floating note */
export default function FloatingNote({open, onClose, reference, rows, noteNode, ...tippyProps}) {
const textRef = React.useRef(null);
const [text, setText] = useState('');
const [header, setHeader] = useState('');
useEffect(()=>{
if(noteNode) {
setText(noteNode.getNote());
let [schema, name] = noteNode.getSchemaTableName();
setHeader(`${name} (${schema})`);
}
if(open) {
textRef?.current.focus();
textRef?.current.dispatchEvent(new KeyboardEvent('keypress'));
}
}, [noteNode, open]);
return (
<Tippy render={(attrs)=>(
<div className="floating-note" {...attrs}>
<div className="note-header">{gettext('Note')}:</div>
<div className="note-body">
<div className="p-1">{header}</div>
<textarea ref={textRef} className="pg-textarea" value={text} rows={rows} onChange={(e)=>setText(e.target.value)}></textarea>
<div className="pg_buttons">
<button className="btn btn-primary long_text_editor pg-alertify-button" data-label="OK"
onClick={()=>{
let updated = (noteNode.getNote() != text);
noteNode.setNote(text);
if(onClose) onClose(updated);
}}>
<span className="fa fa-check pg-alertify-button"></span>&nbsp;{gettext('OK')}
</button>
</div>
</div>
</div>
)}
visible={open}
interactive={true}
animation={false}
reference={reference}
placement='auto-end'
{...tippyProps}
/>
);
}
FloatingNote.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
reference: CustomPropTypes.ref,
rows: PropTypes.number,
noteNode: PropTypes.object,
};

View File

@@ -1,34 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import PropTypes from 'prop-types';
/* The loader/spinner component */
export default function Loader({message, autoEllipsis=false}) {
if(message || message == '') {
return (
<div className="pg-sp-container">
<div className="pg-sp-content">
<div className="row">
<div className="col-12 pg-sp-icon"></div>
</div>
<div className="row"><div className="col-12 pg-sp-text">{message}{autoEllipsis ? '...':''}</div></div>
</div>
</div>
);
} else {
return null;
}
}
Loader.propTypes = {
message: PropTypes.string,
autoEllipsis: PropTypes.bool,
};

View File

@@ -1,146 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { forwardRef } from 'react';
import Tippy from '@tippyjs/react';
import {isMac} from 'sources/keyboard_shortcuts';
import gettext from 'sources/gettext';
import PropTypes from 'prop-types';
import CustomPropTypes from 'sources/custom_prop_types';
/* The base icon button.
React does not pass ref prop to child component hierarchy.
Use forwardRef for the same
*/
const BaseIconButton = forwardRef((props, ref)=>{
const {icon, text, className, ...otherProps} = props;
return(
<button ref={ref} className={className} {...otherProps}>
{icon && <span className={`${icon} sql-icon-lg`} aria-hidden="true" role="img"></span>}
{text && <span className="text-icon">{text}</span>}
</button>
);
});
BaseIconButton.displayName = 'BaseIconButton';
BaseIconButton.propTypes = {
icon: PropTypes.string,
text: PropTypes.string,
className: PropTypes.string,
ref: CustomPropTypes.ref,
};
/* The tooltip content to show shortcut details */
export function Shortcut({shortcut}) {
let keys = [];
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
shortcut.control && keys.push('Ctrl');
shortcut.shift && keys.push('Shift');
keys.push(shortcut.key.char.toUpperCase());
return (
<div style={{justifyContent: 'center', marginTop: '0.125rem'}} className="d-flex">
{keys.map((key, i)=>{
return <div key={i} className="shortcut-key">{key}</div>;
})}
</div>
);
}
const shortcutPropType = PropTypes.shape({
alt: PropTypes.bool,
control: PropTypes.bool,
shift: PropTypes.bool,
key: PropTypes.shape({
char: PropTypes.string,
}),
});
Shortcut.propTypes = {
shortcut: shortcutPropType,
};
/* The icon button component which can have a tooltip based on props.
React does not pass ref prop to child component hierarchy.
Use forwardRef for the same
*/
export const IconButton = forwardRef((props, ref) => {
const {title, shortcut, className, ...otherProps} = props;
if (title) {
return (
<Tippy content={
<>
{<div style={{textAlign: 'center'}}>{title}</div>}
{shortcut && <Shortcut shortcut={shortcut} />}
</>
}>
<BaseIconButton ref={ref} className={'btn btn-sm btn-primary-icon ' + (className || '')} {...otherProps}/>
</Tippy>
);
} else {
return <BaseIconButton ref={ref} className='btn btn-sm btn-primary-icon' {...otherProps}/>;
}
});
IconButton.displayName = 'IconButton';
IconButton.propTypes = {
title: PropTypes.string,
shortcut: shortcutPropType,
className: PropTypes.string,
};
/* Toggle button, icon changes based on value */
export function DetailsToggleButton({showDetails, ...props}) {
return (
<IconButton
icon={showDetails ? 'far fa-eye' : 'fas fa-low-vision'}
title={showDetails ? gettext('Show fewer details') : gettext('Show more details') }
{...props} />
);
}
DetailsToggleButton.propTypes = {
showDetails: PropTypes.bool,
};
/* Button group container */
export function ButtonGroup({className, children}) {
return (
<div className={'btn-group mr-1 ' + (className ? className : '')} role="group" aria-label="save group">
{children}
</div>
);
}
ButtonGroup.propTypes = {
className: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
};
/* Toolbar container */
export default function ToolBar({id, children}) {
return (
<div id={id} className="editor-toolbar d-flex" role="toolbar" aria-label="">
{children}
</div>
);
}
ToolBar.propTypes = {
id: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
};

View File

@@ -1,35 +0,0 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'sources/pgadmin', 'pgadmin.tools.erd/erd_tool', 'pgadmin.browser',
'pgadmin.browser.server.privilege', 'pgadmin.node.database', 'pgadmin.node.primary_key',
'pgadmin.node.foreign_key', 'pgadmin.browser.datamodel', 'pgadmin.tools.file_manager',
], function(
pgAdmin, ERDToolModule
) {
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
var ERDTool = ERDToolModule.default;
/* Return back, this has been called more than once */
if (pgTools.ERDToolHook)
return pgTools.ERDToolHook;
pgTools.ERDToolHook = {
load: function(params) {
/* Create the ERD Tool object and render it */
let erdObj = new ERDTool('#erd-tool-container', params);
erdObj.render();
},
};
return pgTools.ERDToolHook;
});

View File

@@ -6,18 +6,16 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import $ from 'jquery';
import _ from 'underscore';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import * as csrfToken from 'sources/csrf';
import {initialize} from './erd_module';
var wcDocker = window.wcDocker;
import 'pgadmin.tools.file_manager';
import ERDModule from './ERDModule';
let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser, wcDocker);
if(!pgAdmin.Tools) {
pgAdmin.Tools = {};
}
pgAdmin.Tools.ERD = ERDModule.getInstance(pgAdmin, pgBrowser);
module.exports = {
pgBrowser: pgBrowserOut,
ERD: pgAdmin.Tools.ERD,
};

View File

@@ -1,265 +0,0 @@
.shortcut-key {
padding: 0 0.25rem;
border: 1px solid $border-color;
margin-right: 0.125rem;
border-radius: $btn-border-radius;
}
.connection_status_wrapper {
width: 100%;
border-top: $panel-border;
border-bottom: $panel-border;
}
.connection_status .obtaining-conn {
background-image: $loader-icon-small !important;
background-position: center center;
background-repeat: no-repeat;
&:before {
content:'';
}
min-width: 50%;
min-height: 100%;
}
.connection_status {
background-color: $sql-title-bg;
color: $sql-title-fg;
border-right: $border-width solid $border-color;
padding: 0px 8px;
}
.editor-title {
padding: $sql-title-padding;
background: $sql-title-bg;
color: $sql-title-fg;
}
.connection-info {
background: $sql-title-bg;
color: $sql-title-fg;
width:100%;
display: inherit;
}
.connection-data {
display: inherit;
cursor: pointer;
width: auto;
}
#erd-tool-container {
width: 100%;
height: 100%;
.file-input-hidden {
height: 0;
width: 0;
visibility: hidden;
}
.text-icon {
font-weight: bold;
}
.erd-hint-bar {
background: $sql-gutters-bg;
padding: 0.25rem 0.5rem;
}
.diagram-container {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
}
.floating-note {
width: 250px;
border: $panel-border;
border-radius: $panel-border-radius;
box-shadow: $dialog-box-shadow;
background-color: $alert-dialog-body-bg !important;
color: $color-fg !important;
.note-header {
padding: 0.25rem 0.5rem;
background-color: $alert-header-bg;
font-size: $font-size-base;
font-weight: bold;
color: $alert-header-fg;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
border-radius: 0rem;
border-top-left-radius: $panel-border-radius;
border-top-right-radius: $panel-border-radius;
border-bottom: none;
margin: -$alertify-borderremove-margin; //-24px is default by alertify
margin-bottom: 0px;
}
.note-body {
word-break: break-all;
& textarea {
width: 100%;
border: none;
border-bottom: $border-width solid $erd-node-border-color;
border-top: $border-width solid $erd-node-border-color;
}
& .pg_buttons {
padding: 0.25rem;
}
}
}
.html2canvas-reset {
background-image: none !important;
overflow: auto !important;
}
.diagram-canvas{
width: 100%;
height: 100%;
color: $color-fg;
font-family: sans-serif;
background-color: $erd-canvas-bg;
background-image: $erd-bg-grid;
cursor: unset;
.table-node {
background-color: $input-bg;
border: $border-width solid $erd-node-border-color;
border-radius: $input-border-radius;
position: relative;
width: 175px;
font-size: 0.8em;
.table-icon {
padding: 0rem 0.125rem;
}
&.selected {
border-color: $input-focus-border-color;
box-shadow: $input-btn-focus-box-shadow;
}
.table-toolbar {
background: $editor-toolbar-bg;
border-bottom: $border-width solid $erd-node-border-color;
padding: 0.125rem;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
display: flex;
.btn {
&:not(:first-of-type) {
margin-left: 0.125rem;
}
}
}
.table-schema-data {
border-bottom: $border-width solid $erd-node-border-color;
padding: $erd-row-padding;
& .table-schema {
font-weight: bold;
word-break: break-all;
}
}
.table-name-data {
border-bottom: $border-width*2 solid $erd-node-border-color;
padding: $erd-row-padding;
& .table-name {
font-weight: bold;
word-break: break-all;
}
& .fetch-error {
color: $color-danger;
}
}
.table-cols {
.col-row {
border-bottom: $border-width solid $erd-node-border-color;
.col-row-data {
padding: $erd-row-padding;
width: 100%;
word-break: break-all;
}
.col-row-port {
padding: 0;
min-height: 0;
}
}
}
}
.svg-link-ele {
stroke: $erd-link-color;
}
.svg-link-ele.path {
pointer-events: all;
cursor: move;
}
@keyframes svg-link-ele-selected {
from { stroke-dashoffset: 24; } to { stroke-dashoffset: 0; }
}
.svg-link-ele.selected {
stroke: $erd-link-selected-color;
stroke-dasharray: 10, 2;
animation: svg-link-ele-selected 1s linear infinite;
}
.svg-link-ele.svg-otom-circle {
fill: $erd-link-color;
}
.custom-node-color{
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
transform: translate(-50%, -50%);
border-radius: 10px;
}
.circle-port{
width: 12px;
height: 12px;
margin: 2px;
border-radius: 4px;
background: darkgray;
cursor: pointer;
}
.circle-port:hover{
background: mediumpurple;
}
.port {
display: inline-block;
margin: auto;
}
}
}
.alertify {
.erd-dialog {
.ajs-body .ajs-content {
bottom: 0!important;
}
.ajs-footer {
display: none;
}
}
}

View File

@@ -7,15 +7,23 @@
{% block body %}
<style>
body {padding: 0px;}
{% if is_desktop_mode and is_linux %}
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
.alertify-notifier{-webkit-transform: none;}
.alertify-notifier .ajs-message{-webkit-transform: none;}
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
{% endif %}
#erd-tool-container {
display: flex;
flex-direction: column;
height: 100%;
}
#erd-tool-container:not(:empty) + .pg-sp-container {
display: none;
}
</style>
<div id="erd-tool-container" class="d-flex flex-column">
<div class="pg-sp-container">
<div class="pg-sp-content">
<div class="row">
<div class="col-12 pg-sp-icon"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block init_script %}
@@ -23,25 +31,9 @@
require(
['sources/generated/browser_nodes', 'sources/generated/codemirror'],
function() {
require(['sources/generated/erd_tool'], function(erdToolHook) {
var erdToolHook = erdToolHook || pgAdmin.Tools.ERDToolHook;
erdToolHook.load({{ params|safe }});
if(window.opener) {
$(window).on('unload', function(ev) {
$.ajax({
method: 'DELETE',
url: '{{close_url}}'
});
});
} else {
$(window).on('beforeunload', function(ev) {
$.ajax({
method: 'DELETE',
url: '{{close_url}}'
});
});
}
require(['sources/generated/erd_tool'], function(module) {
window.pgAdmin.Tools.ERD.loadComponent(
document.getElementById('erd-tool-container'), {{ params|safe }});
}, function() {
console.log(arguments);
});

View File

@@ -16,7 +16,6 @@ import $ from 'jquery';
import url_for from 'sources/url_for';
import _ from 'lodash';
import alertify from 'pgadmin.alertifyjs';
var wcDocker = window.wcDocker;
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'pgadmin.browser';
@@ -29,6 +28,7 @@ import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme';
const wcDocker = window.wcDocker;
export function setPanelTitle(queryToolPanel, panelTitle) {
queryToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');

View File

@@ -13,7 +13,7 @@ import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
import url_for from 'sources/url_for';
import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/helpers/Layout';
import ConfirmSaveContent from '../dialogs/ConfirmSaveContent';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import gettext from 'sources/gettext';
import OrigCodeMirror from 'bundled_codemirror';
import Notifier from '../../../../../../static/js/helpers/Notifier';

View File

@@ -25,7 +25,7 @@ import { QuerySources } from './QueryHistory';
import { getBrowser } from '../../../../../../static/js/utils';
import CopyData from '../QueryToolDataGrid/CopyData';
import moment from 'moment';
import ConfirmSaveContent from '../dialogs/ConfirmSaveContent';
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
import { makeStyles } from '@material-ui/styles';
import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanelMessage';
import { GraphVisualiser } from './GraphVisualiser';

View File

@@ -15,13 +15,6 @@
#sqleditor-container:not(:empty) + .pg-sp-container {
display: none;
}
{% if is_desktop_mode and is_linux %}
.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
.alertify-notifier{-webkit-transform: none;}
.alertify-notifier .ajs-message{-webkit-transform: none;}
.alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
.sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
{% endif %}
</style>
<div id="sqleditor-container">
<div class="pg-sp-container">

View File

@@ -6,7 +6,7 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {KeyboardShortcutAction} from 'pgadmin.tools.erd/erd_tool/ui_components/BodyWidget';
import {KeyboardShortcutAction} from 'pgadmin.tools.erd/erd_tool/components/ERDTool';
describe('KeyboardShortcutAction', ()=>{
let keyAction = null;

View File

@@ -5,7 +5,7 @@ import '../helper/enzyme.helper';
import { DefaultNodeModel } from '@projectstorm/react-diagrams';
import {TableNodeModel, TableNodeWidget} from 'pgadmin.tools.erd/erd_tool/nodes/TableNode';
import { IconButton, DetailsToggleButton } from 'pgadmin.tools.erd/erd_tool/ui_components/ToolBar';
import Theme from '../../../pgadmin/static/js/Theme';
describe('ERD TableNodeModel', ()=>{
@@ -211,63 +211,45 @@ describe('ERD TableNodeWidget', ()=>{
});
it('render', ()=>{
let nodeWidget = mount(<TableNodeWidget node={node}/>);
expect(nodeWidget.getDOMNode().className).toBe('table-node ');
expect(nodeWidget.find('.table-node .table-toolbar').length).toBe(1);
expect(nodeWidget.find('.table-node .table-schema').text()).toBe('erd');
expect(nodeWidget.find('.table-node .table-name').text()).toBe('table1');
expect(nodeWidget.find('.table-node .table-cols').length).toBe(1);
expect(nodeWidget.find(DetailsToggleButton).length).toBe(1);
expect(nodeWidget.find(IconButton).findWhere(n => n.prop('title')=='Check note').length).toBe(1);
});
it('node selected', ()=>{
spyOn(node, 'isSelected').and.returnValue(true);
let nodeWidget = mount(<TableNodeWidget node={node}/>);
expect(nodeWidget.getDOMNode().className).toBe('table-node selected');
let nodeWidget = mount(<Theme><TableNodeWidget node={node}/></Theme>);
expect(nodeWidget.find('DefaultButton[aria-label="Show Details"]').length).toBe(1);
expect(nodeWidget.find('DefaultButton[aria-label="Check Note"]').length).toBe(1);
expect(nodeWidget.find('div[data-test="schema-name"]').length).toBe(1);
expect(nodeWidget.find('div[data-test="table-name"]').length).toBe(1);
expect(nodeWidget.find('div[data-test="column-row"]').length).toBe(3);
});
it('remove note', ()=>{
node.setNote('');
let nodeWidget = mount(<TableNodeWidget node={node}/>);
expect(nodeWidget.find(IconButton).findWhere(n => n.prop('title')=='Check note').length).toBe(0);
let nodeWidget = mount(<Theme><TableNodeWidget node={node}/></Theme>);
expect(nodeWidget.find('PgIconButton[aria-label="Check Note"]').length).toBe(0);
});
describe('generateColumn', ()=>{
let nodeWidget = null;
beforeEach(()=>{
nodeWidget = mount(<TableNodeWidget node={node}/>);
nodeWidget = mount(<Theme><TableNodeWidget node={node}/></Theme>);
});
it('count', ()=>{
expect(nodeWidget.find('.table-node .table-cols .col-row').length).toBe(3);
expect(nodeWidget.find('div[data-test="column-row"]').length).toBe(3);
});
it('column names', ()=>{
let cols = nodeWidget.find('.table-node .table-cols .col-row-data');
expect(cols.at(0).find('.col-name').text()).toBe('id');
expect(cols.at(1).find('.col-name').text()).toBe('amount');
expect(cols.at(2).find('.col-name').text()).toBe('desc');
let cols = nodeWidget.find('div[data-test="column-row"]');
expect(cols.at(0).find('span[data-test="column-name"]').text()).toBe('id');
expect(cols.at(1).find('span[data-test="column-name"]').text()).toBe('amount');
expect(cols.at(2).find('span[data-test="column-name"]').text()).toBe('desc');
});
it('data types', ()=>{
let cols = nodeWidget.find('.table-node .table-cols .col-row-data');
expect(cols.at(0).find('.col-datatype').text()).toBe('integer');
expect(cols.at(1).find('.col-datatype').text()).toBe('number(10,5)');
expect(cols.at(2).find('.col-datatype').text()).toBe('character varrying(50)');
});
let cols = nodeWidget.find('div[data-test="column-row"]');
it('show_details', (done)=>{
nodeWidget.setState({show_details: false});
expect(nodeWidget.find('.table-node .table-cols .col-row-data .col-datatype').length).toBe(0);
nodeWidget.instance().toggleShowDetails(jasmine.createSpyObj('event', ['preventDefault']));
/* Dummy set state to wait for toggleShowDetails -> setState to complete */
nodeWidget.setState({}, ()=>{
expect(nodeWidget.find('.table-node .table-cols .col-row-data .col-datatype').length).toBe(3);
done();
});
expect(cols.at(0).find('span[data-test="column-type"]').text()).toBe('integer');
expect(cols.at(1).find('span[data-test="column-type"]').text()).toBe('number(10,5)');
expect(cols.at(2).find('span[data-test="column-type"]').text()).toBe('character varrying(50)');
});
});
});

View File

@@ -0,0 +1,32 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import {mount} from 'enzyme';
import '../../helper/enzyme.helper';
import ConnectionBar, {STATUS} from 'pgadmin.tools.erd/erd_tool/components/ConnectionBar';
import Theme from '../../../../pgadmin/static/js/Theme';
describe('ERD ConnectionBar', ()=>{
beforeEach(()=>{
jasmineEnzyme();
});
it('<ConnectionBar /> comp', ()=>{
const connBar = mount(<Theme><ConnectionBar status={STATUS.DISCONNECTED} title="test title"/></Theme>);
expect(connBar.find('DefaultButton[data-test="btn-conn-title"]').text()).toBe('test title');
connBar.setProps({
children: <ConnectionBar status={STATUS.CONNECTING} title="test title"/>
});
expect(connBar.find('DefaultButton[data-test="btn-conn-title"]').text()).toBe('(Obtaining connection...) test title');
connBar.setProps({
children: <ConnectionBar status={STATUS.CONNECTING} title="test title" bgcolor='#000' fgcolor='#fff'/>
});
const titleEle = connBar.find('DefaultButton[data-test="btn-conn-title"]');
expect(titleEle.prop('style').backgroundColor).toBe('#000');
expect(titleEle.prop('style').color).toBe('#fff');
});
});

View File

@@ -1,4 +1,3 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import {mount} from 'enzyme';
import '../../helper/enzyme.helper';
@@ -6,12 +5,14 @@ import MockAdapter from 'axios-mock-adapter';
import axios from 'axios/index';
import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore';
import * as erdModule from 'pgadmin.tools.erd/erd_module';
import * as erdModule from 'pgadmin.tools.erd/ERDModule';
import erdPref from './erd_preferences';
import BodyWidget from 'pgadmin.tools.erd/erd_tool/ui_components/BodyWidget';
import ERDTool from 'pgadmin.tools.erd/erd_tool/components/ERDTool';
import * as ERDSqlTool from 'tools/sqleditor/static/js/show_query_tool';
import { FakeLink, FakeNode, FakePort } from '../fake_item';
import Notify from '../../../../pgadmin/static/js/helpers/Notifier';
import Theme from '../../../../pgadmin/static/js/Theme';
import ModalProvider from '../../../../pgadmin/static/js/helpers/ModalProvider';
let pgAdmin = {
@@ -50,18 +51,9 @@ let pgWindow = {
pgAdmin: pgAdmin,
};
let alertify = jasmine.createSpyObj('alertify', {
'success': null,
'error': null,
'confirm': null,
'alert': {
'set': ()=>{/*This is intentional (SonarQube)*/},
},
});
let tableDialog = jasmine.createSpyObj('TableDialog', ['show']);
let otmDialog = jasmine.createSpyObj('otmDialog', ['show']);
let mtmDialog = jasmine.createSpyObj('mtmDialog', ['show']);
let tableDialog = jasmine.createSpy('TableDialog');
let otmDialog = jasmine.createSpy('otmDialog');
let mtmDialog = jasmine.createSpy('mtmDialog');
let getDialog = (dialogName)=>{
switch(dialogName) {
@@ -71,7 +63,8 @@ let getDialog = (dialogName)=>{
}
};
describe('ERD BodyWidget', ()=>{
describe('ERDTool', ()=>{
let erd = null;
let body = null;
let bodyInstance = null;
let networkMock = null;
@@ -129,36 +122,34 @@ describe('ERD BodyWidget', ()=>{
});
beforeEach(()=>{
jasmineEnzyme();
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} alertify={alertify}/>);
erd = mount(
<Theme>
<ModalProvider>
<ERDTool params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} />
</ModalProvider>
</Theme>
);
body = erd.find('ERDTool');
bodyInstance = body.instance();
spyOn(bodyInstance, 'getDialog').and.callFake(getDialog);
});
afterAll(() => {
networkMock.restore();
if(body) {
body.unmount();
if(erd) {
erd.unmount();
}
});
it('constructor', (done)=>{
expect(body.find('ToolBar').length).toBe(1);
expect(body.find('ConnectionBar').length).toBe(1);
expect(body.find('FloatingNote').length).toBe(1);
expect(body.find('.diagram-container Loader').length).toBe(1);
expect(body.find('.diagram-container CanvasWidget').length).toBe(1);
body.instance().setState({}, ()=>{
let instance = body.instance();
bodyInstance.setState({}, ()=>{
setTimeout(()=>{
expect(body.state()).toEqual(jasmine.objectContaining({
server_version: serverVersion,
preferences: erdPref,
}));
expect(instance.diagram.getCache('colTypes')).toEqual(colTypes);
expect(instance.diagram.getCache('schemas')).toEqual(schemas);
expect(bodyInstance.diagram.getCache('colTypes')).toEqual(colTypes);
expect(bodyInstance.diagram.getCache('schemas')).toEqual(schemas);
done();
});
});
@@ -237,17 +228,6 @@ describe('ERD BodyWidget', ()=>{
});
});
it('getDialog', ()=>{
bodyInstance.getDialog('table_dialog')();
expect(tableDialog.show).toHaveBeenCalled();
bodyInstance.getDialog('onetomany_dialog')();
expect(otmDialog.show).toHaveBeenCalled();
bodyInstance.getDialog('manytomany_dialog')();
expect(mtmDialog.show).toHaveBeenCalled();
});
it('addEditTable', ()=>{
let node1 = new FakeNode({'name': 'table1', schema: 'erd1', columns: [{name: 'col1', type: 'type1', attnum: 1}]}, 'id1');
let node2 = new FakeNode({'name': 'table2', schema: 'erd2', columns: [{name: 'col2', type: 'type2', attnum: 2}]}, 'id2');
@@ -261,23 +241,23 @@ describe('ERD BodyWidget', ()=>{
spyOn(bodyInstance.diagram, 'addLink');
spyOn(bodyInstance.diagram, 'syncTableLinks');
/* New */
tableDialog.show.calls.reset();
tableDialog.calls.reset();
bodyInstance.addEditTable();
expect(tableDialog.show).toHaveBeenCalled();
expect(tableDialog).toHaveBeenCalled();
let saveCallback = tableDialog.show.calls.mostRecent().args[7];
let saveCallback = tableDialog.calls.mostRecent().args[3];
let newData = {key: 'value'};
saveCallback(newData);
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith(newData);
/* Existing */
tableDialog.show.calls.reset();
tableDialog.calls.reset();
let node = new FakeNode({name: 'table1', schema: 'erd1'});
spyOn(node, 'setData');
bodyInstance.addEditTable(node);
expect(tableDialog.show).toHaveBeenCalled();
expect(tableDialog).toHaveBeenCalled();
saveCallback = tableDialog.show.calls.mostRecent().args[7];
saveCallback = tableDialog.calls.mostRecent().args[3];
newData = {key: 'value'};
saveCallback(newData);
expect(node.setData).toHaveBeenCalledWith(newData);
@@ -435,11 +415,8 @@ describe('ERD BodyWidget', ()=>{
spyOn(bodyInstance.diagram, 'addLink');
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
otmDialog.show.calls.reset();
bodyInstance.onOneToManyClick();
expect(otmDialog.show).toHaveBeenCalled();
let saveCallback = otmDialog.show.calls.mostRecent().args[4];
let saveCallback = otmDialog.calls.mostRecent().args[2];
let newData = {
local_table_uid: 'id1',
local_column_attnum: 1,
@@ -452,13 +429,6 @@ describe('ERD BodyWidget', ()=>{
it('onManyToManyClick', ()=>{
let node = new FakeNode({}, 'id1');
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
mtmDialog.show.calls.reset();
bodyInstance.onManyToManyClick();
expect(mtmDialog.show).toHaveBeenCalled();
/* onSave */
let node1 = new FakeNode({'name': 'table1', schema: 'erd1', columns: [{name: 'col1', type: 'type1', attnum: 1}]}, 'id1');
let node2 = new FakeNode({'name': 'table2', schema: 'erd2', columns: [{name: 'col2', type: 'type2', attnum: 2}]}, 'id2');
let nodesDict = {
@@ -469,8 +439,13 @@ describe('ERD BodyWidget', ()=>{
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
'getNodesDict': ()=>nodesDict,
});
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
bodyInstance.onManyToManyClick();
/* onSave */
spyOn(bodyInstance.diagram, 'addLink');
let saveCallback = mtmDialog.show.calls.mostRecent().args[4];
let saveCallback = mtmDialog.calls.mostRecent().args[2];
let newData = {
left_table_uid: 'id1',
left_table_column_attnum: 1,

View File

@@ -3,7 +3,8 @@ import React from 'react';
import {mount} from 'enzyme';
import '../../helper/enzyme.helper';
import FloatingNote from 'pgadmin.tools.erd/erd_tool/ui_components/FloatingNote';
import FloatingNote from 'pgadmin.tools.erd/erd_tool/components/FloatingNote';
import Theme from '../../../../pgadmin/static/js/Theme';
describe('ERD FloatingNote', ()=>{
@@ -24,15 +25,20 @@ describe('ERD FloatingNote', ()=>{
},
};
floatNote = mount(<FloatingNote open={false} onClose={onClose}
reference={null} noteNode={noteNode} appendTo={document.body} rows={8}/>);
floatNote = mount(
<Theme>
<FloatingNote
open={true} onClose={onClose} anchorEl={document.body} rows={8} noteNode={noteNode}
/>
</Theme>);
floatNote.find('textarea').simulate('change', {
target: {
value: 'the new note',
},
});
floatNote.find('button[data-label="OK"]').simulate('click');
floatNote.find('DefaultButton').simulate('click');
expect(noteNode.setNote).toHaveBeenCalledWith('the new note');
expect(onClose).toHaveBeenCalled();
});

View File

@@ -1,25 +0,0 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import {mount} from 'enzyme';
import '../../helper/enzyme.helper';
import ConnectionBar, {STATUS} from 'pgadmin.tools.erd/erd_tool/ui_components/ConnectionBar';
describe('ERD ConnectionBar', ()=>{
beforeEach(()=>{
jasmineEnzyme();
});
it('<ConnectionBar /> comp', ()=>{
let connBar = mount(<ConnectionBar statusId="conn-bar" status={STATUS.DISCONNECTED} title="test title"/>);
expect(connBar.find('.editor-title').text()).toBe('test title');
connBar.setProps({status: STATUS.CONNECTING});
expect(connBar.find('.editor-title').text()).toBe('(Obtaining connection...) test title');
connBar.setProps({bgcolor: '#000', fgcolor: '#fff'});
expect(connBar.find('.editor-title').prop('style').backgroundColor).toBe('#000');
expect(connBar.find('.editor-title').prop('style').color).toBe('#fff');
});
});

View File

@@ -1,23 +0,0 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import {shallow} from 'enzyme';
import '../../helper/enzyme.helper';
import Loader from 'pgadmin.tools.erd/erd_tool/ui_components/Loader';
describe('ERD Loader', ()=>{
beforeEach(()=>{
jasmineEnzyme();
});
it('<Loader /> comp', ()=>{
let loaderComp = shallow(<Loader />);
expect(loaderComp.isEmptyRender()).toBeTruthy();
loaderComp.setProps({message: 'test message'});
expect(loaderComp.find('.pg-sp-text').text()).toBe('test message');
loaderComp.setProps({autoEllipsis: true});
expect(loaderComp.find('.pg-sp-text').text()).toBe('test message...');
});
});

View File

@@ -1,76 +0,0 @@
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import Tippy from '@tippyjs/react';
import {mount, shallow} from 'enzyme';
import '../../helper/enzyme.helper';
import ToolBar, {ButtonGroup, DetailsToggleButton, IconButton, Shortcut} from 'pgadmin.tools.erd/erd_tool/ui_components/ToolBar';
describe('ERD Toolbar', ()=>{
beforeEach(()=>{
jasmineEnzyme();
});
it('<Toolbar /> comp', ()=>{
let toolBar = mount(<ToolBar id="id1"><div className="test"></div></ToolBar>);
expect(toolBar.getDOMNode().id).toBe('id1');
expect(toolBar.find('.test').length).toBe(1);
});
it('<ButtonGroup /> comp', ()=>{
let btnGrp = mount(<ButtonGroup><div className="test"></div></ButtonGroup>);
expect(btnGrp.getDOMNode().className).toBe('btn-group mr-1 ');
expect(btnGrp.find('.test').length).toBe(1);
btnGrp.unmount();
btnGrp = mount(<ButtonGroup className="someclass"></ButtonGroup>);
expect(btnGrp.getDOMNode().className).toBe('btn-group mr-1 someclass');
});
it('<DetailsToggleButton /> comp', ()=>{
let toggle = shallow(<DetailsToggleButton showDetails={true} />);
let btn = toggle.find(IconButton);
expect(btn.prop('icon')).toBe('far fa-eye');
expect(btn.prop('title')).toBe('Show fewer details');
toggle.setProps({showDetails: false});
btn = toggle.find(IconButton);
expect(btn.prop('icon')).toBe('fas fa-low-vision');
expect(btn.prop('title')).toBe('Show more details');
});
it('<IconButton /> comp', ()=>{
let btn = mount(<IconButton />);
let tippy = btn.find(Tippy);
expect(tippy.length).toBe(0);
btn.setProps({title: 'test title'});
tippy = btn.find(Tippy);
expect(tippy.length).toBe(1);
expect(btn.find('button').getDOMNode().className).toBe('btn btn-sm btn-primary-icon ');
btn.setProps({icon: 'fa fa-icon'});
expect(btn.find('button .sql-icon-lg').getDOMNode().className).toBe('fa fa-icon sql-icon-lg');
});
it('<Shortcut /> comp', ()=>{
let key = {
alt: true,
control: true,
shift: false,
key: {
key_code: 65,
char: 'a',
},
};
let shortcutComp = mount(<Shortcut shortcut={key}/>);
expect(shortcutComp.find('.shortcut-key').length).toBe(3);
key.alt = false;
shortcutComp.setProps({shortcut: key});
expect(shortcutComp.find('.shortcut-key').length).toBe(2);
});
});

View File

@@ -13,6 +13,13 @@ beforeAll(function () {
spyOn(console, 'warn').and.callThrough();
spyOn(console, 'error').and.callThrough();
jasmine.getEnv().allowRespy(true);
window.addEventListener('error', e => {
if(e.message === 'ResizeObserver loop completed with undelivered notifications.' ||
e.message === 'ResizeObserver loop limit exceeded') {
e.stopImmediatePropagation();
}
});
});
afterEach(function (done) {

View File

@@ -379,7 +379,7 @@ module.exports = [{
slickgrid: sourceDir + '/bundle/slickgrid.js',
sqleditor: './pgadmin/tools/sqleditor/static/js/index.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js',
erd_tool: './pgadmin/tools/erd/static/js/index.js',
psql_tool: './pgadmin/tools/psql/static/js/index.js',
debugger: './pgadmin/tools/debugger/static/js/index.js',
'pgadmin.style': pgadminCssStyles,
@@ -546,7 +546,7 @@ module.exports = [{
'pure|pgadmin.tools.schema_diff',
'pure|pgadmin.tools.file_manager',
'pure|pgadmin.tools.search_objects',
'pure|pgadmin.tools.erd_module',
'pure|pgadmin.tools.erd',
'pure|pgadmin.tools.psql_module',
'pure|pgadmin.tools.sqleditor',
'pure|pgadmin.misc.cloud',

View File

@@ -281,7 +281,6 @@ var webpackShimConfig = {
'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'),
'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'),
'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),

View File

@@ -2306,11 +2306,6 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
"@popperjs/core@^2.9.0":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
"@projectstorm/geometry@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/geometry/-/geometry-6.6.1.tgz#4a42f5c8fdfcc3d951e73f5db7fe9546514acc3d"
@@ -2501,13 +2496,6 @@
prop-types "^15.7.2"
react-transition-state "^1.1.3"
"@tippyjs/react@^4.2.0":
version "4.2.6"
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71"
integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==
dependencies:
tippy.js "^6.3.1"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -11061,13 +11049,6 @@ tiny-warning@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tippy.js@^6.3.1:
version "6.3.5"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.5.tgz#cbc99d34f87ccc127e6460032b86c8d47971d38f"
integrity sha512-B9hAQ5KNF+jDJRg6cRysV6Y3J+5fiNfD60GuXR5TP0sfrcltpgdzVc7f1wMtjQ3W0+Xsy80CDvk0Z+Vr0cM4sQ==
dependencies:
"@popperjs/core" "^2.9.0"
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"