1) Added support for advanced table fields like the foreign key, primary key in the ERD tool. Fixes #6081

2) Added index creation when generating SQL in the ERD tool. Fixes #6529
This commit is contained in:
Aditya Toshniwal
2021-10-11 17:42:14 +05:30
committed by Akshay Joshi
parent 9796f50362
commit a92c1b43a2
26 changed files with 900 additions and 1416 deletions

View File

@@ -532,6 +532,35 @@ def prequisite(trans_id, sgid, sid, did):
)
def translate_foreign_keys(tab_fks, tab_data, all_nodes):
"""
This function will take the from table foreign keys and translate
it into non oid based format. It will allow creating FK sql even
if table is not already created.
:param tab_fks: Table foreign keyss
:param tab_data: Table data
:param all_nodes: All the nodes info from ERD
:return: Translated foreign key data
"""
for tab_fk in tab_fks:
if 'columns' not in tab_fk:
continue
print(tab_data)
remote_table = all_nodes[tab_fk['columns'][0]['references']]
tab_fk['schema'] = tab_data['schema']
tab_fk['table'] = tab_data['name']
tab_fk['remote_schema'] = remote_table['schema']
tab_fk['remote_table'] = remote_table['name']
new_column = {
'local_column': tab_fk['columns'][0]['local_column'],
'referenced': tab_fk['columns'][0]['referenced']
}
tab_fk['columns'][0] = new_column
return tab_fks
@blueprint.route('/sql/<int:trans_id>/<int:sgid>/<int:sid>/<int:did>',
methods=["POST"],
endpoint='sql')
@@ -542,12 +571,16 @@ def sql(trans_id, sgid, sid, did):
conn = _get_connection(sid, did, trans_id)
sql = ''
for tab_key, tab_data in data.get('nodes', {}).items():
tab_foreign_keys = []
all_nodes = data.get('nodes', {})
for tab_key, tab_data in all_nodes.items():
tab_fks = tab_data.pop('foreign_key', [])
tab_foreign_keys.extend(translate_foreign_keys(tab_fks, tab_data, all_nodes))
sql += '\n\n' + helper.get_table_sql(tab_data)
for link_key, link_data in data.get('links', {}).items():
link_sql, name = fkey_utils.get_sql(conn, link_data, None)
sql += '\n\n' + link_sql
for tab_fk in tab_foreign_keys:
fk_sql, name = fkey_utils.get_sql(conn, tab_fk, None)
sql += '\n\n' + fk_sql
return make_json_response(
data=sql,

View File

@@ -13,11 +13,16 @@
import createEngine from '@projectstorm/react-diagrams';
import {DagreEngine, PathFindingLinkFactory, PortModelAlignment} from '@projectstorm/react-diagrams';
import { ZoomCanvasAction } from '@projectstorm/react-canvas-core';
import _ from 'lodash';
import {TableNodeFactory, TableNodeModel } from './nodes/TableNode';
import {OneToManyLinkFactory, OneToManyLinkModel } from './links/OneToManyLink';
import { OneToManyPortFactory } from './ports/OneToManyPort';
import ERDModel from './ERDModel';
import ForeignKeySchema from '../../../../../browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui';
import diffArray from 'diff-arrays-of-objects';
import TableSchema 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';
export default class ERDCore {
constructor() {
@@ -66,8 +71,8 @@ export default class ERDCore {
else if(e.function === 'showNote') {
this.fireEvent({node: e.entity}, 'showNote', true);
}
else if(e.function === 'editNode') {
this.fireEvent({node: e.entity}, 'editNode', true);
else if(e.function === 'editTable') {
this.fireEvent({node: e.entity}, 'editTable', true);
}
else if(e.function === 'nodeUpdated') {
this.fireEvent({}, 'nodesUpdated', true);
@@ -232,6 +237,188 @@ export default class ERDCore {
return newLink;
}
removePortLinks(port) {
let links = port.getLinks();
Object.values(links).forEach((link)=>{
link.getTargetPort().remove();
link.getSourcePort().remove();
link.setSelected(false);
link.remove();
});
}
syncTableLinks(tableNode, oldTableData) {
let tableData = tableNode.getData();
let tableNodesDict = this.getModel().getNodesDict();
const addLink = (theFk)=>{
let newData = {
local_table_uid: tableNode.getID(),
local_column_attnum: undefined,
referenced_table_uid: theFk.references,
referenced_column_attnum: undefined,
};
let sourceNode = tableNodesDict[newData.referenced_table_uid];
newData.local_column_attnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
newData.referenced_column_attnum = _.find(sourceNode.getColumns(), (col)=>col.name==theFk.referenced).attnum;
this.addLink(newData, 'onetomany');
};
const removeLink = (theFk)=>{
let attnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
let existPort = tableNode.getPort(tableNode.getPortName(attnum));
if(existPort && existPort.getSubtype() == 'many') {
existPort.removeAllLinks();
tableNode.removePort(existPort);
}
};
const changeDiff = diffArray(
oldTableData?.foreign_key || [],
tableData?.foreign_key || [],
'cid'
);
changeDiff.added.forEach((theFk)=>{
addLink(theFk.columns[0]);
});
changeDiff.removed.forEach((theFk)=>{
removeLink(theFk.columns[0]);
});
if(changeDiff.updated.length > 0) {
for(const changedRow of changeDiff.updated) {
let rowIndx = _.findIndex(tableData.foreign_key, (f)=>f.cid==changedRow.cid);
const changeDiffCols = diffArray(
oldTableData.foreign_key[rowIndx].columns,
tableData.foreign_key[rowIndx].columns,
'cid'
);
if(changeDiffCols.removed.length > 0 || changeDiffCols.added.length > 0) {
removeLink(changeDiffCols.removed[0]);
addLink(changeDiffCols.added[0]);
}
}
}
}
addOneToManyLink(onetomanyData) {
let newFk = new ForeignKeySchema({}, {}, ()=>{}, {autoindex: false});
let tableNodesDict = this.getModel().getNodesDict();
let fkColumn = {};
let sourceNode = tableNodesDict[onetomanyData.referenced_table_uid];
let targetNode = tableNodesDict[onetomanyData.local_table_uid];
fkColumn.local_column = _.find(targetNode.getColumns(), (col)=>col.attnum==onetomanyData.local_column_attnum).name;
fkColumn.referenced = _.find(sourceNode.getColumns(), (col)=>col.attnum==onetomanyData.referenced_column_attnum).name;
fkColumn.references = onetomanyData.referenced_table_uid;
fkColumn.references_table_name = sourceNode.getData().name;
let tableData = targetNode.getData();
tableData.foreign_key = tableData.foreign_key || [];
let col = newFk.fkColumnSchema.getNewData(fkColumn);
tableData.foreign_key.push(
newFk.getNewData({
columns: [col],
})
);
targetNode.setData(tableData);
let newLink = this.addLink(onetomanyData, 'onetomany');
this.clearSelection();
newLink.setSelected(true);
this.repaint();
}
removeOneToManyLink(link) {
let linkData = link.getData();
let tableNode = this.getModel().getNodesDict()[linkData.local_table_uid];
let tableData = tableNode.getData();
let newForeingKeys = [];
tableData.foreign_key?.forEach((theFkRow)=>{
let theFk = theFkRow.columns[0];
let attnum = _.find(tableNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
/* Skip all those whose attnum matches to the link */
if(linkData.local_column_attnum != attnum) {
newForeingKeys.push(theFkRow);
}
});
tableData.foreign_key = newForeingKeys;
tableNode.setData(tableData);
link.getTargetPort().remove();
link.getSourcePort().remove();
link.setSelected(false);
link.remove();
}
addManyToManyLink(manytomanyData) {
let nodes = this.getModel().getNodesDict();
let leftNode = nodes[manytomanyData.left_table_uid];
let rightNode = nodes[manytomanyData.right_table_uid];
let tableObj = new TableSchema({}, {}, {
constraints:()=>{},
columns:()=>new ColumnSchema(()=>{}, {}, {}, {}),
vacuum_settings:()=>{},
}, ()=>{}, ()=>{}, ()=>{}, ()=>{});
let tableData = tableObj.getNewData({
name: `${leftNode.getData().name}_${rightNode.getData().name}`,
schema: leftNode.getData().schema,
columns: [tableObj.columnsSchema.getNewData({
...leftNode.getColumnAt(manytomanyData.left_table_column_attnum),
'name': `${leftNode.getData().name}_${leftNode.getColumnAt(manytomanyData.left_table_column_attnum).name}`,
'attnum': 0,
'is_primary_key': false,
}),tableObj.columnsSchema.getNewData({
...rightNode.getColumnAt(manytomanyData.right_table_column_attnum),
'name': `${rightNode.getData().name}_${rightNode.getColumnAt(manytomanyData.right_table_column_attnum).name}`,
'attnum': 1,
'is_primary_key': false,
})],
});
// let tableData = {
// name: `${leftNode.getData().name}_${rightNode.getData().name}`,
// schema: leftNode.getData().schema,
// columns: [{
// ...leftNode.getColumnAt(manytomanyData.left_table_column_attnum),
// 'name': `${leftNode.getData().name}_${leftNode.getColumnAt(manytomanyData.left_table_column_attnum).name}`,
// 'is_primary_key': false,
// 'attnum': 0,
// },{
// ...rightNode.getColumnAt(manytomanyData.right_table_column_attnum),
// 'name': `${rightNode.getData().name}_${rightNode.getColumnAt(manytomanyData.right_table_column_attnum).name}`,
// 'is_primary_key': false,
// 'attnum': 1,
// }],
// };
let newNode = this.addNode(tableData);
this.clearSelection();
newNode.setSelected(true);
let linkData = {
local_table_uid: newNode.getID(),
local_column_attnum: newNode.getColumns()[0].attnum,
referenced_table_uid: manytomanyData.left_table_uid,
referenced_column_attnum : manytomanyData.left_table_column_attnum,
};
this.addOneToManyLink(linkData);
linkData = {
local_table_uid: newNode.getID(),
local_column_attnum: newNode.getColumns()[1].attnum,
referenced_table_uid: manytomanyData.right_table_uid,
referenced_column_attnum : manytomanyData.right_table_column_attnum,
};
this.addOneToManyLink(linkData);
this.repaint();
}
serialize(version) {
return {
version: version||0,
@@ -246,65 +433,53 @@ export default class ERDCore {
}
serializeData() {
let nodes = {}, links = {};
let nodes = {};
let nodesDict = this.getModel().getNodesDict();
Object.keys(nodesDict).forEach((id)=>{
nodes[id] = nodesDict[id].serializeData();
});
this.getModel().getLinks().map((link)=>{
links[link.getID()] = link.serializeData(nodesDict);
});
/* Separate the links from nodes so that we don't have any dependancy issues */
return {
'nodes': nodes,
'links': links,
};
}
deserializeData(data){
let oidUidMap = {};
let uidFks = [];
data.forEach((node)=>{
let newData = {
name: node.name,
schema: node.schema,
description: node.description,
columns: node.columns,
primary_key: node.primary_key,
};
let newNode = this.addNode(newData);
oidUidMap[node.oid] = newNode.getID();
if(node.foreign_key) {
node.foreign_key.forEach((a_fk)=>{
uidFks.push({
uid: newNode.getID(),
data: a_fk.columns[0],
});
/* Add the nodes */
data.forEach((nodeData)=>{
let newNode = this.addNode(TableSchema.getErdSupportedData(nodeData));
oidUidMap[nodeData.oid] = newNode.getID();
});
/* Lets use the oidUidMap for creating the links */
let tableNodesDict = this.getModel().getNodesDict();
_.forIn(tableNodesDict, (node, uid)=>{
let nodeData = node.getData();
if(nodeData.foreign_key) {
nodeData.foreign_key.forEach((theFk)=>{
delete theFk.oid;
theFk = theFk.columns[0];
theFk.references = oidUidMap[theFk.references];
let newData = {
local_table_uid: uid,
local_column_attnum: undefined,
referenced_table_uid: theFk.references,
referenced_column_attnum: undefined,
};
let sourceNode = tableNodesDict[newData.referenced_table_uid];
let targetNode = tableNodesDict[newData.local_table_uid];
newData.local_column_attnum = _.find(targetNode.getColumns(), (col)=>col.name==theFk.local_column).attnum;
newData.referenced_column_attnum = _.find(sourceNode.getColumns(), (col)=>col.name==theFk.referenced).attnum;
this.addLink(newData, 'onetomany');
});
}
});
/* Lets use the oidUidMap for creating the links */
uidFks.forEach((fkData)=>{
let tableNodesDict = this.getModel().getNodesDict();
let newData = {
local_table_uid: fkData.uid,
local_column_attnum: undefined,
referenced_table_uid: oidUidMap[fkData.data.references],
referenced_column_attnum: undefined,
};
let sourceNode = tableNodesDict[newData.referenced_table_uid];
let targetNode = tableNodesDict[newData.local_table_uid];
newData.local_column_attnum = _.find(targetNode.getColumns(), (col)=>col.name==fkData.data.local_column).attnum;
newData.referenced_column_attnum = _.find(sourceNode.getColumns(), (col)=>col.name==fkData.data.referenced).attnum;
this.addLink(newData, 'onetomany');
});
setTimeout(this.dagreDistributeNodes.bind(this), 250);
}

View File

@@ -7,152 +7,98 @@
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import * as commonUtils from 'sources/utils';
import React from 'react';
import ReactDOM from 'react-dom';
import SchemaView from '../../../../../../static/js/SchemaView';
export default class DialogWrapper {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog,
jquery, pgBrowser, alertify, backform, backgrid) {
constructor(dialogContainerSelector, dialogTitle, typeOfDialog, alertify, serverInfo) {
this.dialogContainerSelector = dialogContainerSelector;
this.dialogTitle = dialogTitle;
this.jquery = jquery;
this.pgBrowser = pgBrowser;
this.alertify = alertify;
this.backform = backform;
this.backgrid = backgrid;
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, dialogModel, okCallback) {
main(title, dialogSchema, okCallback) {
this.set('title', title);
this.dialogModel = dialogModel;
this.dialogSchema = dialogSchema;
this.okCallback = okCallback;
}
build() {
this.alertify.pgDialogBuild.apply(this);
this.elements.dialog.classList.add('erd-dialog');
}
disableOKButton() {
this.__internal.buttons[1].element.disabled = true;
}
enableOKButton() {
this.__internal.buttons[1].element.disabled = false;
}
focusOnDialog(alertifyDialog) {
let backform_tab = this.jquery(alertifyDialog.elements.body).find('.backform-tab');
backform_tab.attr('tabindex', -1);
this.pgBrowser.keyboardNavigation.getDialogTabNavigator(this.jquery(alertifyDialog.elements.dialog));
let container = backform_tab.find('.tab-content:first > .tab-pane.active:first');
if(container.length === 0 && alertifyDialog.elements.content.innerHTML) {
container = this.jquery(alertifyDialog.elements.content);
}
commonUtils.findAndSetFocus(container);
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: [{
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
'data-btn-name': 'cancel',
}, {
text: gettext('OK'),
key: 13,
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
'data-btn-name': 'ok',
}],
buttons: [],
// Set options for dialog
options: {
title: this.dialogTitle,
//disable both padding and overflow control.
padding: !1,
overflow: !1,
model: 0,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
modal: false,
modal: true,
autoReset: false,
},
};
}
prepare() {
const $container = this.jquery(this.dialogContainerSelector);
const dialog = this.createDialog($container);
dialog.render();
this.elements.content.innerHTML = '';
this.elements.content.appendChild($container.get(0));
this.jquery(this.elements.body.childNodes[0]).addClass(
'alertify_tools_dialog_properties obj_properties'
);
const statusBar = this.jquery(
'<div class=\'pg-prop-status-bar pg-prop-status-bar-absolute pg-el-xs-12 d-none\'>' +
' <div class="error-in-footer"> ' +
' <div class="d-flex px-2 py-1"> ' +
' <div class="pr-2"> ' +
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
' </div> ' +
' <div class="alert-text" role="alert"></div> ' +
' <div class="ml-auto close-error-bar"> ' +
' <a aria-label="' + gettext('Close error bar') + '" class="close-error fa fa-times text-danger"></a> ' +
' </div> ' +
' </div> ' +
' </div> ' +
'</div>').appendTo($container);
statusBar.find('.close-error').on('click', ()=>{
statusBar.addClass('d-none');
onSaveClick(isNew, data) {
return new Promise((resolve)=>{
this.okCallback(data);
this.close();
resolve();
});
var onSessionInvalid = (msg) => {
statusBar.find('.alert-text').text(msg);
statusBar.removeClass('d-none');
this.disableOKButton();
return true;
};
var onSessionValidated = () => {
statusBar.find('.alert-text').text('');
statusBar.addClass('d-none');
this.enableOKButton();
return true;
};
this.dialogModel.on('pgadmin-session:valid', onSessionValidated);
this.dialogModel.on('pgadmin-session:invalid', onSessionInvalid);
this.dialogModel.startNewSession();
this.disableOKButton();
this.focusOnDialog(this);
}
callback(event) {
if (this.wasOkButtonPressed(event)) {
this.okCallback(this.view.model.toJSON(true));
}
createDialog(container) {
let self = this;
ReactDOM.render(
<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={()=>{}}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
/>, container);
}
createDialog($container) {
let fields = this.backform.generateViewSchema(
null, this.dialogModel, 'create', null, null, true, null
);
this.view = new this.backform.Dialog({
el: $container,
model: this.dialogModel,
schema: fields,
});
return this.view;
}
wasOkButtonPressed(event) {
return event.button['data-btn-name'] === 'ok';
cleanupDialog(container) {
ReactDOM.unmountComponentAtNode(container);
}
}

View File

@@ -8,13 +8,48 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Backform from 'sources/backform.pgadmin';
import Alertify from 'pgadmin.alertifyjs';
import $ from 'jquery';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DialogWrapper from './DialogWrapper';
import _ from 'lodash';
class ManyToManySchema extends BaseUISchema {
constructor(fieldOptions={}, initValues={}) {
super({
left_table_uid: undefined,
left_table_column_attnum: undefined,
right_table_uid: undefined,
right_table_column_attnum: undefined,
...initValues,
});
this.fieldOptions = fieldOptions;
}
get baseFields() {
return [{
id: 'left_table_uid', label: gettext('Local Table'),
type: 'select', readonly: true, controlProps: {allowClear: false},
options: this.fieldOptions.left_table_uid,
}, {
id: 'left_table_column_attnum', label: gettext('Local Column'),
type: 'select', options: this.fieldOptions.left_table_column_attnum,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'right_table_uid', label: gettext('Referenced Table'),
type: 'select', options: this.fieldOptions.right_table_uid,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'right_table_column_attnum', label: gettext('Referenced Column'),
controlProps: {allowClear: false}, deps: ['right_table_uid'],
type: (state)=>({
type: 'select',
options: state.right_table_uid ? ()=>this.fieldOptions.getRefColumns(state.right_table_uid) : [],
optionsReloadBasis: state.right_table_uid,
}),
}];
}
}
export default class ManyToManyDialog {
constructor(pgBrowser) {
this.pgBrowser = pgBrowser;
@@ -24,93 +59,29 @@ export default class ManyToManyDialog {
return 'manytomany_dialog';
}
getDataModel(attributes, tableNodesDict) {
const parseColumns = (columns)=>{
return columns.map((col)=>{
return {
value: col.attnum, label: col.name,
};
});
};
let dialogModel = this.pgBrowser.DataModel.extend({
defaults: {
left_table_uid: undefined,
left_table_column_attnum: undefined,
right_table_uid: undefined,
right_table_column_attnum: undefined,
},
schema: [{
id: 'left_table_uid', label: gettext('Left Table'),
type: 'select2', readonly: true,
options: ()=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
},
}, {
id: 'left_table_column_attnum', label: gettext('Left table Column'),
type: 'select2', disabled: false, first_empty: false,
editable: true, options: (view)=>{
return parseColumns(tableNodesDict[view.model.get('left_table_uid')].getColumns());
},
},{
id: 'right_table_uid', label: gettext('Right Table'),
type: 'select2', disabled: false,
editable: true, options: (view)=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
if(uid === view.model.get('left_table_uid')) {
return;
}
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
},
},{
id: 'right_table_column_attnum', label: gettext('Right table Column'),
type: 'select2', disabled: false, deps: ['right_table_uid'],
editable: true, options: (view)=>{
if(view.model.get('right_table_uid')) {
return parseColumns(tableNodesDict[view.model.get('right_table_uid')].getColumns());
}
return [];
},
}],
validate: function(keys) {
var msg = undefined;
// Nothing to validate
if (keys && keys.length == 0) {
this.errorModel.clear();
return null;
} else {
this.errorModel.clear();
}
if (_.isUndefined(this.get('left_table_column_attnum')) || this.get('left_table_column_attnum') == '') {
msg = gettext('Select the left table column.');
this.errorModel.set('left_table_column_attnum', msg);
return msg;
}
if (_.isUndefined(this.get('right_table_uid')) || this.get('right_table_uid') == '') {
msg = gettext('Select the right table.');
this.errorModel.set('right_table_uid', msg);
return msg;
}
if (_.isUndefined(this.get('right_table_column_attnum')) || this.get('right_table_column_attnum') == '') {
msg = gettext('Select the right table column.');
this.errorModel.set('right_table_column_attnum', msg);
return msg;
}
},
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 dialogModel(attributes);
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',
};
});
},
}, attributes);
}
createOrGetDialog(title) {
@@ -122,19 +93,16 @@ export default class ManyToManyDialog {
`<div class="${dialogName}"></div>`,
title,
null,
$,
this.pgBrowser,
Alertify,
Backform
);
});
}
return Alertify[dialogName];
}
show(title, attributes, tablesData, sVersion, callback) {
show(title, attributes, tablesData, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('manytomany_dialog');
dialog(dialogTitle, this.getDataModel(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
const dialog = this.createOrGetDialog('manytomany_dialog', serverInfo);
dialog(dialogTitle, this.getUISchema(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
}
}

View File

@@ -8,13 +8,48 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Backform from 'sources/backform.pgadmin';
import Alertify from 'pgadmin.alertifyjs';
import $ from 'jquery';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DialogWrapper from './DialogWrapper';
import _ from 'lodash';
class OneToManySchema extends BaseUISchema {
constructor(fieldOptions={}, initValues={}) {
super({
local_table_uid: undefined,
local_column_attnum: undefined,
referenced_table_uid: undefined,
referenced_column_attnum: undefined,
...initValues,
});
this.fieldOptions = fieldOptions;
}
get baseFields() {
return [{
id: 'local_table_uid', label: gettext('Local Table'),
type: 'select', readonly: true, controlProps: {allowClear: false},
options: this.fieldOptions.local_table_uid,
}, {
id: 'local_column_attnum', label: gettext('Local Column'),
type: 'select', options: this.fieldOptions.local_column_attnum,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'referenced_table_uid', label: gettext('Referenced Table'),
type: 'select', options: this.fieldOptions.referenced_table_uid,
controlProps: {allowClear: false}, noEmpty: true,
},{
id: 'referenced_column_attnum', label: gettext('Referenced Column'),
controlProps: {allowClear: false}, deps: ['referenced_table_uid'], noEmpty: true,
type: (state)=>({
type: 'select',
options: state.referenced_table_uid ? ()=>this.fieldOptions.getRefColumns(state.referenced_table_uid) : [],
optionsReloadBasis: state.referenced_table_uid,
}),
}];
}
}
export default class OneToManyDialog {
constructor(pgBrowser) {
this.pgBrowser = pgBrowser;
@@ -24,93 +59,32 @@ export default class OneToManyDialog {
return 'onetomany_dialog';
}
getDataModel(attributes, tableNodesDict) {
const parseColumns = (columns)=>{
return columns.map((col)=>{
return {
value: col.attnum, label: col.name,
};
});
};
let dialogModel = this.pgBrowser.DataModel.extend({
defaults: {
local_table_uid: undefined,
local_column_attnum: undefined,
referenced_table_uid: undefined,
referenced_column_attnum: undefined,
},
schema: [{
id: 'local_table_uid', label: gettext('Local Table'),
type: 'select2', readonly: true,
options: ()=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
},
}, {
id: 'local_column_attnum', label: gettext('Local Column'),
type: 'select2', disabled: false, first_empty: false,
editable: true, options: (view)=>{
return parseColumns(tableNodesDict[view.model.get('local_table_uid')].getColumns());
},
},{
id: 'referenced_table_uid', label: gettext('Referenced Table'),
type: 'select2', disabled: false,
editable: true, options: ()=>{
let retOpts = [];
_.forEach(tableNodesDict, (node, uid)=>{
let [schema, name] = node.getSchemaTableName();
retOpts.push({value: uid, label: `(${schema}) ${name}`});
});
return retOpts;
},
},{
id: 'referenced_column_attnum', label: gettext('Referenced Column'),
type: 'select2', disabled: false, deps: ['referenced_table_uid'],
editable: true, options: (view)=>{
if(view.model.get('referenced_table_uid')) {
return parseColumns(tableNodesDict[view.model.get('referenced_table_uid')].getColumns());
}
return [];
},
}],
validate: function(keys) {
var msg = undefined;
// Nothing to validate
if (keys && keys.length == 0) {
this.errorModel.clear();
return null;
} else {
this.errorModel.clear();
}
if (_.isUndefined(this.get('local_column_attnum')) || this.get('local_column_attnum') == '') {
msg = gettext('Select the local column.');
this.errorModel.set('local_column_attnum', msg);
return msg;
}
if (_.isUndefined(this.get('referenced_table_uid')) || this.get('referenced_table_uid') == '') {
msg = gettext('Select the referenced table.');
this.errorModel.set('referenced_table_uid', msg);
return msg;
}
if (_.isUndefined(this.get('referenced_column_attnum')) || this.get('referenced_column_attnum') == '') {
msg = gettext('Select the referenced table column.');
this.errorModel.set('referenced_column_attnum', msg);
return msg;
}
},
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 dialogModel(attributes);
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',
};
});
},
}, attributes);
}
createOrGetDialog(title) {
createOrGetDialog(title, sVersion) {
const dialogName = this.dialogName();
if (!Alertify[dialogName]) {
@@ -119,19 +93,17 @@ export default class OneToManyDialog {
`<div class="${dialogName}"></div>`,
title,
null,
$,
this.pgBrowser,
Alertify,
Backform
sVersion
);
});
}
return Alertify[dialogName];
}
show(title, attributes, tablesData, sVersion, callback) {
show(title, attributes, tablesData, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('onetomany_dialog');
dialog(dialogTitle, this.getDataModel(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
const dialog = this.createOrGetDialog('onetomany_dialog', serverInfo);
dialog(dialogTitle, this.getUISchema(attributes, tablesData), callback).resizeTo(this.pgBrowser.stdW.sm, this.pgBrowser.stdH.md);
}
}

View File

@@ -8,37 +8,22 @@
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Backgrid from 'sources/backgrid.pgadmin';
import Backform from 'sources/backform.pgadmin';
import Alertify from 'pgadmin.alertifyjs';
import $ from 'jquery';
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';
export function transformToSupported(data) {
/* Table fields */
data = _.pick(data, ['oid', 'name', 'schema', 'description', 'columns', 'primary_key', 'foreign_key']);
class EmptySchema extends BaseUISchema {
get baseFields() {
return [];
}
changeColumnOptions() {
/* Columns */
data['columns'] = data['columns'].map((column)=>{
return _.pick(column,[
'name','description','attowner','attnum','cltype','min_val_attlen','min_val_attprecision','max_val_attlen',
'max_val_attprecision', 'is_primary_key','attnotnull','attlen','attprecision','attidentity','colconstype',
'seqincrement','seqstart','seqmin','seqmax','seqcache','seqcycle',
]);
});
/* Primary key */
data['primary_key'] = data['primary_key'].map((primary_key)=>{
primary_key = _.pick(primary_key, ['columns']);
primary_key['columns'] = primary_key['columns'].map((column)=>{
return _.pick(column, ['column']);
});
return primary_key;
});
return data;
}
}
export default class TableDialog {
@@ -47,682 +32,81 @@ export default class TableDialog {
}
dialogName() {
return 'entity_dialog';
return 'table_dialog';
}
getDataModel(attributes, isNew, allTables, colTypes, schemas, sVersion) {
let dialogObj = this;
let columnsModel = this.pgBrowser.DataModel.extend({
idAttribute: 'attnum',
defaults: {
name: undefined,
description: undefined,
attowner: undefined,
attnum: undefined,
cltype: undefined,
min_val_attlen: undefined,
min_val_attprecision: undefined,
max_val_attlen: undefined,
max_val_attprecision: undefined,
is_primary_key: false,
attnotnull: false,
attlen: null,
attprecision: null,
attidentity: 'a',
colconstype: 'n',
seqincrement: undefined,
seqstart: undefined,
seqmin: undefined,
seqmax: undefined,
seqcache: undefined,
seqcycle: undefined,
},
initialize: function(attrs) {
if (_.size(attrs) !== 0) {
this.set({
'old_attidentity': this.get('attidentity'),
}, {silent: true});
}
dialogObj.pgBrowser.DataModel.prototype.initialize.apply(this, arguments);
getUISchema(attributes, isNew, tableNodesDict, colTypes, schemas) {
let treeNodeInfo = undefined;
if(!this.get('cltype') && colTypes.length > 0) {
this.set({
'cltype': colTypes[0]['value'],
}, {silent: true});
}
},
schema: [{
id: 'name', label: gettext('Name'), cell: 'string',
type: 'text', disabled: false,
cellHeaderClasses: 'width_percent_30',
editable: true,
}, {
// Need to show this field only when creating new table
// [in SubNode control]
id: 'is_primary_key', label: gettext('Primary key?'),
cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
deps: ['name'], cellHeaderClasses: 'width_percent_5',
options: {
onText: gettext('Yes'), offText: gettext('No'),
onColor: 'success', offColor: 'ternary',
},
visible: function () {
return true;
},
disabled: false,
editable: true,
}, {
id: 'description', label: gettext('Comment'), cell: 'string', type: 'multiline',
}, {
id: 'cltype', label: gettext('Data type'),
cell: 'select2',
type: 'select2', disabled: false,
control: 'select2',
cellHeaderClasses: 'width_percent_30',
select2: { allowClear: false, first_empty: false }, group: gettext('Definition'),
options: function () {
return colTypes;
},
}, {
id: 'attlen', label: gettext('Length/Precision'), cell: Backgrid.Extension.IntegerDepCell,
deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses: 'width_percent_20',
disabled: function (m) {
var of_type = m.get('cltype'),
flag = true;
_.each(colTypes, function (o) {
if (of_type == o.value) {
if (o.length) {
m.set('min_val_attlen', o.min_val, { silent: true });
m.set('max_val_attlen', o.max_val, { silent: true });
flag = false;
}
}
});
let columnSchema = new ColumnSchema(
()=>{},
treeNodeInfo,
()=>colTypes,
()=>[],
()=>[],
true,
);
flag && setTimeout(function () {
if (m.get('attlen')) {
m.set('attlen', null);
}
}, 10);
return flag;
},
editable: function (m) {
var of_type = m.get('cltype'),
flag = false;
_.each(colTypes, function (o) {
if (of_type == o.value) {
if (o.length) {
m.set('min_val_attlen', o.min_val, { silent: true });
m.set('max_val_attlen', o.max_val, { silent: true });
flag = true;
}
}
});
!flag && setTimeout(function () {
if (m.get('attlen')) {
m.set('attlen', null);
}
}, 10);
return flag;
},
}, {
id: 'attprecision', label: gettext('Scale'), cell: Backgrid.Extension.IntegerDepCell,
deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses: 'width_percent_20',
disabled: function (m) {
var of_type = m.get('cltype'),
flag = true;
_.each(colTypes, function (o) {
if (of_type == o.value) {
if (o.precision) {
m.set('min_val_attprecision', 0, { silent: true });
m.set('max_val_attprecision', o.max_val, { silent: true });
flag = false;
}
}
});
flag && setTimeout(function () {
if (m.get('attprecision')) {
m.set('attprecision', null);
}
}, 10);
return flag;
},
editable: function (m) {
if (!colTypes) {
// datatypes not loaded yet, may be this call is from CallByNeed from backgrid cell initialize.
return true;
}
var of_type = m.get('cltype'),
flag = false;
_.each(colTypes, function (o) {
if (of_type == o.value) {
if (o.precision) {
m.set('min_val_attprecision', 0, { silent: true });
m.set('max_val_attprecision', o.max_val, { silent: true });
flag = true;
}
}
});
!flag && setTimeout(function () {
if (m.get('attprecision')) {
m.set('attprecision', null);
}
}, 10);
return flag;
},
}, {
id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch',
type: 'switch', cellHeaderClasses: 'width_percent_20',
group: gettext('Constraints'),
options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'ternary' },
disabled: function(m) {
if (m.get('colconstype') == 'i') {
setTimeout(function () {
m.set('attnotnull', true);
}, 10);
}
return false;
},
}, {
id: 'colconstype',
label: gettext('Type'),
cell: 'string',
type: 'radioModern',
controlsClassName: 'pgadmin-controls col-12 col-sm-9',
controlLabelClassName: 'control-label col-sm-3 col-12',
group: gettext('Constraints'),
options: function() {
var opt_array = [
{'label': gettext('NONE'), 'value': 'n'},
{'label': gettext('IDENTITY'), 'value': 'i'},
];
if (sVersion >= 120000) {
opt_array.push({
'label': gettext('GENERATED'),
'value': 'g',
});
}
return opt_array;
},
disabled: false,
visible: function() {
if (sVersion >= 100000) {
return true;
}
return false;
},
}, {
id: 'attidentity', label: gettext('Identity'), control: 'select2',
cell: 'select2',
select2: {placeholder: 'Select identity', allowClear: false, width: '100%'},
group: gettext('Constraints'),
'options': [
{label: gettext('ALWAYS'), value: 'a'},
{label: gettext('BY DEFAULT'), value: 'd'},
],
deps: ['colconstype'],
visible: function(m) {
if (sVersion >= 100000 && m.isTypeIdentity(m)) {
return true;
}
return false;
},
disabled: function() {
return false;
},
}, {
id: 'seqincrement', label: gettext('Increment'), type: 'int',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
visible: 'isTypeIdentity',
},{
id: 'seqstart', label: gettext('Start'), type: 'int',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
disabled: function(m) {
let isIdentity = m.get('attidentity');
if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity))
return false;
return true;
}, deps: ['attidentity', 'colconstype'],
visible: 'isTypeIdentity',
},{
id: 'seqmin', label: gettext('Minimum'), type: 'int',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
visible: 'isTypeIdentity',
},{
id: 'seqmax', label: gettext('Maximum'), type: 'int',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
visible: 'isTypeIdentity',
},{
id: 'seqcache', label: gettext('Cache'), type: 'int',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
visible: 'isTypeIdentity',
},{
id: 'seqcycle', label: gettext('Cycled'), type: 'switch',
mode: ['properties', 'create', 'edit'], group: gettext('Constraints'),
deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn',
visible: 'isTypeIdentity',
}],
validate: function(keys) {
var msg = undefined;
// Nothing to validate
if (keys && keys.length == 0) {
this.errorModel.clear();
return null;
} else {
this.errorModel.clear();
}
if (_.isUndefined(this.get('name'))
|| String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Column name cannot be empty.');
this.errorModel.set('name', msg);
return msg;
}
if (_.isUndefined(this.get('cltype'))
|| String(this.get('cltype')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Column type cannot be empty.');
this.errorModel.set('cltype', msg);
return msg;
}
if (!_.isUndefined(this.get('cltype'))
&& !_.isUndefined(this.get('attlen'))
&& !_.isNull(this.get('attlen'))
&& this.get('attlen') !== '') {
// Validation for Length field
if (this.get('attlen') < this.get('min_val_attlen'))
msg = gettext('Length/Precision should not be less than: ') + this.get('min_val_attlen');
if (this.get('attlen') > this.get('max_val_attlen'))
msg = gettext('Length/Precision should not be greater than: ') + this.get('max_val_attlen');
// If we have any error set then throw it to user
if(msg) {
this.errorModel.set('attlen', msg);
return msg;
}
}
if (!_.isUndefined(this.get('cltype'))
&& !_.isUndefined(this.get('attprecision'))
&& !_.isNull(this.get('attprecision'))
&& this.get('attprecision') !== '') {
// Validation for precision field
if (this.get('attprecision') < this.get('min_val_attprecision'))
msg = gettext('Scale should not be less than: ') + this.get('min_val_attprecision');
if (this.get('attprecision') > this.get('max_val_attprecision'))
msg = gettext('Scale should not be greater than: ') + this.get('max_val_attprecision');
// If we have any error set then throw it to user
if(msg) {
this.errorModel.set('attprecision', msg);
return msg;
}
}
var minimum = this.get('seqmin'),
maximum = this.get('seqmax'),
start = this.get('seqstart');
if (!this.isNew() && this.get('colconstype') == 'i' &&
(this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') &&
(this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) {
if (_.isUndefined(this.get('seqincrement'))
|| String(this.get('seqincrement')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Increment value cannot be empty.');
this.errorModel.set('seqincrement', msg);
return msg;
} else {
this.errorModel.unset('seqincrement');
}
if (_.isUndefined(this.get('seqmin'))
|| String(this.get('seqmin')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Minimum value cannot be empty.');
this.errorModel.set('seqmin', msg);
return msg;
} else {
this.errorModel.unset('seqmin');
}
if (_.isUndefined(this.get('seqmax'))
|| String(this.get('seqmax')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Maximum value cannot be empty.');
this.errorModel.set('seqmax', msg);
return msg;
} else {
this.errorModel.unset('seqmax');
}
if (_.isUndefined(this.get('seqcache'))
|| String(this.get('seqcache')).replace(/^\s+|\s+$/g, '') == '') {
msg = gettext('Cache value cannot be empty.');
this.errorModel.set('seqcache', msg);
return msg;
} else {
this.errorModel.unset('seqcache');
}
}
var min_lt = gettext('Minimum value must be less than maximum value.'),
start_lt = gettext('Start value cannot be less than minimum value.'),
start_gt = gettext('Start value cannot be greater than maximum value.');
if (_.isEmpty(minimum) || _.isEmpty(maximum))
return null;
if ((minimum == 0 && maximum == 0) ||
(parseInt(minimum, 10) >= parseInt(maximum, 10))) {
this.errorModel.set('seqmin', min_lt);
return min_lt;
} else {
this.errorModel.unset('seqmin');
}
if (start && minimum && parseInt(start) < parseInt(minimum)) {
this.errorModel.set('seqstart', start_lt);
return start_lt;
} else {
this.errorModel.unset('seqstart');
}
if (start && maximum && parseInt(start) > parseInt(maximum)) {
this.errorModel.set('seqstart', start_gt);
return start_gt;
} else {
this.errorModel.unset('seqstart');
}
return null;
},
// Check whether the column is identity column or not
isIdentityColumn: function(m) {
let isIdentity = m.get('attidentity');
if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity))
return false;
return true;
},
// Check whether the column is a identity column
isTypeIdentity: function(m) {
let colconstype = m.get('colconstype');
if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'i') {
return true;
}
return false;
},
// Check whether the column is a generated column
isTypeGenerated: function(m) {
let colconstype = m.get('colconstype');
if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'g') {
return true;
}
return false;
},
});
const formatSchemaItem = function(opt) {
if (!opt.id) {
return opt.text;
}
var optimage = $(opt.element).data('image');
if (!optimage) {
return opt.text;
} else {
return $('<span></span>').append(
$('<span></span>', {
class: 'wcTabIcon ' + optimage,
})
).append($('<span></span>').text(opt.text));
}
};
let dialogModel = this.pgBrowser.DataModel.extend({
defaults: {
name: undefined,
schema: undefined,
description: undefined,
columns: [],
primary_key: [],
},
initialize: function() {
dialogObj.pgBrowser.DataModel.prototype.initialize.apply(this, arguments);
if(!this.get('schema') && schemas.length > 0) {
this.set({
'schema': schemas[0]['name'],
}, {silent: true});
}
},
schema: [{
id: 'name', label: gettext('Name'), type: 'text', disabled: false,
},{
id: 'schema', label: gettext('Schema'), type: 'text',
control: 'select2', select2: {
allowClear: false, first_empty: false,
templateResult: formatSchemaItem,
templateSelection: formatSchemaItem,
},
options: function () {
return schemas.map((schema)=>{
return {
'value': schema['name'],
'image': 'icon-schema',
'label': schema['name'],
};
});
},
filter: function(d) {
// If schema name start with pg_* then we need to exclude them
if(d && d.label.match(/^pg_/))
{
return false;
}
return true;
},
},{
id: 'description', label: gettext('Comment'), type: 'multiline',
},{
id: 'columns', label: gettext('Columns'), type: 'collection', mode: ['create'],
group: gettext('Columns'),
model: columnsModel,
subnode: columnsModel,
disabled: false,
uniqueCol : ['name'],
columns : ['name' , 'cltype', 'attlen', 'attprecision', 'attnotnull', 'is_primary_key'],
control: Backform.UniqueColCollectionControl.extend({
initialize: function() {
Backform.UniqueColCollectionControl.prototype.initialize.apply(this, arguments);
var self = this,
collection = self.model.get(self.field.get('name'));
if(collection.isEmpty()) {
self.last_attnum = -1;
} else {
var lastCol = collection.max(function(col) {
return col.get('attnum');
});
self.last_attnum = lastCol.get('attnum');
}
collection.on('change:is_primary_key', function(m) {
var primary_key_coll = self.model.get('primary_key'),
column_name = m.get('name'),
primary_key, primary_key_column_coll;
if(m.get('is_primary_key')) {
// Add column to primary key.
if (primary_key_coll.length < 1) {
primary_key = new (primary_key_coll.model)({}, {
top: self.model,
collection: primary_key_coll,
handler: primary_key_coll,
});
primary_key_coll.add(primary_key);
} else {
primary_key = primary_key_coll.first();
}
primary_key_column_coll = primary_key.get('columns');
var primary_key_column_exist = primary_key_column_coll.where({column:column_name});
if (primary_key_column_exist.length == 0) {
var primary_key_column = new (
primary_key_column_coll.model
)({column: column_name}, {
silent: true,
top: self.model,
collection: primary_key_coll,
handler: primary_key_coll,
});
primary_key_column_coll.add(primary_key_column);
}
primary_key_column_coll.trigger(
'pgadmin:multicolumn:updated', primary_key_column_coll
);
} else {
// remove column from primary key.
if (primary_key_coll.length > 0) {
primary_key = primary_key_coll.first();
// Do not alter existing primary key columns.
if (!_.isUndefined(primary_key.get('oid'))) {
return;
}
primary_key_column_coll = primary_key.get('columns');
var removedCols = primary_key_column_coll.where({column:column_name});
if (removedCols.length > 0) {
primary_key_column_coll.remove(removedCols);
_.each(removedCols, function(local_model) {
local_model.destroy();
});
if (primary_key_column_coll.length == 0) {
/* Ideally above line of code should be "primary_key_coll.reset()".
* But our custom DataCollection (extended from Backbone collection in datamodel.js)
* does not respond to reset event, it only supports add, remove, change events.
* And hence no custom event listeners/validators get called for reset event.
*/
primary_key_coll.remove(primary_key_coll.first());
}
}
primary_key_column_coll.trigger('pgadmin:multicolumn:updated', primary_key_column_coll);
}
}
});
collection.on('change:name', function(m) {
let primary_key = self.model.get('primary_key').first();
if(primary_key) {
let updatedCols = primary_key.get('columns').where(
{column: m.previous('name')}
);
if (updatedCols.length > 0) {
/*
* Table column name has changed so update
* column name in primary key as well.
*/
updatedCols[0].set(
{'column': m.get('name')},
{silent: true});
}
}
});
collection.on('remove', function(m) {
let primary_key = self.model.get('primary_key').first();
if(primary_key) {
let removedCols = primary_key.get('columns').where(
{column: m.get('name')}
);
primary_key.get('columns').remove(removedCols);
}
});
},
return new TableSchema(
{
relowner: [],
schema: schemas.map((schema)=>{
return {
'value': schema['name'],
'image': 'icon-schema',
'label': schema['name'],
};
}),
canAdd: true,
canEdit: true, canDelete: true,
// For each row edit/delete button enable/disable
canEditRow: true,
canDeleteRow: true,
allowMultipleEmptyRow: false,
beforeAdd: function(newModel) {
this.last_attnum++;
newModel.set('attnum', this.last_attnum);
return newModel;
},
},{
// Here we will create tab control for constraints
// We will hide the tab for ERD
type: 'nested', control: 'tab', group: gettext('Constraints'), mode: ['properties'],
schema: [{
id: 'primary_key', label: '',
model: this.pgBrowser.Nodes['primary_key'].model.extend({
validate: ()=>{},
}),
subnode: this.pgBrowser.Nodes['primary_key'].model.extend({
validate: ()=>{},
}),
editable: false, type: 'collection',
},
],
}],
validate: function() {
var msg,
name = this.get('name'),
schema = this.get('schema');
if (
_.isUndefined(name) || _.isNull(name) ||
String(name).replace(/^\s+|\s+$/g, '') == ''
) {
msg = gettext('Table name cannot be empty.');
this.errorModel.set('name', msg);
return msg;
}
/* Check existing table names */
let sameNameCount = _.filter(allTables, (table)=>table[0]==schema&&table[1]==name).length;
if(isNew && this.sessAttrs['name'] && sameNameCount > 0 || isNew && sameNameCount > 0) {
msg = gettext('Table name already exists.');
this.errorModel.set('name', msg);
return msg;
}
this.errorModel.unset('name');
if (
_.isUndefined(schema) || _.isNull(schema) ||
String(schema).replace(/^\s+|\s+$/g, '') == ''
) {
msg = gettext('Table schema cannot be empty.');
this.errorModel.set('schema', msg);
return msg;
}
this.errorModel.unset('schema');
return null;
spcname: [],
coll_inherits: [],
typname: [],
like_relation: [],
},
});
return new dialogModel(attributes);
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) {
createOrGetDialog(type, sVersion) {
const dialogName = this.dialogName();
if (!Alertify[dialogName]) {
@@ -731,19 +115,17 @@ export default class TableDialog {
`<div class="${dialogName}"></div>`,
null,
type,
$,
this.pgBrowser,
Alertify,
Backform
sVersion
);
});
}
return Alertify[dialogName];
}
show(title, attributes, isNew, allTables, colTypes, schemas, sVersion, callback) {
show(title, attributes, isNew, tableNodesDict, colTypes, schemas, serverInfo, callback) {
let dialogTitle = title || gettext('Unknown');
const dialog = this.createOrGetDialog('table_dialog');
dialog(dialogTitle, this.getDataModel(attributes, isNew, allTables, colTypes, schemas, sVersion), callback).resizeTo(this.pgBrowser.stdW.md, this.pgBrowser.stdH.md);
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);
}
}

View File

@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
import TableDialog, {transformToSupported as transformToSupportedTable} from './TableDialog';
import TableDialog from './TableDialog';
import OneToManyDialog from './OneToManyDialog';
import ManyToManyDialog from './ManyToManyDialog';
import pgBrowser from 'top/browser/static/js/browser';
@@ -15,7 +15,7 @@ import 'sources/backgrid.pgadmin';
import 'sources/backform.pgadmin';
export default function getDialog(dialogName) {
if(dialogName === 'entity_dialog') {
if(dialogName === 'table_dialog') {
return new TableDialog(pgBrowser);
} else if(dialogName === 'onetomany_dialog') {
return new OneToManyDialog(pgBrowser);
@@ -23,10 +23,3 @@ export default function getDialog(dialogName) {
return new ManyToManyDialog(pgBrowser);
}
}
export function transformToSupported(type, data) {
if(type == 'table') {
return transformToSupportedTable(data);
}
return data;
}

View File

@@ -12,7 +12,7 @@ import ReactDOM from 'react-dom';
import _ from 'lodash';
import BodyWidget from './ui_components/BodyWidget';
import getDialog, {transformToSupported} from './dialogs';
import getDialog from './dialogs';
import Alertify from 'pgadmin.alertifyjs';
import pgWindow from 'sources/window';
import pgAdmin from 'sources/pgadmin';
@@ -40,7 +40,6 @@ export default class ERDTool {
<BodyWidget
params={this.params}
getDialog={getDialog}
transformToSupported={transformToSupported}
pgWindow={pgWindow}
pgAdmin={pgAdmin}
panel={panel}

View File

@@ -188,7 +188,7 @@ export class TableNodeWidget extends React.Component {
render() {
let node_data = this.props.node.getData();
return (
<div className={'table-node ' + (this.props.node.isSelected() ? 'selected': '') } onDoubleClick={()=>{this.props.node.fireEvent({}, 'editNode');}}>
<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();}} />
{this.props.node.getNote() &&

View File

@@ -25,6 +25,7 @@ import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import {showERDSqlTool} from 'tools/datagrid/static/js/show_query_tool';
import 'wcdocker';
import Theme from '../../../../../../static/js/Theme';
/* Custom react-diagram action for keyboard events */
export class KeyboardShortcutAction extends Action {
@@ -76,6 +77,9 @@ export default class BodyWidget extends React.Component {
show_details: true,
is_new_tab: false,
preferences: {},
table_dialog_open: true,
oto_dialog_open: true,
otm_dialog_open: true,
};
this.diagram = new ERDCore();
/* Flag for checking if user has opted for save before close */
@@ -88,7 +92,7 @@ export default class BodyWidget extends React.Component {
this.keyboardActionObj = null;
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSaveAsDiagram', 'onSQLClick',
'onImageClick', 'onAddNewNode', 'onEditNode', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
'onNoteClose', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
'onDetailsToggle', 'onHelpClick'
]);
@@ -130,8 +134,8 @@ export default class BodyWidget extends React.Component {
'showNote': (event)=>{
this.showNote(event.node);
},
'editNode': (event) => {
this.addEditNode(event.node);
'editTable': (event) => {
this.addEditTable(event.node);
},
};
Object.keys(diagramEvents).forEach(eventName => {
@@ -150,7 +154,7 @@ export default class BodyWidget extends React.Component {
[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.onEditNode],
[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],
@@ -297,19 +301,20 @@ export default class BodyWidget extends React.Component {
}
getDialog(dialogName) {
if(dialogName === 'entity_dialog') {
let allTables = this.diagram.getModel().getNodes().map((node)=>{
return node.getSchemaTableName();
});
let serverInfo = {
type: this.props.params.server_type,
version: this.state.server_version,
};
if(dialogName === 'table_dialog') {
return (title, attributes, isNew, callback)=>{
this.props.getDialog(dialogName).show(
title, attributes, isNew, allTables, this.diagram.getCache('colTypes'), this.diagram.getCache('schemas'), this.state.server_version, callback
title, attributes, isNew, this.diagram.getModel().getNodesDict(), this.diagram.getCache('colTypes'), 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(), this.state.server_version, callback
title, attributes, this.diagram.getModel().getNodesDict(), serverInfo, callback
);
};
}
@@ -328,17 +333,20 @@ export default class BodyWidget extends React.Component {
}
}
addEditNode(node) {
let dialog = this.getDialog('entity_dialog');
addEditTable(node) {
let dialog = this.getDialog('table_dialog');
if(node) {
let [schema, table] = node.getSchemaTableName();
dialog(gettext('Table: %s (%s)', _.escape(table),_.escape(schema)), node.getData(), false, (newData)=>{
let oldData = node.getData();
node.setData(newData);
this.diagram.syncTableLinks(node, oldData);
this.diagram.repaint();
});
} else {
dialog(gettext('New table'), {name: this.diagram.getNextTableName()}, true, (newData)=>{
dialog(gettext('New table'), {}, true, (newData)=>{
let newNode = this.diagram.addNode(newData);
this.diagram.syncTableLinks(newNode);
newNode.setSelected(true);
});
}
@@ -353,15 +361,15 @@ export default class BodyWidget extends React.Component {
}
}
onEditNode() {
onEditTable() {
const selected = this.diagram.getSelectedNodes();
if(selected.length == 1) {
this.addEditNode(selected[0]);
this.addEditTable(selected[0]);
}
}
onAddNewNode() {
this.addEditNode();
this.addEditTable();
}
onCloneNode() {
@@ -385,10 +393,7 @@ export default class BodyWidget extends React.Component {
node.remove();
});
this.diagram.getSelectedLinks().forEach((link)=>{
link.getTargetPort().remove();
link.getSourcePort().remove();
link.setSelected(false);
link.remove();
this.diagram.removeOneToManyLink(link);
});
this.diagram.repaint();
},
@@ -656,10 +661,7 @@ export default class BodyWidget extends React.Component {
let dialog = this.getDialog('onetomany_dialog');
let initData = {local_table_uid: this.diagram.getSelectedNodes()[0].getID()};
dialog(gettext('One to many relation'), initData, (newData)=>{
let newLink = this.diagram.addLink(newData, 'onetomany');
this.diagram.clearSelection();
newLink.setSelected(true);
this.diagram.repaint();
this.diagram.addOneToManyLink(newData);
});
}
@@ -667,46 +669,7 @@ export default class BodyWidget extends React.Component {
let dialog = this.getDialog('manytomany_dialog');
let initData = {left_table_uid: this.diagram.getSelectedNodes()[0].getID()};
dialog(gettext('Many to many relation'), initData, (newData)=>{
let nodes = this.diagram.getModel().getNodesDict();
let left_table = nodes[newData.left_table_uid];
let right_table = nodes[newData.right_table_uid];
let tableData = {
name: `${left_table.getData().name}_${right_table.getData().name}`,
schema: left_table.getData().schema,
columns: [{
...left_table.getColumnAt(newData.left_table_column_attnum),
'name': `${left_table.getData().name}_${left_table.getColumnAt(newData.left_table_column_attnum).name}`,
'is_primary_key': false,
'attnum': 0,
},{
...right_table.getColumnAt(newData.right_table_column_attnum),
'name': `${right_table.getData().name}_${right_table.getColumnAt(newData.right_table_column_attnum).name}`,
'is_primary_key': false,
'attnum': 1,
}],
};
let newNode = this.diagram.addNode(tableData);
this.diagram.clearSelection();
newNode.setSelected(true);
let linkData = {
local_table_uid: newNode.getID(),
local_column_attnum: newNode.getColumns()[0].attnum,
referenced_table_uid: newData.left_table_uid,
referenced_column_attnum : newData.left_table_column_attnum,
};
this.diagram.addLink(linkData, 'onetomany');
linkData = {
local_table_uid: newNode.getID(),
local_column_attnum: newNode.getColumns()[1].attnum,
referenced_table_uid: newData.right_table_uid,
referenced_column_attnum : newData.right_table_column_attnum,
};
this.diagram.addLink(linkData, 'onetomany');
this.diagram.repaint();
this.diagram.addManyToManyLink(newData);
});
}
@@ -794,10 +757,7 @@ export default class BodyWidget extends React.Component {
try {
let response = await axios.get(url);
let tables = response.data.data.map((table)=>{
return this.props.transformToSupported('table', table);
});
this.diagram.deserializeData(tables);
this.diagram.deserializeData(response.data.data);
return true;
} catch (error) {
this.handleAxiosCatch(error);
@@ -809,7 +769,7 @@ export default class BodyWidget extends React.Component {
render() {
return (
<>
<Theme>
<ToolBar id="btn-toolbar">
<ButtonGroup>
<IconButton id="open-file" icon="fa fa-folder-open" onClick={this.onLoadDiagram} title={gettext('Load from file')}
@@ -828,7 +788,7 @@ export default class BodyWidget extends React.Component {
<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.onEditNode} title={gettext('Edit 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}/>
@@ -869,7 +829,7 @@ export default class BodyWidget extends React.Component {
<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()} />
</div>
</>
</Theme>
);
}
}
@@ -888,7 +848,6 @@ BodyWidget.propTypes = {
gen: PropTypes.bool.isRequired,
}),
getDialog: PropTypes.func.isRequired,
transformToSupported: PropTypes.func.isRequired,
pgWindow: PropTypes.object.isRequired,
pgAdmin: PropTypes.object.isRequired,
alertify: PropTypes.object.isRequired,

View File

@@ -205,3 +205,14 @@
}
}
}
.alertify {
.erd-dialog {
.ajs-body .ajs-content {
bottom: 0!important;
}
.ajs-footer {
display: none;
}
}
}