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

@@ -9,6 +9,8 @@ This release contains a number of bug fixes and new features since the release o
New features
************
| `Issue #6081 <https://redmine.postgresql.org/issues/6081>`_ - Added support for advanced table fields like the foreign key, primary key in the ERD tool.
| `Issue #6529 <https://redmine.postgresql.org/issues/6529>`_ - Added index creation when generating SQL in the ERD tool.
Housekeeping
************

View File

@@ -83,7 +83,7 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "4.0.0-alpha.58",
"@material-ui/pickers": "^3.2.10",
"@projectstorm/react-diagrams": "^6.4.2",
"@projectstorm/react-diagrams": "^6.6.1",
"@simonwep/pickr": "^1.5.1",
"@tippyjs/react": "^4.2.0",
"@types/classnames": "^2.2.6",

View File

@@ -19,7 +19,7 @@ export function getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser) {
}
export default class ColumnSchema extends BaseUISchema {
constructor(getPrivilegeRoleSchema, nodeInfo, cltypeOptions, collspcnameOptions) {
constructor(getPrivilegeRoleSchema, nodeInfo, cltypeOptions, collspcnameOptions, inErd=false) {
super({
name: undefined,
attowner: undefined,
@@ -57,6 +57,7 @@ export default class ColumnSchema extends BaseUISchema {
this.nodeInfo = nodeInfo;
this.cltypeOptions = cltypeOptions;
this.collspcnameOptions = collspcnameOptions;
this.inErd = inErd;
this.datatypes = [];
}
@@ -71,8 +72,7 @@ export default class ColumnSchema extends BaseUISchema {
return true;
}
if('schema' in this.nodeInfo)
{
if(this.nodeInfo && ('schema' in this.nodeInfo)) {
// We will disable control if it's system columns
// inheritedfrom check is useful when we use this schema in table node
// inheritedfrom has value then we should disable it
@@ -86,7 +86,7 @@ export default class ColumnSchema extends BaseUISchema {
// ie: it's position is less than 1
return !(!_.isUndefined(state.attnum) && state.attnum > 0);
}
return true;
return false;
}
editableCheckForTable(state) {
@@ -179,7 +179,7 @@ export default class ColumnSchema extends BaseUISchema {
) || (
'is_partitioned' in obj.top.origData
&& obj.top.origData['is_partitioned']
&& obj.nodeInfo.server && obj.nodeInfo.server.version < 11000
&& obj.getServerVersion() < 11000
))
) {
return true;
@@ -239,7 +239,7 @@ export default class ColumnSchema extends BaseUISchema {
filter: (options)=>{
let result = options;
let edit_types = state?.edit_types || [];
if(!obj.isNew(state)) {
if(!obj.isNew(state) && !this.inErd) {
result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1);
}
return result;
@@ -256,7 +256,7 @@ export default class ColumnSchema extends BaseUISchema {
filter: (options)=>{
let result = options;
let edit_types = row?.edit_types || [];
if(!obj.isNew(row)) {
if(!obj.isNew(row) && !this.inErd) {
result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1);
}
return result;
@@ -272,7 +272,10 @@ export default class ColumnSchema extends BaseUISchema {
id: 'inheritedfrom', label: gettext('Inherited from table'),
type: 'text', readonly: true, editable: false,
visible: function() {
return _.isUndefined(this.nodeInfo['table'] || this.nodeInfo['view'] || this.nodeInfo['mview']);
if(this.nodeInfo) {
return _.isUndefined(this.nodeInfo['table'] || this.nodeInfo['view'] || this.nodeInfo['mview']);
}
return false;
},
},{
id: 'attlen', label: gettext('Length/Precision'),
@@ -417,8 +420,7 @@ export default class ColumnSchema extends BaseUISchema {
{'label': gettext('IDENTITY'), 'value': 'i'},
];
if (this.nodeInfo && this.nodeInfo.server &&
this.nodeInfo.server.version >= 120000) {
if (this.getServerVersion() >= 120000) {
// You can't change the existing column to Generated column.
if (this.isNew(state)) {
options.push({
@@ -529,15 +531,18 @@ export default class ColumnSchema extends BaseUISchema {
], null, null, ['name', 'value']),
uniqueCol : ['name'], mode: ['edit', 'create'],
canAdd: true, canEdit: false, canDelete: true,
}, {
},{
id: 'security', label: gettext('Security'), type: 'group',
visible: !this.inErd,
},{
id: 'attacl', label: gettext('Privileges'), type: 'collection',
group: gettext('Security'),
group: 'security',
schema: this.getPrivilegeRoleSchema(['a','r','w','x']),
mode: ['edit'], canAdd: true, canDelete: true,
uniqueCol : ['grantee'],
},{
id: 'seclabels', label: gettext('Security labels'), canAdd: true,
schema: new SecLabelSchema(), group: gettext('Security'),
schema: new SecLabelSchema(), group: 'security',
mode: ['edit', 'create'], editable: false, type: 'collection',
min_version: 90100, canEdit: false, canDelete: true,
uniqueCol : ['provider'],

View File

@@ -86,7 +86,7 @@ class ForeignKeyHeaderSchema extends BaseUISchema {
}
}
class ForeignKeyColumnSchema extends BaseUISchema {
export class ForeignKeyColumnSchema extends BaseUISchema {
constructor() {
super({
local_column: undefined,
@@ -111,7 +111,7 @@ class ForeignKeyColumnSchema extends BaseUISchema {
}
export default class ForeignKeySchema extends BaseUISchema {
constructor(fieldOptions={}, nodeInfo, getColumns, initData={}) {
constructor(fieldOptions={}, nodeInfo, getColumns, initValues={}, inErd=false) {
super({
name: undefined,
reftab: undefined,
@@ -128,7 +128,7 @@ export default class ForeignKeySchema extends BaseUISchema {
autoindex: true,
coveringindex: undefined,
hasindex:undefined,
...initData,
...initValues,
});
this.nodeInfo = nodeInfo;
@@ -136,7 +136,7 @@ export default class ForeignKeySchema extends BaseUISchema {
this.fkHeaderSchema = new ForeignKeyHeaderSchema(fieldOptions, getColumns);
this.fkHeaderSchema.fkObj = this;
this.fkColumnSchema = new ForeignKeyColumnSchema();
this.inErd = inErd;
}
get idAttribute() {
@@ -240,9 +240,7 @@ export default class ForeignKeySchema extends BaseUISchema {
return true;
}
// If we are in table edit mode then
if(obj.inTable) {
return true;
} else if(state.hasindex) {
if(state.hasindex) {
return true;
}
return false;
@@ -252,7 +250,7 @@ export default class ForeignKeySchema extends BaseUISchema {
return {};
}
// If we are in table edit mode
if(obj.inTable) {
if(obj.inTable && !this.inErd) {
if(obj.isNew(state) && obj.top.isNew()) {
return {autoindex: false, coveringindex: ''};
}

View File

@@ -70,7 +70,7 @@ export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) {
}
export class ConstraintsSchema extends BaseUISchema {
constructor(nodeInfo, getFkObj, getExConsObj, otherOptions) {
constructor(nodeInfo, getFkObj, getExConsObj, otherOptions, inErd=false) {
super();
this.nodeInfo = nodeInfo;
this.primaryKeyObj = new PrimaryKeySchema({
@@ -81,13 +81,16 @@ export class ConstraintsSchema extends BaseUISchema {
spcname: otherOptions.spcname,
}, nodeInfo);
this.exConsObj = getExConsObj();
this.inErd = inErd;
}
changeColumnOptions(colOptions) {
this.primaryKeyObj.changeColumnOptions(colOptions);
this.fkObj.changeColumnOptions(colOptions);
this.uniqueConsObj.changeColumnOptions(colOptions);
this.exConsObj.changeColumnOptions(colOptions);
if(!this.inErd) {
this.uniqueConsObj.changeColumnOptions(colOptions);
this.exConsObj.changeColumnOptions(colOptions);
}
}
anyColumnAdded(state) {
@@ -147,20 +150,24 @@ export class ConstraintsSchema extends BaseUISchema {
return {foreign_key: []};
}
}
},{
id: 'check_group', type: 'group', label: gettext('Check'), visible: !this.inErd,
},{
id: 'check_constraint', label: '',
schema: new CheckConstraintSchema(),
editable: false, type: 'collection',
group: gettext('Check'), mode: ['edit', 'create'],
group: 'check_group', mode: ['edit', 'create'],
canEdit: true, canDelete: true, deps:['is_partitioned'],
canAdd: true,
columns : ['name', 'consrc'],
disabled: this.inCatalog,
},{
id: 'unique_group', type: 'group', label: gettext('Unique'), visible: !this.inErd,
},{
id: 'unique_constraint', label: '',
schema: this.uniqueConsObj,
editable: false, type: 'collection',
group: gettext('Unique'), mode: ['edit', 'create'],
group: 'unique_group', mode: ['edit', 'create'],
canEdit: true, canDelete: true, deps:['is_partitioned', 'typname'],
columns : ['name', 'columns'],
disabled: this.inCatalog,
@@ -176,11 +183,13 @@ export class ConstraintsSchema extends BaseUISchema {
return {unique_constraint: []};
}
}
},{
id: 'exclude_group', type: 'group', label: gettext('Exclude'), visible: !this.inErd,
},{
id: 'exclude_constraint', label: '',
schema: this.exConsObj,
editable: false, type: 'collection',
group: gettext('Exclude'), mode: ['edit', 'create'],
group: 'exclude_group', mode: ['edit', 'create'],
canEdit: true, canDelete: true, deps:['is_partitioned'],
columns : ['name', 'columns', 'constraint'],
disabled: this.inCatalog,
@@ -276,7 +285,7 @@ export class LikeSchema extends BaseUISchema {
export default class TableSchema extends BaseUISchema {
constructor(fieldOptions={}, nodeInfo, schemas={}, getPrivilegeRoleSchema=()=>{}, getColumns=()=>[],
getCollations=()=>[], getOperatorClass=()=>[], getAttachTables=()=>[], initValues={}) {
getCollations=()=>[], getOperatorClass=()=>[], getAttachTables=()=>[], initValues={}, inErd=false) {
super({
name: undefined,
oid: undefined,
@@ -307,6 +316,7 @@ export default class TableSchema extends BaseUISchema {
autovacuum_enabled: 'x',
primary_key: [],
foreign_key: [],
partition_keys: [],
partitions: [],
partition_type: 'range',
is_partitioned: false,
@@ -325,6 +335,24 @@ export default class TableSchema extends BaseUISchema {
this.columnsSchema = this.schemas.columns && this.schemas.columns() || {};
this.vacuumSettingsSchema = this.schemas.vacuum_settings && this.schemas.vacuum_settings() || {};
this.partitionKeysObj = new PartitionKeysSchema([], getCollations, getOperatorClass);
this.inErd = inErd;
}
static getErdSupportedData(data) {
let newData = {...data};
const SUPPORTED_KEYS = [
'name', 'schema', 'description', 'rlspolicy', 'forcerlspolicy', 'fillfactor',
'toast_tuple_target', 'parallel_workers', 'relhasoids', 'relpersistence',
'columns', 'primary_key', 'foreign_key',
];
newData = _.pick(newData, SUPPORTED_KEYS);
/* Remove inherited references */
newData.columns = newData.columns.map((c)=>{
delete c.inheritedfromtable;
return c;
});
return newData;
}
get idAttribute() {
@@ -397,9 +425,9 @@ export default class TableSchema extends BaseUISchema {
id: 'oid', label: gettext('OID'), type: 'text', mode: ['properties'],
},{
id: 'relowner', label: gettext('Owner'), type: 'select',
options: this.fieldOptions.relowner, noEmpty: true,
options: this.fieldOptions.relowner, noEmpty: this.inErd ? false : true,
mode: ['properties', 'create', 'edit'], controlProps: {allowClear: false},
readonly: this.inCatalog,
readonly: this.inCatalog, visible: !this.inErd,
},{
id: 'schema', label: gettext('Schema'), type: 'select',
options: this.fieldOptions.schema, noEmpty: true,
@@ -407,7 +435,7 @@ export default class TableSchema extends BaseUISchema {
readonly: this.inCatalog,
},{
id: 'spcname', label: gettext('Tablespace'),
visible: !this.inErd,
mode: ['properties', 'create', 'edit'], deps: ['is_partitioned'],
readonly: this.inCatalog, type: (state)=>{
return {
@@ -421,6 +449,9 @@ export default class TableSchema extends BaseUISchema {
id: 'partition', type: 'group', label: gettext('Partitions'),
mode: ['edit', 'create'], min_version: 100000,
visible: function(state) {
if(this.inErd) {
return false;
}
// Always show in case of create mode
if (obj.isNew(state) || state.is_partitioned)
return true;
@@ -429,7 +460,7 @@ export default class TableSchema extends BaseUISchema {
},{
id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch',
type: 'switch', mode: ['properties', 'create', 'edit'],
min_version: 100000,
min_version: 100000, visible: !this.inErd,
readonly: function(state) {
if (!obj.isNew(state))
return true;
@@ -447,7 +478,7 @@ export default class TableSchema extends BaseUISchema {
type: 'select', group: gettext('Columns'),
deps: ['typname', 'is_partitioned'], mode: ['create', 'edit'],
controlProps: { multiple: true, allowClear: false, placeholder: gettext('Select to inherit from...')},
options: this.fieldOptions.coll_inherits,
options: this.fieldOptions.coll_inherits, visible: !this.inErd,
optionsLoaded: (res)=>obj.inheritedTableList=res,
disabled: (state)=>{
if(state.adding_inherit_cols || state.is_partitioned){
@@ -581,7 +612,18 @@ export default class TableSchema extends BaseUISchema {
deps: ['typname', 'is_partitioned'],
depChange: (state, source, topState, actionObj)=>{
if(source[0] === 'columns') {
obj.changeColumnOptions(state.columns);
/* In ERD, attnum is an imp var for setting the links
Here, attnum is set to max avail value.
*/
let columns = state.columns;
if(actionObj.type === SCHEMA_STATE_ACTIONS.ADD_ROW && this.inErd) {
let lastAttnum = _.maxBy(columns, (c)=>c.attnum)?.attnum;
if(_.isUndefined(lastAttnum) || _.isNull(lastAttnum)) {
lastAttnum = -1;
}
columns[columns.length-1].attnum = lastAttnum + 1;
}
obj.changeColumnOptions(columns);
/* If primary key switch changes, primary key collection need to change */
if(actionObj.path.indexOf('is_primary_key') > -1) {
let tabColPath = _.slice(actionObj.path, 0, -1);
@@ -631,6 +673,7 @@ export default class TableSchema extends BaseUISchema {
},{
id: 'typname', label: gettext('Of type'), type: 'select',
mode: ['properties', 'create', 'edit'], group: 'advanced', deps: ['coll_inherits'],
visible: !this.inErd,
disabled: (state)=>{
if(!obj.inSchemaWithModelCheck(state) && isEmptyString(state.coll_inherits)) {
return false;
@@ -743,6 +786,7 @@ export default class TableSchema extends BaseUISchema {
type: 'nested-fieldset', label: gettext('Like'),
group: 'advanced', mode: ['create'],
schema: new LikeSchema(this.fieldOptions.like_relation),
visible: !this.inErd,
},{
id: 'partition_type', label:gettext('Partition Type'),
editable: false, type: 'select', controlProps: {allowClear: false},
@@ -774,6 +818,7 @@ export default class TableSchema extends BaseUISchema {
id: 'partition_keys', label:gettext('Partition Keys'),
schema: obj.partitionKeysObj,
editable: true, type: 'collection',
columns: ['key_type', 'pt_column', 'expression'].concat(!this.inErd ? ['collationame', 'op_class'] : []),
group: 'partition', mode: ['create'],
deps: ['is_partitioned', 'partition_type', 'typname'],
canEdit: false, canDelete: true,
@@ -885,28 +930,32 @@ export default class TableSchema extends BaseUISchema {
'</li></ul>',
].join(''),
min_version: 100000,
},
{
},{
type: 'group', id: 'parameters', label: gettext('Parameters'),
visible: !this.inErd,
},{
// Here - we will create tab control for storage parameters
// (auto vacuum).
type: 'nested-tab', group: gettext('Parameters'),
type: 'nested-tab', group: 'parameters',
mode: ['edit', 'create'], deps: ['is_partitioned'],
schema: this.vacuumSettingsSchema,
schema: this.vacuumSettingsSchema, visible: !this.inErd,
},{
id: 'security_group', type: 'group', label: gettext('Security'), visible: !this.inErd,
},
{
id: 'relacl_str', label: gettext('Privileges'), disabled: this.inCatalog,
type: 'text', mode: ['properties'], group: gettext('Security'),
type: 'text', mode: ['properties'], group: 'security_group',
},
{
id: 'relacl', label: gettext('Privileges'), type: 'collection',
group: gettext('Security'), schema: this.getPrivilegeRoleSchema(['a','r','w','d','D','x','t']),
group: 'security_group', schema: this.getPrivilegeRoleSchema(['a','r','w','d','D','x','t']),
mode: ['edit', 'create'], canAdd: true, canDelete: true,
uniqueCol : ['grantee'],
},{
id: 'seclabels', label: gettext('Security labels'), canEdit: false,
schema: new SecLabelSchema(), editable: false, canAdd: true,
type: 'collection', min_version: 90100, mode: ['edit', 'create'],
group: gettext('Security'), canDelete: true, control: 'unique-col-collection',
group: 'security_group', canDelete: true, control: 'unique-col-collection',
},{
id: 'vacuum_settings_str', label: gettext('Storage settings'),
type: 'multiline', group: 'advanced', mode: ['properties'],

View File

@@ -1125,7 +1125,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
elif part_type in data and data[part_type] == 'hash':
partition_scheme = 'HASH ('
for row in data[part_keys]:
for row in data.get(part_keys, []):
if row['key_type'] == 'column':
partition_scheme += self.qtIdent(
self.conn, row['pt_column'])
@@ -1141,7 +1141,7 @@ class BaseTableView(PGChildNodeView, BasePartitionTable, VacuumSettings):
partition_scheme += row['expression'] + ', '
# Remove extra space and comma
if len(data[part_keys]) > 0:
if len(data.get(part_keys, [])) > 0:
partition_scheme = partition_scheme[:-2]
partition_scheme += ')'

View File

@@ -205,6 +205,13 @@ export default function FormView({
if(modeSupported) {
let {group, CustomControl} = field;
if(field.type === 'group') {
groupLabels[field.id] = field.label;
if(!visible) {
schemaRef.current.filterGroups.push(field.label);
}
return;
}
group = groupLabels[group] || group || defaultTab;
if(!tabs[group]) tabs[group] = [];
@@ -261,11 +268,6 @@ export default function FormView({
} else {
tabs[group].push(<DataGridView {...props}/>);
}
} else if(field.type === 'group') {
groupLabels[field.id] = field.label;
if(!visible) {
schemaRef.current.filterGroups.push(field.label);
}
} else {
/* Its a form control */
const hasError = _.isEqual(accessPath.concat(field.id), stateUtils.formErr.name);

View File

@@ -89,7 +89,8 @@ function isValueEqual(val1, val2) {
/* If the orig value and new value are of different datatype but of same value(numeric) "no change" */
/* If the orig value is undefined or null and new value is boolean false "no change" */
if ((_.isEqual(val1, val2)
|| ((val1 === null || _.isUndefined(val1)) && !val2)
|| ((val1 === null || _.isUndefined(val1)) && val2 === '')
|| ((val1 === null || _.isUndefined(val1)) && typeof(val2) === 'boolean' && !val2)
|| (attrDefined ? _.isEqual(val1.toString(), val2.toString()) : false
))) {
return true;
@@ -98,22 +99,6 @@ function isValueEqual(val1, val2) {
}
}
function objectComparator(obj1, obj2) {
for(const key of _.union(Object.keys(obj1), Object.keys(obj2))) {
let equal = isValueEqual(obj1[key], obj2[key]);
if(equal) {
continue;
} else {
return false;
}
}
return true;
}
const diffArrayOptions = {
compareFunction: objectComparator,
};
function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, includeSkipChange=true) {
let changedData = {};
let isEdit = viewHelperProps.mode === 'edit';
@@ -173,13 +158,13 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i
);
change = {};
if(changeDiff.added.length > 0) {
change['added'] = cleanCid(changeDiff.added);
change['added'] = cleanCid(changeDiff.added, viewHelperProps.keepCid);
}
if(changeDiff.removed.length > 0) {
change['deleted'] = cleanCid(changeDiff.removed.map((row)=>{
/* Deleted records should be original, not the changed */
return _.find(_.get(origVal, field.id), ['cid', row.cid]);
}));
}), viewHelperProps.keepCid);
}
if(changeDiff.updated.length > 0) {
/* There is change in collection. Parse further to go deep */
@@ -204,7 +189,7 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i
});
}
}
change['changed'] = cleanCid(change['changed']);
change['changed'] = cleanCid(change['changed'], viewHelperProps.keepCid);
}
if(Object.keys(change).length > 0) {
attrChanged(field.id, change, true);
@@ -214,21 +199,18 @@ function getChangedData(topSchema, viewHelperProps, sessData, stringify=false, i
}
} else if(!isEdit) {
if(field.type === 'collection') {
/* For fixed rows, check the updated changes */
if(!_.isUndefined(field.fixedRows)) {
const changeDiff = diffArray(
_.get(origVal, field.id) || [],
_.get(sessVal, field.id) || [],
'cid',
diffArrayOptions
);
if(changeDiff.updated.length > 0) {
let change = cleanCid(_.get(sessVal, field.id));
attrChanged(field.id, change, true);
}
} else {
let change = cleanCid(_.get(sessVal, field.id));
attrChanged(field.id, change);
const changeDiff = diffArray(
_.get(origVal, field.id) || [],
_.get(sessVal, field.id) || [],
'cid',
);
/* For fixed rows, check only the updated changes */
if((!_.isUndefined(field.fixedRows) && changeDiff.updated.length > 0)
|| (_.isUndefined(field.fixedRows) && (
changeDiff.added.length > 0 || changeDiff.removed.length > 0 || changeDiff.updated.length > 0
))) {
let change = cleanCid(_.get(sessVal, field.id), viewHelperProps.keepCid);
attrChanged(field.id, change, true);
}
} else {
attrChanged(field.id);
@@ -409,18 +391,18 @@ const sessDataReducer = (state, action)=>{
};
/* Remove cid key added by prepareData */
function cleanCid(coll) {
if(!coll) {
function cleanCid(coll, keepCid=false) {
if(!coll || keepCid) {
return coll;
}
return coll.map((o)=>_.pickBy(o, (v, k)=>k!='cid'));
}
function prepareData(val) {
function prepareData(val, createMode=false) {
if(_.isPlainObject(val)) {
_.forIn(val, function (el) {
if (_.isObject(el)) {
prepareData(el);
prepareData(el, createMode);
}
});
} else if(_.isArray(val)) {
@@ -432,8 +414,8 @@ function prepareData(val) {
So to decide whether row is new or not set, the cid starts with
nn (not new) for existing rows. Newly added will start with 'c' (created)
*/
el['cid'] = _.uniqueId('nn');
prepareData(el);
el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn');
prepareData(el, createMode);
}
});
}
@@ -532,7 +514,7 @@ function SchemaDialogView({
});
} else {
/* Use the defaults as the initital data */
schema.origData = prepareData(schema.defaults);
schema.origData = prepareData(schema.defaults, true);
schema.initialise(schema.origData);
sessDispatch({
type: SCHEMA_STATE_ACTIONS.INIT,
@@ -624,6 +606,7 @@ function SchemaDialogView({
);
}
}).catch((err)=>{
console.error(err);
setFormErr({
name: 'apierror',
message: parseApiError(err),

View File

@@ -106,23 +106,6 @@ basicSettings = createMuiTheme(basicSettings, {
paddingRight: basicSettings.spacing(1.5),
}
},
MuiToggleButton: {
root: {
textTransform: 'none,',
padding: basicSettings.spacing(0.5, 2.5, 0.5, 0.5),
color: 'abc',
'&:hover':{
backgroundColor: 'abc',
},
'&$selected': {
color: 'abc',
backgroundColor: 'abc',
'&:hover':{
backgroundColor: 'abc',
}
}
}
},
MuiAccordion: {
root: {
boxShadow: 'none',
@@ -172,6 +155,17 @@ basicSettings = createMuiTheme(basicSettings, {
body1: {
fontSize: '1em',
}
},
MuiDialog: {
paper: {
margin: 0,
}
},
MuiTooltip: {
popper: {
top: 0,
zIndex: 9999,
}
}
},
transitions: {
@@ -374,6 +368,7 @@ function getFinalTheme(baseTheme) {
},
MuiToggleButton: {
root: {
padding: 'abc',
paddingRight: baseTheme.spacing(2.5),
paddingLeft: baseTheme.spacing(0.5),
color: 'abc',

View File

@@ -42,7 +42,7 @@
}
& .ajs-handle {
z-index: 5;
z-index: 1020;
}
}

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

View File

@@ -9,6 +9,7 @@
import ERDCore from 'pgadmin.tools.erd/erd_tool/ERDCore';
import * as createEngineLib from '@projectstorm/react-diagrams';
import TEST_TABLES_DATA from './test_tables';
import { FakeLink, FakeNode } from './fake_item';
describe('ERDCore', ()=>{
let eleFactory = jasmine.createSpyObj('nodeFactories', {
@@ -132,7 +133,8 @@ describe('ERDCore', ()=>{
});
it('addNode', ()=>{
let newNode = jasmine.createSpyObj('newNode', ['setPosition']);
let newNode = new FakeNode({});
spyOn(newNode, 'setPosition');
spyOn(erdCoreObj, 'getNewNode').and.returnValue(newNode);
spyOn(erdCoreObj, 'clearSelection');
@@ -151,33 +153,17 @@ describe('ERDCore', ()=>{
it('addLink', ()=>{
let node1 = new FakeNode({'name': 'table1'}, 'id1');
let node2 = new FakeNode({'name': 'table2'}, 'id2');
spyOn(node1, 'addPort').and.callThrough();
spyOn(node2, 'addPort').and.callThrough();
let nodesDict = {
'id1': {
serializeData: function(){ return {
'name': 'table1',
};},
getPortName: function(attnum) {
return `port-${attnum}`;
},
getPort: function() {
return null;
},
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
},
'id2': {
serializeData: function(){ return {
'name': 'table2',
};},
getPortName: function(attnum) {
return `port-${attnum}`;
},
getPort: function() {
return null;
},
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
},
'id1': node1,
'id2': node2,
};
let link = jasmine.createSpyObj('link', ['setSourcePort', 'setTargetPort']);
let link = new FakeLink();
spyOn(link, 'setSourcePort').and.callThrough();
spyOn(link, 'setTargetPort').and.callThrough();
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue(nodesDict);
spyOn(erdCoreObj, 'getNewLink').and.callFake(function() {
return link;
@@ -199,7 +185,6 @@ describe('ERDCore', ()=>{
expect(nodesDict['id2'].addPort).toHaveBeenCalledWith({name: 'port-3'});
expect(link.setSourcePort).toHaveBeenCalledWith({name: 'port-1'});
expect(link.setTargetPort).toHaveBeenCalledWith({name: 'port-3'});
});
it('serialize', ()=>{
@@ -222,41 +207,26 @@ describe('ERDCore', ()=>{
});
it('serializeData', ()=>{
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue({
'id1': {
serializeData: function(){ return {
'name': 'table1',
};},
},
'id2': {
serializeData: function(){ return {
'name': 'table2',
};},
},
});
let node1 = new FakeNode({'name': 'table1'}, 'id1');
let node2 = new FakeNode({'name': 'table2'}, 'id2');
let nodesDict = {
'id1': node1,
'id2': node2,
};
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue(nodesDict);
spyOn(erdEngine.getModel(), 'getLinks').and.returnValue([
{
serializeData: function(){ return {
'name': 'link1',
};},
getID: function(){ return 'lid1'; },
},
{
serializeData: function(){ return {
'name': 'link2',
};},
getID: function(){ return 'lid2'; },
},
new FakeLink({
'name': 'link1',
}, 'lid1'),
new FakeLink({
'name': 'link2',
}, 'lid2'),
]);
expect(JSON.stringify(erdCoreObj.serializeData())).toEqual(JSON.stringify({
nodes: {
'id1': {'name': 'table1'},
'id2': {'name': 'table2'},
},
links: {
'lid1': {'name': 'link1'},
'lid2': {'name': 'link2'},
},
}));
});
@@ -276,6 +246,9 @@ describe('ERDCore', ()=>{
addPort: function() {
},
getData: function() {
return table;
}
};
});
spyOn(erdEngine.getModel(), 'getNodesDict').and.returnValue(nodesDict);
@@ -288,11 +261,7 @@ describe('ERDCore', ()=>{
});
spyOn(erdCoreObj, 'getNewPort').and.returnValue({id: 'id'});
spyOn(erdCoreObj, 'addNode').and.callFake(function(data) {
return {
getID: function() {
return `id-${data.name}`;
},
};
return new FakeNode({}, `id-${data.name}`);
});
spyOn(erdCoreObj, 'addLink');
spyOn(erdCoreObj, 'dagreDistributeNodes');
@@ -319,8 +288,8 @@ describe('ERDCore', ()=>{
it('getNodesData', ()=>{
spyOn(erdEngine.getModel(), 'getNodes').and.returnValue([
{getData: function () {return {name:'node1'};}},
{getData: function () {return {name:'node2'};}},
new FakeNode({name:'node1'}),
new FakeNode({name:'node2'}),
]);
expect(JSON.stringify(erdCoreObj.getNodesData())).toEqual(JSON.stringify([
{name:'node1'}, {name:'node2'},

View File

@@ -0,0 +1,42 @@
import _ from 'lodash';
export class FakeNode {
constructor(data, id='nid1') {
this.data = data || {};
this.id = id;
}
setSelected() {}
getColumns() {return this.data.columns;}
getID() {return this.id;}
setData(data) {this.data = data;}
getData() {return this.data;}
getPosition() {return {x: 30, y: 30};}
setPosition() {}
serializeData() {return this.getData();}
getPortName(attnum) {return `port-${attnum}`;}
getPort() {return null;}
addPort(obj) {return obj;}
getColumnAt(pos) {return _.find(this.getColumns()||[], (c)=>c.attnum==pos);}
remove() {}
getSchemaTableName() {return [this.data.schema, this.data.name];}
cloneData(tabName) {
let retVal = {...this.data};
retVal.name = tabName;
return retVal;
}
}
export class FakeLink {
constructor(data, id='lid1') {
this.data = data;
this.id = id;
}
setSelected() {}
getID() {return this.id;}
getData() {return this.data;}
getSourcePort() {return {remove: ()=>{}};}
setSourcePort() {}
getTargetPort() {return {remove: ()=>{}};}
setTargetPort() {}
remove() {}
}

View File

@@ -10,6 +10,7 @@ import * as erdModule from 'pgadmin.tools.erd/erd_module';
import erdPref from './erd_preferences';
import BodyWidget from 'pgadmin.tools.erd/erd_tool/ui_components/BodyWidget';
import * as ERDSqlTool from 'tools/datagrid/static/js/show_query_tool';
import { FakeLink, FakeNode } from '../fake_item';
let pgAdmin = {
Browser: {
@@ -60,7 +61,7 @@ let mtmDialog = jasmine.createSpyObj('mtmDialog', ['show']);
let getDialog = (dialogName)=>{
switch(dialogName) {
case 'entity_dialog': return tableDialog;
case 'table_dialog': return tableDialog;
case 'onetomany_dialog': return otmDialog;
case 'manytomany_dialog': return mtmDialog;
}
@@ -93,19 +94,16 @@ describe('ERD BodyWidget', ()=>{
title: 'postgres/postgres@PostgreSQL 12',
trans_id: 110008,
};
let newNode = new FakeNode({
columns: [{attnum: 0}, {attnum: 1}],
}, 'newid1');
beforeAll(()=>{
spyOn(erdModule, 'setPanelTitle');
spyOn(ERDCore.prototype, 'repaint');
spyOn(ERDCore.prototype, 'deserializeData');
spyOn(ERDCore.prototype, 'addNode').and.returnValue({
setSelected: ()=>{},
getColumns: ()=>([{attnum: 0}, {attnum: 1}]),
getID: ()=>'newid1',
});
spyOn(ERDCore.prototype, 'addLink').and.returnValue({
setSelected: ()=>{},
});
spyOn(ERDCore.prototype, 'addNode').and.returnValue(newNode);
spyOn(ERDCore.prototype, 'addLink').and.returnValue(new FakeLink());
spyOn(alertify, 'confirm').and.callFake((arg1, arg2, okCallback)=>{
okCallback();
});
@@ -128,7 +126,7 @@ describe('ERD BodyWidget', ()=>{
beforeEach(()=>{
jasmineEnzyme();
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} transformToSupported={()=>{}} alertify={alertify}/>);
body = mount(<BodyWidget params={params} pgAdmin={pgAdmin} pgWindow={pgWindow} getDialog={getDialog} alertify={alertify}/>);
bodyInstance = body.instance();
});
@@ -225,18 +223,18 @@ describe('ERD BodyWidget', ()=>{
});
});
it('event editNode', (done)=>{
it('event editTable', (done)=>{
let node = {key: 'value', getNote: ()=>'a note'};
spyOn(bodyInstance, 'addEditNode');
bodyInstance.diagram.fireEvent({node: node}, 'editNode', true);
spyOn(bodyInstance, 'addEditTable');
bodyInstance.diagram.fireEvent({node: node}, 'editTable', true);
setTimeout(()=>{
expect(bodyInstance.addEditNode).toHaveBeenCalledWith(node);
expect(bodyInstance.addEditTable).toHaveBeenCalledWith(node);
done();
});
});
it('getDialog', ()=>{
bodyInstance.getDialog('entity_dialog')();
bodyInstance.getDialog('table_dialog')();
expect(tableDialog.show).toHaveBeenCalled();
bodyInstance.getDialog('onetomany_dialog')();
@@ -246,10 +244,20 @@ describe('ERD BodyWidget', ()=>{
expect(mtmDialog.show).toHaveBeenCalled();
});
it('addEditNode', ()=>{
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');
let nodesDict = {
'id1': node1,
'id2': node2,
};
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
'getNodesDict': ()=>nodesDict,
});
spyOn(bodyInstance.diagram, 'addLink');
/* New */
tableDialog.show.calls.reset();
bodyInstance.addEditNode();
bodyInstance.addEditTable();
expect(tableDialog.show).toHaveBeenCalled();
let saveCallback = tableDialog.show.calls.mostRecent().args[7];
@@ -259,12 +267,9 @@ describe('ERD BodyWidget', ()=>{
/* Existing */
tableDialog.show.calls.reset();
let node = jasmine.createSpyObj('node',{
getSchemaTableName: ['erd1', 'table1'],
setData: null,
getData: null,
});
bodyInstance.addEditNode(node);
let node = new FakeNode({name: 'table1', schema: 'erd1'});
spyOn(node, 'setData');
bodyInstance.addEditTable(node);
expect(tableDialog.show).toHaveBeenCalled();
saveCallback = tableDialog.show.calls.mostRecent().args[7];
@@ -273,49 +278,44 @@ describe('ERD BodyWidget', ()=>{
expect(node.setData).toHaveBeenCalledWith(newData);
});
it('onEditNode', ()=>{
it('onEditTable', ()=>{
let node = {key: 'value'};
spyOn(bodyInstance, 'addEditNode');
spyOn(bodyInstance, 'addEditTable');
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
bodyInstance.onEditNode();
expect(bodyInstance.addEditNode).toHaveBeenCalledWith(node);
bodyInstance.onEditTable();
expect(bodyInstance.addEditTable).toHaveBeenCalledWith(node);
});
it('onAddNewNode', ()=>{
spyOn(bodyInstance, 'addEditNode');
spyOn(bodyInstance, 'addEditTable');
bodyInstance.onAddNewNode();
expect(bodyInstance.addEditNode).toHaveBeenCalled();
expect(bodyInstance.addEditTable).toHaveBeenCalled();
});
it('onCloneNode', ()=>{
let node = jasmine.createSpyObj('node',{
getSchemaTableName: ['erd1', 'table1'],
setData: null,
getData: null,
cloneData: {key: 'value'},
getPosition: {x: 30, y: 30},
});
let node = new FakeNode({name: 'table1', schema: 'erd1'});
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
spyOn(bodyInstance.diagram, 'getNextTableName').and.returnValue('newtable1');
bodyInstance.diagram.addNode.calls.reset();
bodyInstance.onCloneNode();
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith({key: 'value'}, [50, 50]);
let cloneArgs = bodyInstance.diagram.addNode.calls.argsFor(0);
expect(cloneArgs[0]).toEqual(jasmine.objectContaining({
name: 'newtable1',
schema: 'erd1',
}));
expect(cloneArgs[1]).toEqual([50, 50]);
});
it('onDeleteNode', (done)=>{
let node = jasmine.createSpyObj('node',{
getSchemaTableName: ['erd1', 'table1'],
setData: null,
getData: null,
cloneData: {key: 'value'},
getPosition: {x: 30, y: 30},
remove: null,
setSelected: null,
});
let link = jasmine.createSpyObj('link', {
remove: null,
setSelected: null,
getTargetPort: jasmine.createSpyObj('port', ['remove']),
getSourcePort: jasmine.createSpyObj('port', ['remove']),
let node = new FakeNode({name: 'table1', schema: 'erd1'});
spyOn(node, 'remove');
let link = new FakeLink({local_table_uid: 'tid1'});
spyOn(link, 'remove');
let nodesDict = {
'tid1': node
};
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
'getNodesDict': ()=>nodesDict,
});
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
spyOn(bodyInstance.diagram, 'getSelectedLinks').and.returnValue([link]);
@@ -413,9 +413,17 @@ describe('ERD BodyWidget', ()=>{
});
it('onOneToManyClick', ()=>{
let node = jasmine.createSpyObj('node',{
getID: 'id1',
let node = new FakeNode({}, 'id1');
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 = {
'id1': node1,
'id2': node2,
};
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
'getNodesDict': ()=>nodesDict,
});
spyOn(bodyInstance.diagram, 'addLink');
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
otmDialog.show.calls.reset();
@@ -423,15 +431,18 @@ describe('ERD BodyWidget', ()=>{
expect(otmDialog.show).toHaveBeenCalled();
let saveCallback = otmDialog.show.calls.mostRecent().args[4];
let newData = {key: 'value'};
let newData = {
local_table_uid: 'id1',
local_column_attnum: 1,
referenced_table_uid: 'id2',
referenced_column_attnum: 2,
};
saveCallback(newData);
expect(bodyInstance.diagram.addLink).toHaveBeenCalledWith(newData, 'onetomany');
});
it('onManyToManyClick', ()=>{
let node = jasmine.createSpyObj('node',{
getID: 'id1',
});
let node = new FakeNode({}, 'id1');
spyOn(bodyInstance.diagram, 'getSelectedNodes').and.returnValue([node]);
mtmDialog.show.calls.reset();
@@ -439,19 +450,12 @@ describe('ERD BodyWidget', ()=>{
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 = {
'id1': {
getID: ()=>'id1',
getData: ()=>({name: 'table1', schema: 'erd1'}),
getColumnAt: ()=>({name: 'col1', type: 'type1', attnum: 0}),
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
},
'id2': {
getID: ()=>'id2',
getData: ()=>({name: 'table2', schema: 'erd2'}),
getColumnAt: ()=>({name: 'col2', type: 'type2', attnum: 1}),
addPort: jasmine.createSpy('addPort').and.callFake((obj)=>obj),
},
'id1': node1,
'id2': node2,
'newid1': newNode,
};
spyOn(bodyInstance.diagram, 'getModel').and.returnValue({
'getNodesDict': ()=>nodesDict,
@@ -468,24 +472,21 @@ describe('ERD BodyWidget', ()=>{
bodyInstance.diagram.addNode.calls.reset();
bodyInstance.diagram.addLink.calls.reset();
saveCallback(newData);
expect(bodyInstance.diagram.addNode).toHaveBeenCalledWith({
let tableData = bodyInstance.diagram.addNode.calls.argsFor(0)[0];
expect(tableData).toEqual(jasmine.objectContaining({
name: 'table1_table2',
schema: 'erd1',
columns: [
{
type: 'type1',
name: 'table1_col1',
is_primary_key: false,
attnum: 0,
},
{
type: 'type2',
name: 'table2_col2',
is_primary_key: false,
attnum: 1,
},
],
});
}));
expect(tableData.columns[0]).toEqual(jasmine.objectContaining({
type: 'type1',
name: 'table1_col1',
attnum: 0,
}));
expect(tableData.columns[1]).toEqual(jasmine.objectContaining({
type: 'type2',
name: 'table2_col2',
attnum: 1,
}));
let linkData = {
local_table_uid: 'newid1',

View File

@@ -202,13 +202,13 @@ describe('ColumnSchema', ()=>{
expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(true);
schemaObj.nodeInfo = {};
expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(true);
expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(false);
});
it('editableCheckForTable', ()=>{
let state = {};
schemaObj.nodeInfo = {};
expect(schemaObj.editableCheckForTable(state)).toBe(false);
expect(schemaObj.editableCheckForTable(state)).toBe(true);
});
it('depChange', ()=>{

View File

@@ -1464,50 +1464,50 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.0.tgz#32e63212293dd3efbb521cd35a5020ab66eaa546"
integrity sha512-wjtKehFAIARq2OxK8j3JrggNlEslJfNuSm2ArteIbKyRMts2g0a7KzTxfRVNUM+O0gnBJ2hNV8nWPOYBgI1sew==
"@projectstorm/geometry@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/geometry/-/geometry-6.5.2.tgz#76ccc7280a49c64953036aa96287f408981e5913"
integrity sha512-PGrcMXr6CkdH1DvcY+MDYzg6suWnOXehP8S69DhiEeWUFmQhKJSCAPEovdQOpohL6urlKaSNbDM8auUypFbTfw==
"@projectstorm/geometry@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/geometry/-/geometry-6.6.1.tgz#4a42f5c8fdfcc3d951e73f5db7fe9546514acc3d"
integrity sha512-gWRkv+fm+VIpoffHzDHPmGYlEqx8xWGfE/JR7TXAZweNdjEIxyhT++hVlCJiFJS+/cGqgN3z+eP7PNjwZUPGRg==
"@projectstorm/react-canvas-core@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/react-canvas-core/-/react-canvas-core-6.5.2.tgz#65726df39c15487d9c6b23616cfa6b182d82fcbe"
integrity sha512-tFfWpaPZ71lMDFRL69jRFTjmYit6FKNpkAiI9sqYeH2I4Y7OjuxmCqYomlzh8ZSLVwKHtcI6Vd7hhoatEBupEQ==
"@projectstorm/react-canvas-core@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/react-canvas-core/-/react-canvas-core-6.6.1.tgz#93cc6e70e986fe620b6fad6b597d3aa038075244"
integrity sha512-wAxEh4Wja2Au0QAuLqxJNaWpVxYIffTFUZhjH8wtW8MKCWS6W9RnP6upeC8QVQi29NS59UIX4wXNzb6e6ht5ww==
dependencies:
"@projectstorm/geometry" "^6.5.2"
"@projectstorm/geometry" "^6.6.1"
"@projectstorm/react-diagrams-core@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-core/-/react-diagrams-core-6.5.2.tgz#4c8c92ed288b2934de11c1a52fda72249b151932"
integrity sha512-7ganJGs7lcRB1sTvUs+MQ9RiSCGD+IHgISo45aS81+XFllO8u7uZRaPdIyObhz+khwZvMk2pWmXYPNJwGThOZQ==
"@projectstorm/react-diagrams-core@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-core/-/react-diagrams-core-6.6.1.tgz#f626c6253dd6f4b04c3256976b55377ffac2b1bb"
integrity sha512-TiDwpcH+t2b2tG/UHDQvhlyx3gOQRJXxTyNDo7p+430ikTrvz1f8uNe5Rt3SrzyqxeUazLFXYBgbGpEanqOykQ==
dependencies:
"@projectstorm/geometry" "^6.5.2"
"@projectstorm/react-canvas-core" "^6.5.2"
"@projectstorm/geometry" "^6.6.1"
"@projectstorm/react-canvas-core" "^6.6.1"
"@projectstorm/react-diagrams-defaults@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-defaults/-/react-diagrams-defaults-6.5.2.tgz#084138e8b2ac2643bcfa115fb543f69c0eaf88f3"
integrity sha512-8+hAe88Lj7uBsugAT7f5SsjqxmjYfeBYwViaFpnIilmuYF/uXN0w/ns8Mcq2FE49wGOXtBoxFusoWmDWwgE+fQ==
"@projectstorm/react-diagrams-defaults@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-defaults/-/react-diagrams-defaults-6.6.1.tgz#a97cdda56d7336b84775f76a925b90286ea2833d"
integrity sha512-FJu8BNBjvANVZ8N99WXS/f6Mu5/yMC4Pi55kTG3vq7o14tsVMcghosmxst5eoeL251O4I+ulNvQ/yCc1Mc5org==
dependencies:
"@projectstorm/react-diagrams-core" "^6.5.2"
"@projectstorm/react-diagrams-core" "^6.6.1"
"@projectstorm/react-diagrams-routing@^6.5.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-routing/-/react-diagrams-routing-6.5.2.tgz#824dadbcd8c14c647829bdd4761afef69dec5622"
integrity sha512-pWsDmk/4hbx6Ii2Z3Uvd9dZ/xB9F7u66mGVGMWb3txxTT68Y6mwdVJRQFu/frm/Sk3sZv7gNf1EqadsilIh7vg==
"@projectstorm/react-diagrams-routing@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams-routing/-/react-diagrams-routing-6.6.1.tgz#06b77ab1f94f4567391099ffe5e1d91db34408ff"
integrity sha512-m8akJynhanxmpc/A2U7bcgFxIMxsjb3zmYBRGFltVJve87mir8ACaH2gmiHYcAfgEHxdh+x7mCuUlfNP242Ytw==
dependencies:
"@projectstorm/geometry" "^6.5.2"
"@projectstorm/react-diagrams-core" "^6.5.2"
"@projectstorm/react-diagrams-defaults" "^6.5.2"
"@projectstorm/geometry" "^6.6.1"
"@projectstorm/react-diagrams-core" "^6.6.1"
"@projectstorm/react-diagrams-defaults" "^6.6.1"
"@projectstorm/react-diagrams@^6.4.2":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams/-/react-diagrams-6.5.2.tgz#3f65a903b1853e51d68109a513d91d190d278864"
integrity sha512-1xlz1jVffWq5R7Oqj/BEY06Cf+2uLV5eEY6pGxuGFfp5SXeTNCSIS/khkbFou707Gor3D8MOqdBpZvF2NfoLmA==
"@projectstorm/react-diagrams@^6.6.1":
version "6.6.1"
resolved "https://registry.yarnpkg.com/@projectstorm/react-diagrams/-/react-diagrams-6.6.1.tgz#3af4007613f487ba58801cdcded955683b3f01c5"
integrity sha512-tLSXfEf/dGFUN8JCCRMrYyIBhhn+eVw24xQodmtcReJxQpKa31EWh9CmJ6UEg7xUnabMG9f2plOPyJqyFssGTA==
dependencies:
"@projectstorm/react-diagrams-core" "^6.5.2"
"@projectstorm/react-diagrams-defaults" "^6.5.2"
"@projectstorm/react-diagrams-routing" "^6.5.2"
"@projectstorm/react-diagrams-core" "^6.6.1"
"@projectstorm/react-diagrams-defaults" "^6.6.1"
"@projectstorm/react-diagrams-routing" "^6.6.1"
"@simonwep/pickr@^1.5.1":
version "1.8.1"