mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-23 15:03:26 -06:00
Added React framework for the properties dialog and port Server Group, Server, and Database dialogs.
Following changes done for the framework: - Framework for creating React based dynamic form view out of a pre-defined UI schema. Previously, it was based on Backform/Backbone. - The new framework and components will use MaterialUI as the base. Previously, Bootstrap/Backform/jQuery components were used. - The new code uses JSS instead of CSS since material UI and most modern React libraries also use JSS. In the future, this will allow us to change the theme in real-time without refresh. - 90% code covered by 80-85 new jasmine test cases. - Server group node UI Schema migration to new, with schema test cases. - Server node UI Schema migration to new, with schema test cases. - Database node UI Schema migration to new, with schema test cases. - Few other UI changes. Fixes #6130
This commit is contained in:
parent
a10b0c7786
commit
764677431f
@ -18,6 +18,7 @@ New features
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
| `Issue #6130 <https://redmine.postgresql.org/issues/6130>`_ - Added React framework for the properties dialog and port server group, server, and database dialogs.
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
@ -13,8 +13,10 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@emotion/core": "^10.0.14",
|
||||
"@emotion/memoize": "^0.7.5",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@emotion/styled": "^10.0.14",
|
||||
"@emotion/utils": "^1.0.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
|
||||
"autoprefixer": "^10.2.4",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
@ -48,7 +50,7 @@
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"mini-css-extract-plugin": "^1.3.5",
|
||||
"popper.js": "^1.14.7",
|
||||
"popper.js": "^1.16.1",
|
||||
"postcss-loader": "^5.0.0",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.7.2",
|
||||
@ -68,7 +70,12 @@
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@emotion/sheet": "^1.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@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",
|
||||
"@simonwep/pickr": "^1.5.1",
|
||||
"@tippyjs/react": "^4.2.0",
|
||||
@ -90,6 +97,7 @@
|
||||
"css-loader": "^5.0.1",
|
||||
"cssnano": "^5.0.2",
|
||||
"dagre": "^0.8.4",
|
||||
"diff-arrays-of-objects": "^1.1.8",
|
||||
"dropzone": "^5.7.4",
|
||||
"html2canvas": "^1.0.0-rc.7",
|
||||
"immutability-helper": "^3.0.0",
|
||||
@ -114,12 +122,15 @@
|
||||
"raf": "^3.4.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-select": "^4.2.1",
|
||||
"react-table": "^7.6.3",
|
||||
"select2": "^4.0.13",
|
||||
"shim-loader": "^1.0.1",
|
||||
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",
|
||||
"snapsvg-cjs": "^0.0.6",
|
||||
"socket.io-client": "^4.0.0",
|
||||
"split.js": "^1.5.10",
|
||||
"styled-components": "^5.2.1",
|
||||
"tablesorter": "^2.31.2",
|
||||
"tempusdominus-bootstrap-4": "^5.1.2",
|
||||
"tempusdominus-core": "^5.0.3",
|
||||
|
@ -7,6 +7,11 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../static/js/node_ajax';
|
||||
import { getNodePrivilegeRoleSchema } from '../../../static/js/privilege.ui';
|
||||
import { getNodeVariableSchema } from '../../../static/js/variable.ui';
|
||||
import DatabaseSchema from './database.ui';
|
||||
|
||||
define('pgadmin.node.database', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||
'sources/utils', 'sources/pgadmin', 'pgadmin.browser.utils',
|
||||
@ -308,30 +313,56 @@ define('pgadmin.node.database', [
|
||||
pgBrowser.Node.callbacks.refresh.apply(this, arguments);
|
||||
},
|
||||
},
|
||||
getSchema: function(treeNodeInfo, itemNodeData) {
|
||||
return new DatabaseSchema(
|
||||
()=>getNodeVariableSchema(this, treeNodeInfo, itemNodeData, false, true),
|
||||
(privileges)=>getNodePrivilegeRoleSchema(this, treeNodeInfo, itemNodeData, privileges),
|
||||
{
|
||||
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
|
||||
encoding:
|
||||
()=>getNodeAjaxOptions('get_encodings', this, treeNodeInfo, itemNodeData, {
|
||||
cacheLevel: 'server',
|
||||
}),
|
||||
template:
|
||||
()=>getNodeAjaxOptions('get_databases', this, treeNodeInfo, itemNodeData, {
|
||||
cacheLevel: 'server',
|
||||
}, (data)=>{
|
||||
let res = [];
|
||||
if (data && _.isArray(data)) {
|
||||
_.each(data, function(d) {
|
||||
res.push({label: d, value: d,
|
||||
image: 'pg-icon-database'});
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
spcname:
|
||||
()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, (m)=>{
|
||||
return (m.label != 'pg_global');
|
||||
}),
|
||||
datcollate:
|
||||
()=>getNodeAjaxOptions('get_ctypes', this, treeNodeInfo, itemNodeData, {
|
||||
cacheLevel: 'server',
|
||||
}),
|
||||
datctype:
|
||||
()=>getNodeAjaxOptions('get_ctypes', this, treeNodeInfo, itemNodeData, {
|
||||
cacheLevel: 'server',
|
||||
}),
|
||||
},
|
||||
{
|
||||
datowner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name,
|
||||
}
|
||||
);
|
||||
},
|
||||
/* Few fields are kept since the properties tab for collection is not
|
||||
yet migrated to new react schema. Once the properties for collection
|
||||
is removed, remove this model */
|
||||
model: pgBrowser.Node.Model.extend({
|
||||
idAttribute: 'did',
|
||||
defaults: {
|
||||
name: undefined,
|
||||
owner: undefined,
|
||||
is_sys_obj: undefined,
|
||||
comment: undefined,
|
||||
encoding: 'UTF8',
|
||||
template: undefined,
|
||||
tablespace: undefined,
|
||||
collation: undefined,
|
||||
char_type: undefined,
|
||||
datconnlimit: -1,
|
||||
datallowconn: undefined,
|
||||
variables: [],
|
||||
privileges: [],
|
||||
securities: [],
|
||||
datacl: [],
|
||||
deftblacl: [],
|
||||
deffuncacl: [],
|
||||
defseqacl: [],
|
||||
is_template: false,
|
||||
deftypeacl: [],
|
||||
schema_res:'',
|
||||
},
|
||||
|
||||
// Default values!
|
||||
@ -356,187 +387,11 @@ define('pgadmin.node.database', [
|
||||
id: 'datowner', label: gettext('Owner'),
|
||||
editable: false, type: 'text', node: 'role',
|
||||
control: Backform.NodeListByNameControl, select2: { allowClear: false },
|
||||
},{
|
||||
id: 'acl', label: gettext('Privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'tblacl', label: gettext('Default TABLE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'seqacl', label: gettext('Default SEQUENCE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'funcacl', label: gettext('Default FUNCTION privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'typeacl', label: gettext('Default TYPE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'], min_version: 90200,
|
||||
},{
|
||||
id: 'is_sys_obj', label: gettext('System database?'),
|
||||
cell:'boolean', type: 'switch', mode: ['properties'],
|
||||
},{
|
||||
id: 'comments', label: gettext('Comment'),
|
||||
editable: false, type: 'multiline',
|
||||
},{
|
||||
id: 'encoding', label: gettext('Encoding'),
|
||||
editable: false, type: 'text', group: gettext('Definition'),
|
||||
readonly: function(m) { return !m.isNew(); }, url: 'get_encodings',
|
||||
control: 'node-ajax-options', cache_level: 'server',
|
||||
},{
|
||||
id: 'template', label: gettext('Template'),
|
||||
editable: false, type: 'text', group: gettext('Definition'),
|
||||
readonly: function(m) { return !m.isNew(); },
|
||||
control: 'node-list-by-name', url: 'get_databases', cache_level: 'server',
|
||||
select2: { allowClear: false }, mode: ['create'],
|
||||
transform: function(data, cell) {
|
||||
var res = [],
|
||||
control = cell || this,
|
||||
label = control.model.get('name');
|
||||
|
||||
if (!control.model.isNew()) {
|
||||
res.push({label: label, value: label});
|
||||
}
|
||||
else {
|
||||
if (data && _.isArray(data)) {
|
||||
_.each(data, function(d) {
|
||||
res.push({label: d, value: d,
|
||||
image: 'pg-icon-database'});
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
},{
|
||||
id: 'spcname', label: gettext('Tablespace'),
|
||||
editable: false, type: 'text', group: gettext('Definition'),
|
||||
control: 'node-list-by-name', node: 'tablespace',
|
||||
select2: { allowClear: false },
|
||||
filter: function(m) {
|
||||
return (m.label != 'pg_global');
|
||||
},
|
||||
},{
|
||||
id: 'datcollate', label: gettext('Collation'),
|
||||
editable: false, type: 'text', group: gettext('Definition'),
|
||||
readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
|
||||
control: 'node-ajax-options', cache_level: 'server',
|
||||
},{
|
||||
id: 'datctype', label: gettext('Character type'),
|
||||
editable: false, type: 'text', group: gettext('Definition'),
|
||||
readonly: function(m) { return !m.isNew(); }, url: 'get_ctypes',
|
||||
control: 'node-ajax-options', cache_level: 'server',
|
||||
},{
|
||||
id: 'datconnlimit', label: gettext('Connection limit'),
|
||||
editable: false, type: 'int', group: gettext('Definition'), min: -1,
|
||||
},{
|
||||
id: 'is_template', label: gettext('Template?'),
|
||||
editable: false, type: 'switch', group: gettext('Definition'),
|
||||
readonly: true, mode: ['properties', 'edit'],
|
||||
},{
|
||||
id: 'datallowconn', label: gettext('Allow connections?'),
|
||||
editable: false, type: 'switch', group: gettext('Definition'),
|
||||
mode: ['properties'],
|
||||
},{
|
||||
id: 'datacl', label: gettext('Privileges'), type: 'collection',
|
||||
model: pgBrowser.Node.PrivilegeRoleModel.extend({
|
||||
privileges: ['C', 'T', 'c'],
|
||||
}), uniqueCol : ['grantee', 'grantor'], editable: false,
|
||||
group: gettext('Security'), mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true, control: 'unique-col-collection',
|
||||
},{
|
||||
id: 'variables', label: '', type: 'collection',
|
||||
model: pgBrowser.Node.VariableModel.extend({keys:['name', 'role']}), editable: false,
|
||||
group: gettext('Parameters'), mode: ['edit', 'create'],
|
||||
canAdd: true, canEdit: false, canDelete: true, hasRole: true,
|
||||
control: Backform.VariableCollectionControl, node: 'role',
|
||||
},{
|
||||
id: 'seclabels', label: gettext('Security labels'),
|
||||
model: pgBrowser.SecLabelModel,
|
||||
editable: false, type: 'collection', canEdit: false,
|
||||
group: gettext('Security'), canDelete: true,
|
||||
mode: ['edit', 'create'], canAdd: true,
|
||||
control: 'unique-col-collection', uniqueCol : ['provider'],
|
||||
min_version: 90200,
|
||||
},{
|
||||
type: 'nested', control: 'tab', group: gettext('Default Privileges'),
|
||||
mode: ['edit'],
|
||||
schema:[{
|
||||
id: 'deftblacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
|
||||
{privileges: ['a', 'r', 'w', 'd', 'D', 'x', 't']}), label: '',
|
||||
editable: false, type: 'collection', group: gettext('Tables'),
|
||||
mode: ['edit', 'create'], control: 'unique-col-collection',
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'defseqacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
|
||||
{privileges: ['r', 'w', 'U']}), label: '',
|
||||
editable: false, type: 'collection', group: gettext('Sequences'),
|
||||
mode: ['edit', 'create'], control: 'unique-col-collection',
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'deffuncacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
|
||||
{privileges: ['X']}), label: '',
|
||||
editable: false, type: 'collection', group: gettext('Functions'),
|
||||
mode: ['edit', 'create'], control: 'unique-col-collection',
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'deftypeacl', model: pgBrowser.Node.PrivilegeRoleModel.extend(
|
||||
{privileges: ['U']}), label: '',
|
||||
editable: false, type: 'collection', group: 'deftypesacl_group',
|
||||
mode: ['edit', 'create'], control: 'unique-col-collection',
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
min_version: 90200,
|
||||
},{
|
||||
id: 'deftypesacl_group', type: 'group', label: gettext('Types'),
|
||||
mode: ['edit', 'create'], min_version: 90200,
|
||||
},
|
||||
],
|
||||
},{
|
||||
type: 'collection', group: gettext('Advanced'),
|
||||
},
|
||||
{
|
||||
id: 'schema_res', label: gettext('Schema restriction'),
|
||||
type: 'select2', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
helpMessage: gettext('Note: Changes to the schema restriction will require the Schemas node in the browser to be refreshed before they will be shown.'),
|
||||
select2: {
|
||||
multiple: true, allowClear: false, tags: true,
|
||||
tokenSeparators: [','], first_empty: false,
|
||||
selectOnClose: true, emptyOptions: true,
|
||||
},
|
||||
control: Backform.Select2Control.extend({
|
||||
onChange: function() {
|
||||
Backform.Select2Control.prototype.onChange.apply(this, arguments);
|
||||
if (!this.model || !(
|
||||
this.model.changed &&
|
||||
this.model.get('oid') !== undefined
|
||||
)) {
|
||||
this.model.inform_text = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.model.origSessAttrs.schema_res != this.model.changed.schema_res)
|
||||
{
|
||||
this.model.inform_text = gettext(
|
||||
'Please refresh the Schemas node to make changes to the schema restriction take effect.'
|
||||
);
|
||||
} else {
|
||||
this.model.inform_text = undefined;
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
validate: function() {
|
||||
var name = this.get('name');
|
||||
if (_.isUndefined(name) || _.isNull(name) ||
|
||||
String(name).replace(/^\s+|\s+$/g, '') == '') {
|
||||
var msg = gettext('Name cannot be empty.');
|
||||
this.errorModel.set('name', msg);
|
||||
return msg;
|
||||
} else {
|
||||
this.errorModel.unset('name');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,216 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import SecLabelSchema from '../../../static/js/sec_label.ui';
|
||||
|
||||
export class DefaultPrivSchema extends BaseUISchema {
|
||||
constructor(getPrivilegeRoleSchema) {
|
||||
super();
|
||||
this.getPrivilegeRoleSchema = getPrivilegeRoleSchema;
|
||||
}
|
||||
|
||||
get fields() {
|
||||
return [
|
||||
{
|
||||
id: 'deftblacl', type: 'collection', group: gettext('Tables'),
|
||||
schema: this.getPrivilegeRoleSchema(['a', 'r', 'w', 'd', 'D', 'x', 't']),
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true,
|
||||
uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'defseqacl', type: 'collection', group: gettext('Sequences'),
|
||||
schema: this.getPrivilegeRoleSchema(['r', 'w', 'U']),
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true,
|
||||
uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'deffuncacl', type: 'collection', group: gettext('Functions'),
|
||||
schema: this.getPrivilegeRoleSchema(['X']),
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
},{
|
||||
id: 'deftypeacl', type: 'collection', group: gettext('Types'),
|
||||
schema: this.getPrivilegeRoleSchema(['U']), min_version: 90200,
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true, uniqueCol : ['grantee', 'grantor'],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default class DatabaseSchema extends BaseUISchema {
|
||||
constructor(getVariableSchema, getPrivilegeRoleSchema, fieldOptions={}, initValues) {
|
||||
super({
|
||||
name: undefined,
|
||||
owner: undefined,
|
||||
is_sys_obj: undefined,
|
||||
comment: undefined,
|
||||
encoding: 'UTF8',
|
||||
template: undefined,
|
||||
tablespace: undefined,
|
||||
collation: undefined,
|
||||
char_type: undefined,
|
||||
datconnlimit: -1,
|
||||
datallowconn: undefined,
|
||||
variables: [],
|
||||
privileges: [],
|
||||
securities: [],
|
||||
datacl: [],
|
||||
deftblacl: [],
|
||||
deffuncacl: [],
|
||||
defseqacl: [],
|
||||
is_template: false,
|
||||
deftypeacl: [],
|
||||
schema_res:'',
|
||||
...initValues,
|
||||
});
|
||||
this.getVariableSchema = getVariableSchema;
|
||||
this.getPrivilegeRoleSchema = getPrivilegeRoleSchema;
|
||||
this.fieldOptions = {
|
||||
role: [],
|
||||
encoding: [],
|
||||
template: [],
|
||||
spcname: [],
|
||||
datcollate: [],
|
||||
datctype: [],
|
||||
...fieldOptions,
|
||||
};
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'did';
|
||||
}
|
||||
|
||||
get fields() {
|
||||
let obj = this;
|
||||
return [
|
||||
{
|
||||
id: 'name', label: gettext('Database'), cell: 'text',
|
||||
editable: false, type: 'text', noEmpty: true,
|
||||
},{
|
||||
id: 'did', label: gettext('OID'), cell: 'text', mode: ['properties'],
|
||||
editable: false, type: 'text',
|
||||
},{
|
||||
id: 'datowner', label: gettext('Owner'),
|
||||
editable: false, type: 'select', options: this.fieldOptions.role,
|
||||
controlProps: { allowClear: false },
|
||||
},{
|
||||
id: 'is_sys_obj', label: gettext('System database?'),
|
||||
cell: 'switch', type: 'switch', mode: ['properties'],
|
||||
},{
|
||||
id: 'comments', label: gettext('Comment'),
|
||||
editable: false, type: 'multiline',
|
||||
},{
|
||||
id: 'encoding', label: gettext('Encoding'),
|
||||
editable: false, type: 'select', group: gettext('Definition'),
|
||||
readonly: function(state) {return !obj.isNew(state); },
|
||||
options: this.fieldOptions.encoding,
|
||||
},{
|
||||
id: 'template', label: gettext('Template'),
|
||||
editable: false, type: 'select', group: gettext('Definition'),
|
||||
readonly: function(state) {return !obj.isNew(state); },
|
||||
options: this.fieldOptions.template,
|
||||
controlProps: { allowClear: false }, mode: ['create'],
|
||||
},{
|
||||
id: 'spcname', label: gettext('Tablespace'),
|
||||
editable: false, type: 'select', group: gettext('Definition'),
|
||||
options: this.fieldOptions.spcname,
|
||||
controlProps: { allowClear: false },
|
||||
},{
|
||||
id: 'datcollate', label: gettext('Collation'),
|
||||
editable: false, type: 'select', group: gettext('Definition'),
|
||||
readonly: function(state) {return !obj.isNew(state); },
|
||||
options: this.fieldOptions.datcollate,
|
||||
},{
|
||||
id: 'datctype', label: gettext('Character type'),
|
||||
editable: false, type: 'select', group: gettext('Definition'),
|
||||
readonly: function(state) {return !obj.isNew(state); },
|
||||
options: this.fieldOptions.datctype,
|
||||
},{
|
||||
id: 'datconnlimit', label: gettext('Connection limit'),
|
||||
editable: false, type: 'int', group: gettext('Definition'),
|
||||
min: -1,
|
||||
},{
|
||||
id: 'is_template', label: gettext('Template?'),
|
||||
editable: false, type: 'switch', group: gettext('Definition'),
|
||||
readonly: true, mode: ['properties', 'edit'],
|
||||
},{
|
||||
id: 'datallowconn', label: gettext('Allow connections?'),
|
||||
editable: false, type: 'switch', group: gettext('Definition'),
|
||||
mode: ['properties'],
|
||||
},{
|
||||
id: 'acl', label: gettext('Privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'tblacl', label: gettext('Default TABLE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'seqacl', label: gettext('Default SEQUENCE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'funcacl', label: gettext('Default FUNCTION privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'],
|
||||
},{
|
||||
id: 'typeacl', label: gettext('Default TYPE privileges'), type: 'text',
|
||||
group: gettext('Security'), mode: ['properties'], min_version: 90200,
|
||||
},
|
||||
{
|
||||
id: 'datacl', label: gettext('Privileges'), type: 'collection',
|
||||
schema: this.getPrivilegeRoleSchema(['C', 'T', 'c']),
|
||||
uniqueCol : ['grantee', 'grantor'],
|
||||
editable: false,
|
||||
group: gettext('Security'), mode: ['edit', 'create'],
|
||||
canAdd: true, canDelete: true,
|
||||
},
|
||||
{
|
||||
id: 'variables', label: '', type: 'collection',
|
||||
schema: this.getVariableSchema(),
|
||||
editable: false,
|
||||
group: gettext('Parameters'), mode: ['edit', 'create'],
|
||||
canAdd: true, canEdit: false, canDelete: true, hasRole: true,
|
||||
node: 'role',
|
||||
},{
|
||||
id: 'seclabels', label: gettext('Security labels'), type: 'collection',
|
||||
schema: new SecLabelSchema(),
|
||||
editable: false, group: gettext('Security'),
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: true, canEdit: false, canDelete: true,
|
||||
uniqueCol : ['provider'],
|
||||
min_version: 90200,
|
||||
},{
|
||||
type: 'nested-tab', group: gettext('Default Privileges'),
|
||||
mode: ['edit'],
|
||||
schema: new DefaultPrivSchema(this.getPrivilegeRoleSchema),
|
||||
},
|
||||
{
|
||||
id: 'schema_res', label: gettext('Schema restriction'),
|
||||
type: 'select', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
helpMessage: gettext('Note: Changes to the schema restriction will require the Schemas node in the browser to be refreshed before they will be shown.'),
|
||||
controlProps: {
|
||||
multiple: true, allowClear: false, creatable: true,
|
||||
}, depChange: (state)=>{
|
||||
if(!_.isUndefined(state.oid)) {
|
||||
obj.informText = undefined;
|
||||
}
|
||||
|
||||
if(obj.origData.schema_res != state.schema_res) {
|
||||
obj.informText = gettext(
|
||||
'Please refresh the Schemas node to make changes to the schema restriction take effect.'
|
||||
);
|
||||
} else {
|
||||
obj.informText = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import { getNodeListByName } from '../../../../static/js/node_ajax';
|
||||
|
||||
export function getNodePrivilegeRoleSchema(nodeObj, treeNodeInfo, itemNodeData, privileges) {
|
||||
let keys = ['grantee', 'privileges', 'grantor'];
|
||||
return new PrivilegeRoleSchema(
|
||||
()=>getNodeListByName('role', treeNodeInfo, itemNodeData, ()=>true, (res)=>{
|
||||
res.unshift({label: 'PUBLIC', value: 'PUBLIC'});
|
||||
return res;
|
||||
}),
|
||||
()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
|
||||
keys,
|
||||
treeNodeInfo,
|
||||
privileges
|
||||
);
|
||||
}
|
||||
|
||||
export default class PrivilegeRoleSchema extends BaseUISchema {
|
||||
constructor(granteeOptions, grantorOptions, keys, nodeInfo, supportedPrivs) {
|
||||
super({
|
||||
grantee: undefined,
|
||||
grantor: nodeInfo?.server?.user?.name,
|
||||
privileges: undefined,
|
||||
});
|
||||
this.granteeOptions = granteeOptions;
|
||||
this.grantorOptions = grantorOptions;
|
||||
this.nodeInfo = nodeInfo;
|
||||
this.supportedPrivs = supportedPrivs || [];
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
|
||||
return [{
|
||||
id: 'grantee', label: gettext('Grantee'), type:'text',
|
||||
editable: true,
|
||||
cell: ()=>({
|
||||
cell: 'select', options: this.granteeOptions,
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
}
|
||||
}),
|
||||
noEmpty: true,
|
||||
},
|
||||
{
|
||||
id: 'privileges', label: gettext('Privileges'),
|
||||
type: 'text', group: null,
|
||||
cell: ()=>({cell: 'privilege', controlProps: {
|
||||
supportedPrivs: this.supportedPrivs,
|
||||
}}), minWidth: 280,
|
||||
disabled : function(state) {
|
||||
return !(
|
||||
obj.nodeInfo &&
|
||||
obj.nodeInfo.server.user.name == state['grantor']
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'grantor', label: gettext('Grantor'), type: 'text', readonly: true,
|
||||
cell: ()=>({cell: 'select', options: obj.grantorOptions}),
|
||||
}];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
if((state.privileges || []).length <= 0) {
|
||||
setError('privileges', gettext('At least one privilege should be selected.'));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
|
||||
export default class SecLabelSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
provider: undefined,
|
||||
label: undefined,
|
||||
});
|
||||
this.keys = ['provider', 'label'];
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [{
|
||||
id: 'provider', label: gettext('Provider'),
|
||||
type: 'text', editable: true, cell: 'text',
|
||||
},
|
||||
{
|
||||
id: 'label', label: gettext('Security label'),
|
||||
type: 'text', editable: true, cell: 'text', noEmpty: true,
|
||||
}];
|
||||
}
|
||||
}
|
@ -7,23 +7,22 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { getNodeListById } from '../../../../static/js/node_ajax';
|
||||
import ServerSchema from './server.ui';
|
||||
|
||||
define('pgadmin.node.server', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
|
||||
'sources/pgadmin', 'pgadmin.browser',
|
||||
'pgadmin.server.supported_servers', 'pgadmin.user_management.current_user',
|
||||
'pgadmin.user_management.current_user',
|
||||
'pgadmin.alertifyjs', 'pgadmin.backform',
|
||||
'sources/browser/server_groups/servers/model_validation',
|
||||
'pgadmin.authenticate.kerberos',
|
||||
'pgadmin.browser.server.privilege',
|
||||
], function(
|
||||
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser,
|
||||
supported_servers, current_user, Alertify, Backform,
|
||||
modelValidation, Kerberos,
|
||||
current_user, Alertify, Backform, Kerberos,
|
||||
) {
|
||||
|
||||
if (!pgBrowser.Nodes['server']) {
|
||||
var SSL_MODES = ['prefer', 'require', 'verify-ca', 'verify-full'];
|
||||
|
||||
pgBrowser.SecLabelModel = pgBrowser.Node.Model.extend({
|
||||
defaults: {
|
||||
provider: undefined,
|
||||
@ -738,469 +737,15 @@ define('pgadmin.node.server', [
|
||||
pgBrowser.psql.psql_tool(d, i, true);
|
||||
}
|
||||
},
|
||||
model: pgAdmin.Browser.Node.Model.extend({
|
||||
defaults: {
|
||||
gid: undefined,
|
||||
id: undefined,
|
||||
name: '',
|
||||
sslmode: 'prefer',
|
||||
host: '',
|
||||
hostaddr: '',
|
||||
port: 5432,
|
||||
db: 'postgres',
|
||||
username: current_user.name,
|
||||
role: null,
|
||||
connect_now: true,
|
||||
password: undefined,
|
||||
save_password: false,
|
||||
db_res: '',
|
||||
passfile: undefined,
|
||||
sslcompression: false,
|
||||
sslcert: undefined,
|
||||
sslkey: undefined,
|
||||
sslrootcert: undefined,
|
||||
sslcrl: undefined,
|
||||
service: undefined,
|
||||
use_ssh_tunnel: 0,
|
||||
tunnel_host: undefined,
|
||||
tunnel_port: 22,
|
||||
tunnel_username: undefined,
|
||||
tunnel_identity_file: undefined,
|
||||
tunnel_password: undefined,
|
||||
tunnel_authentication: 0,
|
||||
save_tunnel_password: false,
|
||||
connect_timeout: 10,
|
||||
},
|
||||
// Default values!
|
||||
initialize: function(attrs, args) {
|
||||
var isNew = (_.size(attrs) === 0);
|
||||
|
||||
if (isNew) {
|
||||
this.set({'gid': args.node_info['server_group']._id});
|
||||
getSchema: (treeNodeInfo, itemNodeData)=>{
|
||||
let schema = new ServerSchema(
|
||||
getNodeListById(pgBrowser.Nodes['server_group'], treeNodeInfo, itemNodeData),
|
||||
{
|
||||
gid: treeNodeInfo['server_group']._id,
|
||||
}
|
||||
pgAdmin.Browser.Node.Model.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
schema: [{
|
||||
id: 'id', label: gettext('ID'), type: 'int', mode: ['properties'],
|
||||
visible: function(model){
|
||||
if (model.attributes.user_id != current_user.id && pgAdmin.server_mode == 'True')
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
},{
|
||||
id: 'name', label: gettext('Name'), type: 'text',
|
||||
mode: ['properties', 'edit', 'create'], disabled: 'isShared',
|
||||
},
|
||||
{
|
||||
id: 'gid', label: gettext('Server group'), type: 'int',
|
||||
control: 'node-list-by-id', node: 'server_group',
|
||||
mode: ['create', 'edit'], select2: {allowClear: false}, disabled: 'isShared',
|
||||
},
|
||||
{
|
||||
id: 'server_owner', label: gettext('Shared Server Owner'), type: 'text', mode: ['properties'],
|
||||
visible:function(model){
|
||||
var serverOwner = model.attributes.user_id;
|
||||
if (model.attributes.shared && serverOwner != current_user.id && pgAdmin.server_mode == 'True'){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'server_type', label: gettext('Server type'), type: 'options',
|
||||
mode: ['properties'], visible: 'isConnected',
|
||||
'options': supported_servers,
|
||||
},{
|
||||
id: 'connected', label: gettext('Connected?'), type: 'switch',
|
||||
mode: ['properties'], group: gettext('Connection'), 'options': {
|
||||
'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini',
|
||||
},
|
||||
},{
|
||||
id: 'version', label: gettext('Version'), type: 'text', group: null,
|
||||
mode: ['properties'], visible: 'isConnected',
|
||||
},{
|
||||
id: 'bgcolor', label: gettext('Background'), type: 'color',
|
||||
group: null, mode: ['edit', 'create'], disabled: 'isfgColorSet',
|
||||
deps: ['fgcolor'],
|
||||
},{
|
||||
id: 'fgcolor', label: gettext('Foreground'), type: 'color',
|
||||
group: null, mode: ['edit', 'create'], disabled: 'isConnected',
|
||||
},{
|
||||
id: 'connect_now', controlLabel: gettext('Connect now?'), type: 'checkbox',
|
||||
group: null, mode: ['create'],
|
||||
},{
|
||||
id: 'shared', label: gettext('Shared?'), type: 'switch',
|
||||
mode: ['properties', 'create', 'edit'], 'options': {'size': 'mini'},
|
||||
readonly: function(model){
|
||||
var serverOwner = model.attributes.user_id;
|
||||
if (!model.isNew() && serverOwner != current_user.id){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},visible: function(){
|
||||
if (current_user.is_admin && pgAdmin.server_mode == 'True')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'comment', label: gettext('Comments'), type: 'multiline', group: null,
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
},{
|
||||
id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'],disabled: 'isShared',
|
||||
control: Backform.InputControl.extend({
|
||||
onChange: function() {
|
||||
Backform.InputControl.prototype.onChange.apply(this, arguments);
|
||||
if (!this.model || !this.model.changed) {
|
||||
this.model.inform_text = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.model.origSessAttrs.host != this.model.changed.host && !this.model.isNew() && this.model.get('connected'))
|
||||
{
|
||||
this.model.inform_text = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
this.model.inform_text = undefined;
|
||||
}
|
||||
},
|
||||
}),
|
||||
},{
|
||||
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: 'isShared',
|
||||
control: Backform.InputControl.extend({
|
||||
onChange: function() {
|
||||
Backform.InputControl.prototype.onChange.apply(this, arguments);
|
||||
if (!this.model || !this.model.changed) {
|
||||
this.model.inform_text = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.model.origSessAttrs.port != this.model.changed.port && !this.model.isNew() && this.model.get('connected'))
|
||||
{
|
||||
this.model.inform_text = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
this.model.inform_text = undefined;
|
||||
}
|
||||
},
|
||||
}),
|
||||
},{
|
||||
id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',disabled: 'isShared',
|
||||
},{
|
||||
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
control: Backform.InputControl.extend({
|
||||
onChange: function() {
|
||||
Backform.InputControl.prototype.onChange.apply(this, arguments);
|
||||
if (!this.model || !this.model.changed) {
|
||||
this.model.inform_text = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.model.origSessAttrs.username != this.model.changed.username && !this.model.isNew() && this.model.get('connected'))
|
||||
{
|
||||
this.model.inform_text = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
this.model.inform_text = undefined;
|
||||
}
|
||||
},
|
||||
}),
|
||||
},{
|
||||
id: 'kerberos_conn', label: gettext('Kerberos authentication?'), type: 'switch',
|
||||
group: gettext('Connection'), 'options': {
|
||||
'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini',
|
||||
}
|
||||
},{
|
||||
id: 'gss_authenticated', label: gettext('GSS authenticated?'), type: 'switch',
|
||||
group: gettext('Connection'), 'options': {
|
||||
'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini',
|
||||
}, mode: ['properties'], visible: 'isConnected'
|
||||
},{
|
||||
id: 'gss_encrypted', label: gettext('GSS encrypted?'), type: 'switch',
|
||||
group: gettext('Connection'), 'options': {
|
||||
'onText': gettext('True'), 'offText': gettext('False'), 'size': 'mini',
|
||||
}, mode: ['properties'], visible: 'isConnected',
|
||||
},{
|
||||
id: 'password', label: gettext('Password'), type: 'password', maxlength: null,
|
||||
group: gettext('Connection'), control: 'input', mode: ['create'],
|
||||
deps: ['connect_now', 'kerberos_conn'],
|
||||
visible: function(model) {
|
||||
return model.get('connect_now') && model.isNew();
|
||||
},
|
||||
disabled: function(model) {
|
||||
if (model.get('kerberos_conn'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
},{
|
||||
id: 'save_password', controlLabel: gettext('Save password?'),
|
||||
type: 'checkbox', group: gettext('Connection'), mode: ['create'],
|
||||
deps: ['connect_now', 'kerberos_conn'], visible: function(model) {
|
||||
return model.get('connect_now') && model.isNew();
|
||||
},
|
||||
disabled: function(model) {
|
||||
if (!current_user.allow_save_password || model.get('kerberos_conn'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
},{
|
||||
id: 'role', label: gettext('Role'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
|
||||
},{
|
||||
id: 'service', label: gettext('Service'), type: 'text',
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
|
||||
group: gettext('Connection'),
|
||||
},{
|
||||
id: 'sslmode', label: gettext('SSL mode'), control: 'select2', group: gettext('SSL'),
|
||||
select2: {
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: Infinity,
|
||||
},
|
||||
mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
|
||||
'options': [
|
||||
{label: gettext('Allow'), value: 'allow'},
|
||||
{label: gettext('Prefer'), value: 'prefer'},
|
||||
{label: gettext('Require'), value: 'require'},
|
||||
{label: gettext('Disable'), value: 'disable'},
|
||||
{label: gettext('Verify-CA'), value: 'verify-ca'},
|
||||
{label: gettext('Verify-Full'), value: 'verify-full'},
|
||||
],
|
||||
},{
|
||||
id: 'sslcert', label: gettext('Client certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: 'isSSL', readonly: 'isConnected', control: Backform.FileControl,
|
||||
dialog_type: 'select_file', supp_types: ['*'],
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslkey', label: gettext('Client certificate key'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: 'isSSL', readonly: 'isConnected', control: Backform.FileControl,
|
||||
dialog_type: 'select_file', supp_types: ['*'],
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslrootcert', label: gettext('Root certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: 'isSSL', readonly: 'isConnected', control: Backform.FileControl,
|
||||
dialog_type: 'select_file', supp_types: ['*'],
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: 'isSSL', readonly: 'isConnected', control: Backform.FileControl,
|
||||
dialog_type: 'select_file', supp_types: ['*'],
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch',
|
||||
mode: ['edit', 'create'], group: gettext('SSL'),
|
||||
'options': {'size': 'mini'},
|
||||
deps: ['sslmode'], disabled: 'isSSL', readonly: 'isConnected',
|
||||
},{
|
||||
id: 'sslcert', label: gettext('Client certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(model) {
|
||||
var sslcert = model.get('sslcert');
|
||||
return !_.isUndefined(sslcert) && !_.isNull(sslcert);
|
||||
},
|
||||
},{
|
||||
id: 'sslkey', label: gettext('Client certificate key'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(model) {
|
||||
var sslkey = model.get('sslkey');
|
||||
return !_.isUndefined(sslkey) && !_.isNull(sslkey);
|
||||
},
|
||||
},{
|
||||
id: 'sslrootcert', label: gettext('Root certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(model) {
|
||||
var sslrootcert = model.get('sslrootcert');
|
||||
return !_.isUndefined(sslrootcert) && !_.isNull(sslrootcert);
|
||||
},
|
||||
},{
|
||||
id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(model) {
|
||||
var sslcrl = model.get('sslcrl');
|
||||
return !_.isUndefined(sslcrl) && !_.isNull(sslcrl);
|
||||
},
|
||||
},{
|
||||
id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch',
|
||||
mode: ['properties'], group: gettext('SSL'),
|
||||
'options': {'size': 'mini'},
|
||||
deps: ['sslmode'], visible: function(model) {
|
||||
var sslmode = model.get('sslmode');
|
||||
return _.indexOf(SSL_MODES, sslmode) != -1;
|
||||
},
|
||||
},{
|
||||
id: 'use_ssh_tunnel', label: gettext('Use SSH tunneling'), type: 'switch',
|
||||
mode: ['properties', 'edit', 'create'], group: gettext('SSH Tunnel'),
|
||||
'options': {'size': 'mini'},
|
||||
disabled: function(model) {
|
||||
if (!pgAdmin.Browser.utils.support_ssh_tunnel) {
|
||||
setTimeout(function() {
|
||||
model.set('use_ssh_tunnel', 0);
|
||||
}, 10);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
},{
|
||||
id: 'tunnel_host', label: gettext('Tunnel host'), type: 'text', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'],
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
},{
|
||||
id: 'tunnel_port', label: gettext('Tunnel port'), type: 'int', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'], max: 65535,
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
},{
|
||||
id: 'tunnel_username', label: gettext('Username'), type: 'text', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'],
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
},{
|
||||
id: 'tunnel_authentication', label: gettext('Authentication'), type: 'switch',
|
||||
mode: ['properties', 'edit', 'create'], group: gettext('SSH Tunnel'),
|
||||
'options': {'onText': gettext('Identity file'),
|
||||
'offText': gettext('Password'), 'size': 'mini', width: '90'},
|
||||
deps: ['use_ssh_tunnel'],
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
}, {
|
||||
id: 'tunnel_identity_file', label: gettext('Identity file'), type: 'text',
|
||||
group: gettext('SSH Tunnel'), mode: ['properties', 'edit', 'create'],
|
||||
control: Backform.FileControl, dialog_type: 'select_file', supp_types: ['*'],
|
||||
deps: ['tunnel_authentication', 'use_ssh_tunnel'],
|
||||
disabled: function(model) {
|
||||
let file = model.get('tunnel_identity_file');
|
||||
if (!model.get('tunnel_authentication') && file) {
|
||||
setTimeout(function() {
|
||||
model.set('tunnel_identity_file', null);
|
||||
}, 10);
|
||||
}
|
||||
return !model.get('tunnel_authentication') || !model.get('use_ssh_tunnel');
|
||||
},
|
||||
},{
|
||||
id: 'tunnel_password', label: gettext('Password'), type: 'password',
|
||||
group: gettext('SSH Tunnel'), control: 'input', mode: ['create'],
|
||||
deps: ['use_ssh_tunnel'],
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
readonly: 'isConnected',
|
||||
}, {
|
||||
id: 'save_tunnel_password', controlLabel: gettext('Save password?'),
|
||||
type: 'checkbox', group: gettext('SSH Tunnel'), mode: ['create'],
|
||||
deps: ['connect_now', 'use_ssh_tunnel'], visible: function(model) {
|
||||
return model.get('connect_now') && model.isNew();
|
||||
},
|
||||
disabled: function(model) {
|
||||
if (!current_user.allow_save_tunnel_password ||
|
||||
!model.get('use_ssh_tunnel'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
}, {
|
||||
id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
|
||||
},{
|
||||
id: 'db_res', label: gettext('DB restriction'), type: 'select2', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected', select2: {multiple: true, allowClear: false,
|
||||
tags: true, tokenSeparators: [','], first_empty: false, selectOnClose: true, emptyOptions: true},
|
||||
},{
|
||||
id: 'passfile', label: gettext('Password file'), type: 'text',
|
||||
group: gettext('Advanced'), mode: ['edit', 'create'],
|
||||
disabled: 'isValidLib', readonly: 'isConnected', control: Backform.FileControl,
|
||||
dialog_type: 'select_file', supp_types: ['*'],
|
||||
},{
|
||||
id: 'passfile', label: gettext('Password file'), type: 'text',
|
||||
group: gettext('Advanced'), mode: ['properties'],
|
||||
visible: function(model) {
|
||||
var passfile = model.get('passfile');
|
||||
return !_.isUndefined(passfile) && !_.isNull(passfile);
|
||||
},
|
||||
},{
|
||||
id: 'connect_timeout', label: gettext('Connection timeout (seconds)'),
|
||||
type: 'int', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
|
||||
min: 0,
|
||||
}],
|
||||
isVisible: function(model){
|
||||
var serverOwner = model.attributes.user_id;
|
||||
if (!model.isNew() && serverOwner != current_user.id){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
},
|
||||
isShared: function(model){
|
||||
var serverOwner = model.attributes.user_id;
|
||||
if (!model.isNew() && serverOwner != current_user.id && model.attributes.shared){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
validate: function() {
|
||||
const validateModel = new modelValidation.ModelValidation(this);
|
||||
return validateModel.validate();
|
||||
},
|
||||
isConnected: function(model) {
|
||||
return model.get('connected');
|
||||
},
|
||||
isfgColorSet: function(model) {
|
||||
var bgcolor = model.get('bgcolor'),
|
||||
fgcolor = model.get('fgcolor');
|
||||
|
||||
if(model.get('connected')) {
|
||||
return true;
|
||||
}
|
||||
// If fgcolor is set and bgcolor is not set then force bgcolor
|
||||
// to set as white
|
||||
if(_.isUndefined(bgcolor) || _.isNull(bgcolor) || !bgcolor) {
|
||||
if(fgcolor) {
|
||||
model.set('bgcolor', '#ffffff');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isSSL: function(model) {
|
||||
var ssl_mode = model.get('sslmode');
|
||||
return _.indexOf(SSL_MODES, ssl_mode) == -1;
|
||||
},
|
||||
isValidLib: function() {
|
||||
// older version of libpq do not support 'passfile' parameter in
|
||||
// connect method, valid libpq must have version >= 100000
|
||||
return pgBrowser.utils.pg_libpq_version < 100000;
|
||||
},
|
||||
}),
|
||||
);
|
||||
return schema;
|
||||
},
|
||||
connection_lost: function(i, resp) {
|
||||
if (pgBrowser.tree) {
|
||||
var t = pgBrowser.tree,
|
||||
|
538
web/pgadmin/browser/server_groups/servers/static/js/server.ui.js
Normal file
538
web/pgadmin/browser/server_groups/servers/static/js/server.ui.js
Normal file
@ -0,0 +1,538 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import _ from 'lodash';
|
||||
import {Address4, Address6} from 'ip-address';
|
||||
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {default as supportedServers} from 'pgadmin.server.supported_servers';
|
||||
|
||||
import current_user from 'pgadmin.user_management.current_user';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
|
||||
export default class ServerSchema extends BaseUISchema {
|
||||
constructor(serverGroupOptions=[], initValues) {
|
||||
super({
|
||||
gid: undefined,
|
||||
id: undefined,
|
||||
name: '',
|
||||
bgcolor: '',
|
||||
fgcolor: '',
|
||||
sslmode: 'prefer',
|
||||
host: '',
|
||||
hostaddr: '',
|
||||
port: 5432,
|
||||
db: 'postgres',
|
||||
username: current_user.name,
|
||||
role: null,
|
||||
connect_now: true,
|
||||
password: undefined,
|
||||
save_password: false,
|
||||
db_res: '',
|
||||
passfile: undefined,
|
||||
sslcompression: false,
|
||||
sslcert: undefined,
|
||||
sslkey: undefined,
|
||||
sslrootcert: undefined,
|
||||
sslcrl: undefined,
|
||||
service: undefined,
|
||||
use_ssh_tunnel: 0,
|
||||
tunnel_host: undefined,
|
||||
tunnel_port: 22,
|
||||
tunnel_username: undefined,
|
||||
tunnel_identity_file: undefined,
|
||||
tunnel_password: undefined,
|
||||
tunnel_authentication: false,
|
||||
save_tunnel_password: false,
|
||||
connect_timeout: 10,
|
||||
...initValues,
|
||||
});
|
||||
|
||||
this.serverGroupOptions = serverGroupOptions;
|
||||
_.bindAll(this, 'isShared', 'isSSL');
|
||||
}
|
||||
|
||||
get SSL_MODES() { return ['prefer', 'require', 'verify-ca', 'verify-full']; }
|
||||
|
||||
isShared(state) {
|
||||
if(!this.isNew(state) && state.user_id != current_user.id && state.shared) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isConnected(state) {
|
||||
return Boolean(state.connected);
|
||||
}
|
||||
|
||||
isSSL(state) {
|
||||
return this.SSL_MODES.indexOf(state.sslmode) == -1;
|
||||
}
|
||||
|
||||
isValidLib() {
|
||||
// older version of libpq do not support 'passfile' parameter in
|
||||
// connect method, valid libpq must have version >= 100000
|
||||
return pgAdmin.Browser.utils.pg_libpq_version < 100000;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
return [
|
||||
{
|
||||
id: 'id', label: gettext('ID'), type: 'int', group: null,
|
||||
mode: ['properties'],
|
||||
},{
|
||||
id: 'name', label: gettext('Name'), type: 'text', group: null,
|
||||
mode: ['properties', 'edit', 'create'], noEmpty: true,
|
||||
disabled: obj.isShared,
|
||||
},{
|
||||
id: 'gid', label: gettext('Server group'), type: 'select',
|
||||
options: obj.serverGroupOptions,
|
||||
mode: ['create', 'edit'],
|
||||
controlProps: { allowClear: false },
|
||||
disabled: obj.isShared,
|
||||
},
|
||||
{
|
||||
id: 'server_owner', label: gettext('Shared Server Owner'), type: 'text', mode: ['properties'],
|
||||
visible: function(state) {
|
||||
var serverOwner = state.user_id;
|
||||
if (state.shared && serverOwner != current_user.id && pgAdmin.server_mode == 'True'){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'server_type', label: gettext('Server type'), type: 'select',
|
||||
mode: ['properties'], visible: obj.isConnected,
|
||||
options: supportedServers,
|
||||
}, {
|
||||
id: 'connected', label: gettext('Connected?'), type: 'switch',
|
||||
mode: ['properties'], group: gettext('Connection'),
|
||||
}, {
|
||||
id: 'version', label: gettext('Version'), type: 'text', group: null,
|
||||
mode: ['properties'], visible: obj.isConnected,
|
||||
},
|
||||
{
|
||||
id: 'bgcolor', label: gettext('Background'), type: 'color',
|
||||
group: null, mode: ['edit', 'create'],
|
||||
disabled: obj.isConnected, deps: ['fgcolor'], depChange: (state)=>{
|
||||
if(!state.bgcolor && state.fgcolor) {
|
||||
return {'bgcolor': '#ffffff'};
|
||||
}
|
||||
}
|
||||
},{
|
||||
id: 'fgcolor', label: gettext('Foreground'), type: 'color',
|
||||
group: null, mode: ['edit', 'create'], disabled: obj.isConnected,
|
||||
},
|
||||
{
|
||||
id: 'connect_now', label: gettext('Connect now?'), type: 'switch',
|
||||
group: null, mode: ['create'],
|
||||
},
|
||||
{
|
||||
id: 'shared', label: gettext('Shared?'), type: 'switch',
|
||||
mode: ['properties', 'create', 'edit'],
|
||||
readonly: function(state){
|
||||
var serverOwner = state.user_id;
|
||||
if (obj.isNew(state) && serverOwner != current_user.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, visible: function(){
|
||||
if (current_user.is_admin && pgAdmin.server_mode == 'True')
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'comment', label: gettext('Comments'), type: 'multiline', group: null,
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
},
|
||||
{
|
||||
id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], disabled: obj.isShared,
|
||||
depChange: (state)=>{
|
||||
if(obj.origData.host != state.host && !obj.isNew(state) && state.connected){
|
||||
obj.informText = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
obj.informText = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: obj.isShared,
|
||||
depChange: (state)=>{
|
||||
if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){
|
||||
obj.informText = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
obj.informText = undefined;
|
||||
}
|
||||
}
|
||||
},{
|
||||
id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, disabled: obj.isShared,
|
||||
noEmpty: true,
|
||||
},{
|
||||
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
depChange: (state)=>{
|
||||
if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){
|
||||
obj.informText = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
} else {
|
||||
obj.informText = undefined;
|
||||
}
|
||||
}
|
||||
},{
|
||||
id: 'kerberos_conn', label: gettext('Kerberos authentication?'), type: 'switch',
|
||||
group: gettext('Connection'),
|
||||
},{
|
||||
id: 'gss_authenticated', label: gettext('GSS authenticated?'), type: 'switch',
|
||||
group: gettext('Connection'), mode: ['properties'], visible: obj.isConnected,
|
||||
},{
|
||||
id: 'gss_encrypted', label: gettext('GSS encrypted?'), type: 'switch',
|
||||
group: gettext('Connection'), mode: ['properties'], visible: obj.isConnected,
|
||||
},{
|
||||
id: 'password', label: gettext('Password'), type: 'password', maxlength: null,
|
||||
group: gettext('Connection'),
|
||||
mode: ['create'],
|
||||
deps: ['connect_now', 'kerberos_conn'],
|
||||
visible: function(state) {
|
||||
return state.connect_now && obj.isNew(state);
|
||||
},
|
||||
disabled: function(state) {return state.kerberos_conn;},
|
||||
},{
|
||||
id: 'save_password', label: gettext('Save password?'),
|
||||
type: 'switch', group: gettext('Connection'), mode: ['create'],
|
||||
deps: ['connect_now', 'kerberos_conn'],
|
||||
visible: function(state) {
|
||||
return state.connect_now && obj.isNew(state);
|
||||
},
|
||||
disabled: function(state) {
|
||||
if (!current_user.allow_save_password || state.kerberos_conn)
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
},{
|
||||
id: 'role', label: gettext('Role'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected,
|
||||
},{
|
||||
id: 'service', label: gettext('Service'), type: 'text',
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected,
|
||||
group: gettext('Connection'),
|
||||
},
|
||||
{
|
||||
id: 'sslmode', label: gettext('SSL mode'), type: 'select', group: gettext('SSL'),
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
},
|
||||
mode: ['properties', 'edit', 'create'], disabled: obj.isConnected,
|
||||
options: [
|
||||
{label: gettext('Allow'), value: 'allow'},
|
||||
{label: gettext('Prefer'), value: 'prefer'},
|
||||
{label: gettext('Require'), value: 'require'},
|
||||
{label: gettext('Disable'), value: 'disable'},
|
||||
{label: gettext('Verify-CA'), value: 'verify-ca'},
|
||||
{label: gettext('Verify-Full'), value: 'verify-full'},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sslcert', label: gettext('Client certificate'), type: 'file',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: obj.isSSL, readonly: obj.isConnected,
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
deps: ['sslmode'],
|
||||
},
|
||||
{
|
||||
id: 'sslkey', label: gettext('Client certificate key'), type: 'file',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: obj.isSSL, readonly: obj.isConnected,
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslrootcert', label: gettext('Root certificate'), type: 'file',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: obj.isSSL, readonly: obj.isConnected,
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
deps: ['sslmode'],
|
||||
},{
|
||||
id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'file',
|
||||
group: gettext('SSL'), mode: ['edit', 'create'],
|
||||
disabled: obj.isSSL, readonly: obj.isConnected,
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
deps: ['sslmode'],
|
||||
},
|
||||
{
|
||||
id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch',
|
||||
mode: ['edit', 'create'], group: gettext('SSL'),
|
||||
disabled: obj.isSSL, readonly: obj.isConnected,
|
||||
deps: ['sslmode'],
|
||||
},
|
||||
{
|
||||
id: 'sslcert', label: gettext('Client certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(state) {
|
||||
var sslcert = state.sslcert;
|
||||
return !_.isUndefined(sslcert) && !_.isNull(sslcert);
|
||||
},
|
||||
},{
|
||||
id: 'sslkey', label: gettext('Client certificate key'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(state) {
|
||||
var sslkey = state.sslkey;
|
||||
return !_.isUndefined(sslkey) && !_.isNull(sslkey);
|
||||
},
|
||||
},{
|
||||
id: 'sslrootcert', label: gettext('Root certificate'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(state) {
|
||||
var sslrootcert = state.sslrootcert;
|
||||
return !_.isUndefined(sslrootcert) && !_.isNull(sslrootcert);
|
||||
},
|
||||
},{
|
||||
id: 'sslcrl', label: gettext('Certificate revocation list'), type: 'text',
|
||||
group: gettext('SSL'), mode: ['properties'],
|
||||
deps: ['sslmode'],
|
||||
visible: function(state) {
|
||||
var sslcrl = state.sslcrl;
|
||||
return !_.isUndefined(sslcrl) && !_.isNull(sslcrl);
|
||||
},
|
||||
},{
|
||||
id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch',
|
||||
mode: ['properties'], group: gettext('SSL'),
|
||||
deps: ['sslmode'],
|
||||
visible: function(state) {
|
||||
return _.indexOf(obj.SSL_MODES, state.sslmode) != -1;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'use_ssh_tunnel', label: gettext('Use SSH tunneling'), type: 'switch',
|
||||
mode: ['properties', 'edit', 'create'], group: gettext('SSH Tunnel'),
|
||||
disabled: function() {
|
||||
return !pgAdmin.Browser.utils.support_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
},{
|
||||
id: 'tunnel_host', label: gettext('Tunnel host'), type: 'text', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'],
|
||||
disabled: function(state) {
|
||||
return !state.use_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
},{
|
||||
id: 'tunnel_port', label: gettext('Tunnel port'), type: 'int', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'], max: 65535,
|
||||
disabled: function(state) {
|
||||
return !state.use_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
},{
|
||||
id: 'tunnel_username', label: gettext('Username'), type: 'text', group: gettext('SSH Tunnel'),
|
||||
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'],
|
||||
disabled: function(state) {
|
||||
return !state.use_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
},{
|
||||
id: 'tunnel_authentication', label: gettext('Authentication'), type: 'toggle',
|
||||
mode: ['properties', 'edit', 'create'], group: gettext('SSH Tunnel'),
|
||||
options: [
|
||||
{'label': gettext('Password'), value: false},
|
||||
{'label': gettext('Identity file'), value: true},
|
||||
],
|
||||
disabled: function(state) {
|
||||
return !state.use_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
},
|
||||
{
|
||||
id: 'tunnel_identity_file', label: gettext('Identity file'), type: 'file',
|
||||
group: gettext('SSH Tunnel'), mode: ['properties', 'edit', 'create'],
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
deps: ['tunnel_authentication', 'use_ssh_tunnel'],
|
||||
depChange: (state)=>{
|
||||
if (!state.tunnel_authentication && state.tunnel_identity_file) {
|
||||
return {tunnel_identity_file: null};
|
||||
}
|
||||
},
|
||||
disabled: function(state) {
|
||||
return !state.tunnel_authentication || !state.use_ssh_tunnel;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tunnel_password', label: gettext('Password'), type: 'password',
|
||||
group: gettext('SSH Tunnel'), mode: ['create'],
|
||||
deps: ['use_ssh_tunnel'],
|
||||
disabled: function(state) {
|
||||
return !state.use_ssh_tunnel;
|
||||
},
|
||||
readonly: obj.isConnected,
|
||||
}, {
|
||||
id: 'save_tunnel_password', label: gettext('Save password?'),
|
||||
type: 'switch', group: gettext('SSH Tunnel'), mode: ['create'],
|
||||
deps: ['connect_now', 'use_ssh_tunnel'],
|
||||
visible: function(state) {
|
||||
return state.connect_now && obj.isNew(state);
|
||||
},
|
||||
disabled: function(state) {
|
||||
return (!current_user.allow_save_tunnel_password || !state.use_ssh_tunnel);
|
||||
},
|
||||
}, {
|
||||
id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected,
|
||||
},
|
||||
{
|
||||
id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'),
|
||||
options: [],
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected, controlProps: {
|
||||
multiple: true, allowClear: false, creatable: true},
|
||||
},
|
||||
{
|
||||
id: 'passfile', label: gettext('Password file'), type: 'file',
|
||||
group: gettext('Advanced'), mode: ['edit', 'create'],
|
||||
disabled: obj.isValidLib, readonly: obj.isConnected,
|
||||
controlProps: {
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'passfile', label: gettext('Password file'), type: 'text',
|
||||
group: gettext('Advanced'), mode: ['properties'],
|
||||
visible: function(state) {
|
||||
var passfile = state.passfile;
|
||||
return !_.isUndefined(passfile) && !_.isNull(passfile);
|
||||
},
|
||||
},{
|
||||
id: 'connect_timeout', label: gettext('Connection timeout (seconds)'),
|
||||
type: 'int', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], readonly: obj.isConnected,
|
||||
min: 0,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
let errmsg = null;
|
||||
|
||||
if (isEmptyString(state.service)) {
|
||||
errmsg = gettext('Either Host name, Address or Service must be specified.');
|
||||
if(isEmptyString(state.host) && isEmptyString(state.hostaddr)) {
|
||||
setError('host', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('host', errmsg);
|
||||
setError('hostaddr', errmsg);
|
||||
}
|
||||
|
||||
/* IP address validate */
|
||||
if (state.hostaddr) {
|
||||
try {
|
||||
new Address4(state.hostaddr);
|
||||
} catch(e) {
|
||||
try {
|
||||
new Address6(state.hostaddr);
|
||||
} catch(ex) {
|
||||
errmsg = gettext('Host address must be valid IPv4 or IPv6 address.');
|
||||
setError('hostaddr', errmsg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError('hostaddr', null);
|
||||
}
|
||||
|
||||
if(isEmptyString(state.username)) {
|
||||
errmsg = gettext('Username must be specified.');
|
||||
setError('username', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('username', errmsg);
|
||||
}
|
||||
|
||||
if(isEmptyString(state.port)) {
|
||||
errmsg = gettext('Port must be specified.');
|
||||
setError('port', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('port', errmsg);
|
||||
}
|
||||
} else {
|
||||
errmsg = null;
|
||||
_.each(['host', 'hostaddr', 'db', 'username', 'port'], (item) => {
|
||||
setError(item, errmsg);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.use_ssh_tunnel) {
|
||||
if(isEmptyString(state.tunnel_host)) {
|
||||
errmsg = gettext('SSH Tunnel host must be specified.');
|
||||
setError('tunnel_host', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('tunnel_host', errmsg);
|
||||
}
|
||||
|
||||
if(isEmptyString(state.tunnel_port)) {
|
||||
errmsg = gettext('SSH Tunnel port must be specified.');
|
||||
setError('tunnel_port', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('tunnel_port', errmsg);
|
||||
}
|
||||
|
||||
if(isEmptyString(state.tunnel_username)) {
|
||||
errmsg = gettext('SSH Tunnel username must be specified.');
|
||||
setError('tunnel_username', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('tunnel_username', errmsg);
|
||||
}
|
||||
|
||||
if (state.tunnel_authentication) {
|
||||
if(isEmptyString(state.tunnel_identity_file)) {
|
||||
errmsg = gettext('SSH Tunnel identity file must be specified.');
|
||||
setError('tunnel_identity_file', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
errmsg = null;
|
||||
setError('tunnel_identity_file', errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import _ from 'lodash';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import { getNodeAjaxOptions, getNodeListByName } from '../../../../static/js/node_ajax';
|
||||
|
||||
export function getNodeVariableSchema(nodeObj, treeNodeInfo, itemNodeData, hasDatabase, hasRole) {
|
||||
let keys = ['name', 'value'];
|
||||
if(hasDatabase) {
|
||||
keys.push('database');
|
||||
}
|
||||
if(hasRole) {
|
||||
keys.push('role');
|
||||
}
|
||||
return new VariableSchema(
|
||||
()=>getNodeAjaxOptions('vopts', nodeObj, treeNodeInfo, itemNodeData, null, (vars)=>{
|
||||
var res = [];
|
||||
_.each(vars, function(v) {
|
||||
res.push({
|
||||
'value': v.name,
|
||||
'image': undefined,
|
||||
'label': v.name,
|
||||
'vartype': v.vartype,
|
||||
'enumvals': v.enumvals,
|
||||
'max_val': v.max_val,
|
||||
'min_val': v.min_val,
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
()=>getNodeListByName('database', treeNodeInfo, itemNodeData),
|
||||
()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
|
||||
keys
|
||||
);
|
||||
}
|
||||
|
||||
export default class VariableSchema extends BaseUISchema {
|
||||
constructor(vnameOptions, databaseOptions, roleOptions, keys) {
|
||||
super({
|
||||
name: undefined,
|
||||
value: undefined,
|
||||
role: null,
|
||||
database: null,
|
||||
});
|
||||
this.vnameOptions = vnameOptions;
|
||||
this.databaseOptions = databaseOptions;
|
||||
this.roleOptions = roleOptions;
|
||||
this.varTypes = {};
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
setVarTypes(options) {
|
||||
options.forEach((option)=>{
|
||||
this.varTypes[option.value] = {
|
||||
...option,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getValueFieldProps(variable) {
|
||||
switch(variable?.vartype) {
|
||||
case 'bool':
|
||||
return 'switch';
|
||||
case 'enum':
|
||||
return {
|
||||
cell: 'select',
|
||||
options: (variable.enumvals || []).map((val)=>({
|
||||
label: val,
|
||||
value: val
|
||||
}))
|
||||
};
|
||||
case 'integer':
|
||||
return 'int';
|
||||
case 'real':
|
||||
return 'number';
|
||||
case 'string':
|
||||
return 'text';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
return [
|
||||
{
|
||||
id: 'id', label: gettext('ID'), type: 'int', group: null,
|
||||
mode: ['properties'],
|
||||
},
|
||||
{
|
||||
id: 'name', label: gettext('Name'), type:'text',
|
||||
readonly: function(state) {
|
||||
return !obj.isNew(state);
|
||||
},
|
||||
cell: ()=>({
|
||||
cell: 'select',
|
||||
options: this.vnameOptions,
|
||||
optionsLoaded: (options)=>{obj.setVarTypes(options);},
|
||||
controlProps: { allowClear: false },
|
||||
}),
|
||||
noEmpty: true,
|
||||
},
|
||||
{
|
||||
id: 'value', label: gettext('Value'), type: 'text',
|
||||
noEmpty: true, deps: ['name'],
|
||||
depChange: (state, changeSource)=>{
|
||||
if(changeSource == 'name') {
|
||||
return {...state
|
||||
, value: null
|
||||
};
|
||||
}
|
||||
},
|
||||
cell: (row)=>{
|
||||
let variable = this.varTypes[row.name];
|
||||
return this.getValueFieldProps(variable);
|
||||
}
|
||||
},
|
||||
{id: 'database', label: gettext('Database'), type: 'text',
|
||||
cell: ()=>({cell: 'select', options: this.databaseOptions }),
|
||||
},
|
||||
{id: 'role', label: gettext('Role'), type: 'text',
|
||||
cell: ()=>({cell: 'select', options: this.roleOptions,
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
}
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import ServerGroupSchema from './server_group.ui';
|
||||
|
||||
define('pgadmin.node.server_group', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||
@ -35,52 +36,7 @@ define('pgadmin.node.server_group', [
|
||||
data: {'action': 'create'}, icon: 'wcTabIcon icon-server_group',
|
||||
}]);
|
||||
},
|
||||
model: pgAdmin.Browser.Node.Model.extend({
|
||||
defaults: {
|
||||
id: undefined,
|
||||
name: null,
|
||||
user_id: undefined,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
id: 'id', label: gettext('ID'), type: 'int', group: null,
|
||||
mode: ['properties'],
|
||||
visible: function(model){
|
||||
if (model.attributes.user_id != current_user.id && !current_user.is_admin)
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
},{
|
||||
id: 'name', label: gettext('Name'), type: 'text', group: null,
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
disabled: function(model){
|
||||
if (model.attributes.user_id != current_user.id && !_.isUndefined(model.attributes.user_id))
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
validate: function() {
|
||||
var errmsg = null;
|
||||
|
||||
this.errorModel.clear();
|
||||
|
||||
if (!this.isNew() && 'id' in this.changed) {
|
||||
errmsg = gettext('The ID cannot be changed.');
|
||||
this.errorModel.set('id', errmsg);
|
||||
return errmsg;
|
||||
}
|
||||
if (_.isUndefined(this.get('name')) ||
|
||||
_.isNull(this.get('name')) ||
|
||||
String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
errmsg = gettext('Name cannot be empty.');
|
||||
this.errorModel.set('name', errmsg);
|
||||
return errmsg;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
|
||||
getSchema: ()=>new ServerGroupSchema(),
|
||||
canDrop: function(itemData) {
|
||||
var serverOwner = itemData.user_id;
|
||||
if (serverOwner != current_user.id && !_.isUndefined(serverOwner))
|
||||
|
@ -0,0 +1,34 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
|
||||
export default class ServerGroupSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
id: undefined,
|
||||
name: null,
|
||||
user_id: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'id', label: gettext('ID'), type: 'int', group: null,
|
||||
mode: ['properties'], visible: true,
|
||||
},{
|
||||
id: 'name', label: gettext('Name'), type: 'text', group: null,
|
||||
mode: ['properties', 'edit', 'create'], noEmpty: true,
|
||||
disabled: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import {removeNodeView} from './node_view';
|
||||
|
||||
define([
|
||||
'sources/gettext', 'jquery', 'underscore', 'sources/pgadmin',
|
||||
'backbone', 'alertify', 'backform', 'backgrid', 'sources/browser/generate_url',
|
||||
@ -239,6 +241,8 @@ define([
|
||||
j.data('obj-view', null);
|
||||
}
|
||||
|
||||
/* Remove any dom rendered by getNodeView */
|
||||
removeNodeView(j[0]);
|
||||
// Make sure the HTML element is empty.
|
||||
j.empty();
|
||||
j.data('obj-view', gridView);
|
||||
|
@ -7,6 +7,8 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import {getNodeView, removeNodeView} from './node_view';
|
||||
|
||||
define('pgadmin.browser.node', [
|
||||
'sources/tree/pgadmin_tree_node', 'sources/url_for',
|
||||
'sources/gettext', 'jquery', 'underscore', 'sources/pgadmin',
|
||||
@ -1240,6 +1242,22 @@ define('pgadmin.browser.node', [
|
||||
// Cache the current IDs for next time
|
||||
$(this).data('node-prop', treeHierarchy);
|
||||
|
||||
/* Remove any dom rendered by getNodeView */
|
||||
removeNodeView(j[0]);
|
||||
/* getSchema is a schema for React. Get the react node view */
|
||||
if(that.getSchema) {
|
||||
let treeNodeInfo = that.getTreeNodeHierarchy.apply(this, [item]);
|
||||
getNodeView(
|
||||
that.type, treeNodeInfo, 'properties', data, 'tab', j[0], this, onCancelFunc, onEdit,
|
||||
(nodeData)=>{
|
||||
if(nodeData.node) {
|
||||
onSaveFunc(nodeData.node, treeNodeInfo);
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.hasClass('has-pg-prop-btn-group'))
|
||||
content.addClass('has-pg-prop-btn-group');
|
||||
|
||||
@ -1484,6 +1502,28 @@ define('pgadmin.browser.node', [
|
||||
action = 'edit';
|
||||
}
|
||||
self.$container.attr('action-mode', action);
|
||||
|
||||
self.icon(
|
||||
_.isFunction(that['node_image']) ?
|
||||
(that['node_image']).apply(that, [data]) :
|
||||
(that['node_image'] || ('icon-' + that.type))
|
||||
);
|
||||
/* Remove any dom rendered by getNodeView */
|
||||
removeNodeView(j[0]);
|
||||
/* getSchema is a schema for React. Get the react node view */
|
||||
if(that.getSchema) {
|
||||
let treeNodeInfo = that.getTreeNodeHierarchy.apply(this, [item]);
|
||||
getNodeView(
|
||||
that.type, treeNodeInfo, action, data, 'dialog', j[0], this, onCancelFunc, onEdit,
|
||||
(nodeData)=>{
|
||||
if(nodeData.node) {
|
||||
onSaveFunc(nodeData.node, treeNodeInfo);
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to release any existing view, before
|
||||
// creating the new view.
|
||||
if (view) {
|
||||
@ -1678,10 +1718,10 @@ define('pgadmin.browser.node', [
|
||||
// Closing this panel
|
||||
this.close();
|
||||
}.bind(panel),
|
||||
updateTreeItem = function(obj) {
|
||||
updateTreeItem = function(obj, tnode, node_info) {
|
||||
var _old = data,
|
||||
_new = _.clone(view.model.tnode),
|
||||
info = _.clone(view.model.node_info);
|
||||
_new = tnode || _.clone(view.model.tnode),
|
||||
info = node_info || _.clone(view.model.node_info);
|
||||
|
||||
// Clear the cache for this node now.
|
||||
setTimeout(function() {
|
||||
@ -1705,7 +1745,7 @@ define('pgadmin.browser.node', [
|
||||
);
|
||||
closePanel(false);
|
||||
},
|
||||
saveNewNode = function(obj) {
|
||||
saveNewNode = function(obj, tnode, node_info) {
|
||||
var $props = this.$container.find('.obj_properties').first(),
|
||||
objview = $props.data('obj-view');
|
||||
|
||||
@ -1715,8 +1755,8 @@ define('pgadmin.browser.node', [
|
||||
}, 0);
|
||||
try {
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:browser:tree:add', _.clone(objview.model.tnode),
|
||||
_.clone(objview.model.node_info)
|
||||
'pgadmin:browser:tree:add', _.clone(tnode || objview.model.tnode),
|
||||
_.clone(node_info || objview.model.node_info)
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(e.stack || e);
|
||||
@ -1748,10 +1788,10 @@ define('pgadmin.browser.node', [
|
||||
}
|
||||
} else {
|
||||
/* Show properties */
|
||||
properties();
|
||||
onEdit = editInNewPanel.bind(panel);
|
||||
properties();
|
||||
}
|
||||
if (panel.closeable()) {
|
||||
if (panel.closeable() && !that.getSchema) {
|
||||
panel.on(wcDocker.EVENT.CLOSING, warnBeforeChangesLost.bind(
|
||||
panel,
|
||||
gettext('Changes will be lost. Are you sure you want to close the dialog?'),
|
||||
|
164
web/pgadmin/browser/static/js/node_ajax.js
Normal file
164
web/pgadmin/browser/static/js/node_ajax.js
Normal file
@ -0,0 +1,164 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import getApiInstance from '../../../static/js/api_instance';
|
||||
import {generate_url} from 'sources/browser/generate_url';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
/* It generates the URL based on tree node selected */
|
||||
export function generateNodeUrl(treeNodeInfo, actionType, itemNodeData, withId, jumpAfterNode) {
|
||||
let opURL = {
|
||||
'create': 'obj',
|
||||
'drop': 'obj',
|
||||
'edit': 'obj',
|
||||
'properties': 'obj',
|
||||
'statistics': 'stats',
|
||||
},
|
||||
priority = -Infinity;
|
||||
let nodeObj = this;
|
||||
let itemID = withId && itemNodeData._type == nodeObj.type ? encodeURIComponent(itemNodeData._id) : '';
|
||||
actionType = actionType in opURL ? opURL[actionType] : actionType;
|
||||
|
||||
if (nodeObj.parent_type) {
|
||||
if (_.isString(nodeObj.parent_type)) {
|
||||
let p = treeNodeInfo[nodeObj.parent_type];
|
||||
if (p) {
|
||||
priority = p.priority;
|
||||
}
|
||||
} else {
|
||||
_.each(nodeObj.parent_type, function(o) {
|
||||
let p = treeNodeInfo[o];
|
||||
if (p) {
|
||||
if (priority < p.priority) {
|
||||
priority = p.priority;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let jump_after_priority = priority;
|
||||
if(jumpAfterNode && treeNodeInfo[jumpAfterNode]) {
|
||||
jump_after_priority = treeNodeInfo[jumpAfterNode].priority;
|
||||
}
|
||||
|
||||
var nodePickFunction = function(treeInfoValue) {
|
||||
return (treeInfoValue.priority <= jump_after_priority || treeInfoValue.priority == priority);
|
||||
};
|
||||
|
||||
return generate_url(pgAdmin.Browser.URL, treeNodeInfo, actionType, nodeObj.type, nodePickFunction, itemID);
|
||||
}
|
||||
|
||||
|
||||
/* Get the nodes list as options required by select controls
|
||||
* The options are cached for performance reasons.
|
||||
*/
|
||||
export function getNodeAjaxOptions(url, nodeObj, treeNodeInfo, itemNodeData, params={}, transform=(data)=>data) {
|
||||
let otherParams = {
|
||||
urlWithId: false,
|
||||
jumpAfterNode: null,
|
||||
...params
|
||||
};
|
||||
return new Promise((resolve, reject)=>{
|
||||
const api = getApiInstance();
|
||||
let fullUrl = '';
|
||||
if(url) {
|
||||
fullUrl = generateNodeUrl.call(
|
||||
nodeObj, treeNodeInfo, url, itemNodeData, otherParams.urlWithId, nodeObj.parent_type, otherParams.jumpAfterNode
|
||||
);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
let cacheNode = pgAdmin.Browser.Nodes[otherParams.cacheNode] || nodeObj;
|
||||
let cacheLevel = otherParams.cacheLevel || cacheNode.cache_level(treeNodeInfo, otherParams.urlWithId);
|
||||
/*
|
||||
* We needs to check, if we have already cached data for this url.
|
||||
* If yes - use that, and do not bother about fetching it again,
|
||||
* and use it.
|
||||
*/
|
||||
var data = cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel);
|
||||
|
||||
if (_.isUndefined(data) || _.isNull(data)) {
|
||||
api.get(fullUrl)
|
||||
.then((res)=>{
|
||||
data = res.data.data;
|
||||
cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data);
|
||||
resolve(transform(data));
|
||||
})
|
||||
.catch((err)=>{
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
// To fetch only options from cache, we do not need time from 'at'
|
||||
// attribute but only options.
|
||||
resolve(transform(data.data || []));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Get the nodes list based on current selected node id */
|
||||
export function getNodeListById(nodeObj, treeNodeInfo, itemNodeData, filter=()=>true) {
|
||||
/* Transform the result to add image details */
|
||||
const transform = (rows) => {
|
||||
var res = [];
|
||||
|
||||
_.each(rows, function(r) {
|
||||
if (filter(r)) {
|
||||
var l = (_.isFunction(nodeObj['node_label']) ?
|
||||
(nodeObj['node_label']).apply(nodeObj, [r]) :
|
||||
r.label),
|
||||
image = (_.isFunction(nodeObj['node_image']) ?
|
||||
(nodeObj['node_image']).apply(nodeObj, [r]) :
|
||||
(nodeObj['node_image'] || ('icon-' + nodeObj.type)));
|
||||
|
||||
res.push({
|
||||
'value': r._id,
|
||||
'image': image,
|
||||
'label': l,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
return getNodeAjaxOptions('nodes', nodeObj, treeNodeInfo, itemNodeData, null, transform);
|
||||
}
|
||||
|
||||
/* Get the nodes list based on node name passed */
|
||||
export function getNodeListByName(node, treeNodeInfo, itemNodeData, filter=()=>true, postTransform=(res)=>res) {
|
||||
let nodeObj = pgAdmin.Browser.Nodes[node];
|
||||
/* Transform the result to add image details */
|
||||
const transform = (rows) => {
|
||||
var res = [];
|
||||
|
||||
_.each(rows, function(r) {
|
||||
if (filter(r)) {
|
||||
var l = (_.isFunction(nodeObj['node_label']) ?
|
||||
(nodeObj['node_label']).apply(nodeObj, [r]) :
|
||||
r.label),
|
||||
image = (_.isFunction(nodeObj['node_image']) ?
|
||||
(nodeObj['node_image']).apply(nodeObj, [r]) :
|
||||
(nodeObj['node_image'] || ('icon-' + nodeObj.type)));
|
||||
|
||||
res.push({
|
||||
'value': r.label,
|
||||
'image': image,
|
||||
'label': l,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return postTransform(res);
|
||||
};
|
||||
|
||||
return getNodeAjaxOptions('nodes', nodeObj, treeNodeInfo, itemNodeData, null, transform);
|
||||
}
|
198
web/pgadmin/browser/static/js/node_view.jsx
Normal file
198
web/pgadmin/browser/static/js/node_view.jsx
Normal file
@ -0,0 +1,198 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import getApiInstance from 'sources/api_instance';
|
||||
import {getHelpUrl} from 'pgadmin.help';
|
||||
import SchemaView from 'sources/SchemaView';
|
||||
import { generateNodeUrl } from './node_ajax';
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import gettext from 'sources/gettext';
|
||||
import 'wcdocker';
|
||||
|
||||
/* The entry point for rendering React based view in properties, called in node.js */
|
||||
export function getNodeView(nodeType, treeNodeInfo, actionType, itemNodeData, formType, container, containerPanel, onCancel, onEdit, onSave) {
|
||||
let nodeObj = pgAdmin.Browser.Nodes[nodeType];
|
||||
let serverInfo = treeNodeInfo && ('server' in treeNodeInfo) &&
|
||||
pgAdmin.Browser.serverInfo && pgAdmin.Browser[treeNodeInfo.server._id];
|
||||
let inCatalog = treeNodeInfo && ('catalog' in treeNodeInfo);
|
||||
let urlBase = generateNodeUrl.call(nodeObj, treeNodeInfo, actionType, itemNodeData, false, null);
|
||||
const api = getApiInstance();
|
||||
const url = (isNew)=>{
|
||||
return urlBase + (isNew ? '' : itemNodeData._id);
|
||||
};
|
||||
let isDirty = false; // usefull for warnings
|
||||
let warnOnCloseFlag = true;
|
||||
const confirmOnCloseReset = pgAdmin.Browser.get_preferences_for_module('browser').confirm_on_properties_close;
|
||||
|
||||
/* Called when dialog is opened in edit mode, promise required */
|
||||
let initData = ()=>new Promise((resolve, reject)=>{
|
||||
api.get(url(false))
|
||||
.then((res)=>{
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
if(err.response){
|
||||
console.error('error resp', err.response);
|
||||
} else if(err.request){
|
||||
console.error('error req', err.request);
|
||||
} else if(err.message){
|
||||
console.error('error msg', err.message);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
/* on save button callback, promise required */
|
||||
const onSaveClick = (isNew, data)=>new Promise((resolve, reject)=>{
|
||||
return api({
|
||||
url: url(isNew),
|
||||
method: isNew ? 'POST' : 'PUT',
|
||||
data: data,
|
||||
}).then((res)=>{
|
||||
/* Don't warn the user before closing dialog */
|
||||
warnOnCloseFlag = false;
|
||||
resolve(res.data);
|
||||
onSave(res.data);
|
||||
}).catch((err)=>{
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
/* Called when switched to SQL tab, promise required */
|
||||
const getSQLValue = (isNew, changedData)=>{
|
||||
const msqlUrl = generateNodeUrl.call(nodeObj, treeNodeInfo, 'msql', itemNodeData, !isNew, null);
|
||||
return new Promise((resolve, reject)=>{
|
||||
api({
|
||||
url: msqlUrl,
|
||||
method: 'GET',
|
||||
params: changedData,
|
||||
}).then((res)=>{
|
||||
resolve(res.data.data);
|
||||
}).catch((err)=>{
|
||||
if(err.response){
|
||||
console.error('error resp', err.response);
|
||||
} else if(err.request){
|
||||
console.error('error req', err.request);
|
||||
} else if(err.message){
|
||||
console.error('error msg', err.message);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/* Callback for help button */
|
||||
const onHelp = (isSqlHelp=false, isNew=false)=>{
|
||||
if(isSqlHelp) {
|
||||
let server = treeNodeInfo.server;
|
||||
let url = pgAdmin.Browser.utils.pg_help_path;
|
||||
if (server.server_type == 'ppas') {
|
||||
url = pgAdmin.Browser.utils.edbas_help_path;
|
||||
}
|
||||
|
||||
let fullUrl = '';
|
||||
if (nodeObj.sqlCreateHelp == '' && nodeObj.sqlAlterHelp != '') {
|
||||
fullUrl = getHelpUrl(url, nodeObj.sqlAlterHelp, server.version, server.server_type);
|
||||
} else if (nodeObj.sqlCreateHelp != '' && nodeObj.sqlAlterHelp == '') {
|
||||
fullUrl = getHelpUrl(url, nodeObj.sqlCreateHelp, server.version, server.server_type);
|
||||
} else {
|
||||
if (isNew) {
|
||||
fullUrl = getHelpUrl(url, nodeObj.sqlCreateHelp, server.version, server.server_type);
|
||||
} else {
|
||||
fullUrl = getHelpUrl(url, nodeObj.sqlAlterHelp, server.version, server.server_type);
|
||||
}
|
||||
}
|
||||
|
||||
window.open(fullUrl, 'postgres_help');
|
||||
} else {
|
||||
window.open(nodeObj.dialogHelp, 'pgadmin_help');
|
||||
}
|
||||
};
|
||||
|
||||
/* A warning before closing the dialog with unsaved changes, based on preference */
|
||||
let warnBeforeChangesLost = (yesCallback)=>{
|
||||
let confirmOnClose = pgAdmin.Browser.get_preferences_for_module('browser').confirm_on_properties_close;
|
||||
if (warnOnCloseFlag && confirmOnClose) {
|
||||
if(isDirty){
|
||||
Alertify.confirm(
|
||||
gettext('Warning'),
|
||||
gettext('Changes will be lost. Are you sure you want to close the dialog?'),
|
||||
function() {
|
||||
yesCallback();
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
).set('labels', {
|
||||
ok: gettext('Yes'),
|
||||
cancel: gettext('No'),
|
||||
}).show();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
yesCallback();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Bind the wcDocker dialog close event and check if user should be warned */
|
||||
if (containerPanel.closeable()) {
|
||||
containerPanel.on(window.wcDocker.EVENT.CLOSING, warnBeforeChangesLost.bind(
|
||||
containerPanel,
|
||||
function() {
|
||||
containerPanel.off(window.wcDocker.EVENT.CLOSING);
|
||||
/* Always clean up the react mounted dom before closing */
|
||||
removeNodeView(container);
|
||||
containerPanel.close();
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/* All other useful details can go with this object */
|
||||
const viewHelperProps = {
|
||||
mode: actionType,
|
||||
serverInfo: serverInfo ? {
|
||||
type: serverInfo.type,
|
||||
version: serverInfo.version,
|
||||
}: undefined,
|
||||
inCatalog: inCatalog,
|
||||
};
|
||||
|
||||
/* Fire at will, mount the DOM */
|
||||
ReactDOM.render(
|
||||
<SchemaView
|
||||
formType={formType}
|
||||
getInitData={initData}
|
||||
schema={nodeObj.getSchema.call(nodeObj, treeNodeInfo, itemNodeData)}
|
||||
viewHelperProps={viewHelperProps}
|
||||
onSave={onSaveClick}
|
||||
onClose={()=>containerPanel.close()}
|
||||
onHelp={onHelp}
|
||||
onEdit={onEdit}
|
||||
onDataChange={(dataChanged)=>{
|
||||
isDirty = dataChanged;
|
||||
}}
|
||||
confirmOnCloseReset={confirmOnCloseReset}
|
||||
hasSQL={nodeObj.hasSQL && (actionType === 'create' || actionType === 'edit')}
|
||||
getSQLValue={getSQLValue}
|
||||
disableSqlHelp={nodeObj.sqlAlterHelp == '' && nodeObj.sqlCreateHelp == ''}
|
||||
/>, container);
|
||||
}
|
||||
|
||||
/* When switching from normal node to collection node, clean up the React mounted DOM */
|
||||
export function removeNodeView(container) {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}
|
@ -28,6 +28,7 @@ define(
|
||||
'MUST_BE_NUM' : gettext("'%s' must be a numeric."),
|
||||
'MUST_GR_EQ' : gettext("'%s' must be greater than or equal to %s."),
|
||||
'MUST_LESS_EQ' : gettext("'%s' must be less than or equal to %s."),
|
||||
'CANNOT_BE_EMPTY': gettext("'%s' cannot be empty."),
|
||||
'STATISTICS_LABEL': gettext("Statistics"),
|
||||
'STATISTICS_VALUE_LABEL': gettext("Value"),
|
||||
'NODE_HAS_NO_SQL': gettext("No SQL could be generated for the selected object."),
|
||||
|
@ -42,7 +42,7 @@ def themes(app):
|
||||
# Let the default theme go if exception occurs
|
||||
pass
|
||||
|
||||
return theme_css
|
||||
return theme_css, theme
|
||||
|
||||
return {
|
||||
'get_theme_css': get_theme_css,
|
||||
|
399
web/pgadmin/static/js/SchemaView/DataGridView.jsx
Normal file
399
web/pgadmin/static/js/SchemaView/DataGridView.jsx
Normal file
@ -0,0 +1,399 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The DataGridView component is based on react-table component */
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { PgIconButton } from '../components/Buttons';
|
||||
import AddIcon from '@material-ui/icons/AddOutlined';
|
||||
import { MappedCellControl } from './MappedControl';
|
||||
import EditRoundedIcon from '@material-ui/icons/EditRounded';
|
||||
import DeleteRoundedIcon from '@material-ui/icons/DeleteRounded';
|
||||
import { useTable, useBlockLayout, useResizeColumns, useSortBy, useExpanded } from 'react-table';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import { SCHEMA_STATE_ACTIONS } from '.';
|
||||
import FormView from './FormView';
|
||||
import { confirmDeleteRow } from '../helpers/legacyConnector';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
grid: {
|
||||
...theme.mixins.panelBorder,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
gridHeader: {
|
||||
display: 'flex',
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
backgroundColor: theme.otherVars.headerBg,
|
||||
},
|
||||
gridHeaderText: {
|
||||
padding: theme.spacing(0.5, 1),
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
},
|
||||
gridControls: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
gridControlsButton: {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
...theme.mixins.panelBorder.left,
|
||||
},
|
||||
gridRowButton: {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
backgroundColor: 'inherit',
|
||||
},
|
||||
gridTableContainer: {
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
},
|
||||
table: {
|
||||
borderSpacing: 0,
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.otherVars.tableBg,
|
||||
},
|
||||
tableCell: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(0.5),
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
...theme.mixins.panelBorder.right,
|
||||
position: 'relative',
|
||||
textAlign: 'center'
|
||||
},
|
||||
tableCellHeader: {
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
padding: theme.spacing(1, 0.5),
|
||||
textAlign: 'left',
|
||||
},
|
||||
resizer: {
|
||||
display: 'inline-block',
|
||||
width: '5px',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: 'translateX(50%)',
|
||||
zIndex: 1,
|
||||
touchAction: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
function DataTableHeader({headerGroups}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div>
|
||||
{headerGroups.map((headerGroup, hi) => (
|
||||
<div key={hi} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column, ci) => (
|
||||
<div key={ci} {...column.getHeaderProps()}>
|
||||
<div {...(column.sortable ? column.getSortByToggleProps() : {})} className={clsx(classes.tableCell, classes.tableCellHeader)}>
|
||||
{column.render('Header')}
|
||||
<span>
|
||||
{column.isSorted
|
||||
? column.isSortedDesc
|
||||
? ' 🔽'
|
||||
: ' 🔼'
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
{column.resizable &&
|
||||
<div
|
||||
{...column.getResizerProps()}
|
||||
className={classes.resizer}
|
||||
/>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DataTableHeader.propTypes = {
|
||||
headerGroups: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
function DataTableRow({row, totalRows, canExpand, isResizing, viewHelperProps, formErr, schema, dataDispatch, accessPath}) {
|
||||
const classes = useStyles();
|
||||
const [key, setKey] = useState(false);
|
||||
// let key = useRef(true);
|
||||
/* Memoize the row to avoid unnecessary re-render.
|
||||
* If table data changes, then react-table re-renders the complete tables
|
||||
* We can avoid re-render by if row data is not changed
|
||||
*/
|
||||
let depsMap = _.values(row.original, Object.keys(row.original).filter((k)=>!k.startsWith('btn')));
|
||||
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]);
|
||||
return (
|
||||
useMemo(()=>
|
||||
<>
|
||||
<div {...row.getRowProps()} className="tr">
|
||||
{row.cells.map((cell, ci) => {
|
||||
return (
|
||||
<div key={ci} {...cell.getCellProps()} className={classes.tableCell}>
|
||||
{cell.render('Cell', {
|
||||
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
canExpand && row.isExpanded &&
|
||||
<FormView key={row.index} value={row.original} viewHelperProps={viewHelperProps} formErr={formErr} dataDispatch={dataDispatch}
|
||||
schema={schema} accessPath={accessPath} isNested={true}/>
|
||||
}
|
||||
</>
|
||||
, depsMap)
|
||||
);
|
||||
}
|
||||
|
||||
export default function DataGridView({
|
||||
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) {
|
||||
const classes = useStyles();
|
||||
/* Calculate the fields which depends on the current field
|
||||
deps has info on fields which the current field depends on. */
|
||||
const dependsOnField = useMemo(()=>{
|
||||
let res = {};
|
||||
schema.fields.forEach((field)=>{
|
||||
(field.deps || []).forEach((dep)=>{
|
||||
res[dep] = res[dep] || [];
|
||||
res[dep].push(field.id);
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}, []);
|
||||
let columns = useMemo(
|
||||
()=>{
|
||||
let cols = [];
|
||||
if(props.canEdit) {
|
||||
let colInfo = {
|
||||
Header: <> </>,
|
||||
id: 'btn-edit',
|
||||
accessor: ()=>{},
|
||||
resizable: false,
|
||||
sortable: false,
|
||||
dataType: 'edit',
|
||||
width: 30,
|
||||
minWidth: '0',
|
||||
Cell: ({row})=><PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon />} className={classes.gridRowButton}
|
||||
onClick={()=>{
|
||||
row.toggleRowExpanded(!row.isExpanded);
|
||||
}}
|
||||
/>
|
||||
};
|
||||
colInfo.Cell.displayName = 'Cell',
|
||||
colInfo.Cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
if(props.canDelete) {
|
||||
let colInfo = {
|
||||
Header: <> </>,
|
||||
id: 'btn-delete',
|
||||
accessor: ()=>{},
|
||||
resizable: false,
|
||||
sortable: false,
|
||||
dataType: 'delete',
|
||||
width: 30,
|
||||
minWidth: '0',
|
||||
Cell: ({row}) => {
|
||||
return (
|
||||
<PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon />}
|
||||
onClick={()=>{
|
||||
confirmDeleteRow(()=>{
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||
path: accessPath,
|
||||
value: row.index,
|
||||
});
|
||||
}, ()=>{}, props.customDeleteTitle, props.customDeleteMsg);
|
||||
}} className={classes.gridRowButton} />
|
||||
);
|
||||
}
|
||||
};
|
||||
colInfo.Cell.displayName = 'Cell',
|
||||
colInfo.Cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
|
||||
cols = cols.concat(
|
||||
schema.fields
|
||||
.map((field)=>{
|
||||
let colInfo = {
|
||||
Header: field.label,
|
||||
accessor: field.id,
|
||||
field: field,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
...(field.minWidth ? {minWidth: field.minWidth} : {}),
|
||||
Cell: ({value, row, ...other}) => {
|
||||
let {visible, disabled, readonly, ..._field} = field;
|
||||
|
||||
let verInLimit = (_.isUndefined(viewHelperProps.serverInfo) ? true :
|
||||
((_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)) &&
|
||||
(_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)) &&
|
||||
(_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version))));
|
||||
let _readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');
|
||||
if(!_readonly) {
|
||||
_readonly = evalFunc(readonly, row.original || {});
|
||||
}
|
||||
|
||||
let _visible = true;
|
||||
if(visible) {
|
||||
_visible = evalFunc(visible, row.original || {});
|
||||
}
|
||||
_visible = _visible && verInLimit;
|
||||
|
||||
disabled = evalFunc(disabled, row.original || {});
|
||||
|
||||
return <MappedCellControl rowIndex={row.index} value={value}
|
||||
row={row.original} {..._field}
|
||||
readonly={_readonly}
|
||||
disabled={disabled}
|
||||
visible={_visible}
|
||||
onCellChange={(value)=>{
|
||||
/* Get the changes on dependent fields as well.
|
||||
* The return value of depChange function is merged and passed to state.
|
||||
*/
|
||||
const depChange = (state)=>{
|
||||
let rowdata = _.get(state, accessPath.concat(row.index));
|
||||
_field.depChange && _.merge(rowdata, _field.depChange(rowdata, _field.id) || {});
|
||||
(dependsOnField[_field.id] || []).forEach((d)=>{
|
||||
d = _.find(schema.fields, (f)=>f.id==d);
|
||||
if(d.depChange) {
|
||||
_.merge(rowdata, d.depChange(rowdata, _field.id) || {});
|
||||
}
|
||||
});
|
||||
return state;
|
||||
};
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat([row.index, _field.id]),
|
||||
value: value,
|
||||
depChange: depChange,
|
||||
});
|
||||
}}
|
||||
reRenderRow={other.reRenderRow}
|
||||
/>;
|
||||
},
|
||||
};
|
||||
colInfo.Cell.displayName = 'Cell',
|
||||
colInfo.Cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
onCellChange: PropTypes.func,
|
||||
};
|
||||
return colInfo;
|
||||
})
|
||||
);
|
||||
return cols;
|
||||
},[]);
|
||||
|
||||
const onAddClick = useCallback(()=>{
|
||||
let newRow = {};
|
||||
columns.forEach((column)=>{
|
||||
if(column.field) {
|
||||
newRow[column.field.id] = schema.defaults[column.field.id];
|
||||
}
|
||||
});
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
|
||||
path: accessPath,
|
||||
value: newRow,
|
||||
});
|
||||
});
|
||||
|
||||
const defaultColumn = useMemo(()=>({
|
||||
minWidth: 175,
|
||||
}));
|
||||
|
||||
let tablePlugins = [
|
||||
useBlockLayout,
|
||||
useResizeColumns,
|
||||
useSortBy,
|
||||
];
|
||||
if(props.canEdit) {
|
||||
tablePlugins.push(useExpanded);
|
||||
}
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: value || [],
|
||||
defaultColumn,
|
||||
manualSortBy: true,
|
||||
autoResetSortBy: false,
|
||||
autoResetExpanded: false,
|
||||
},
|
||||
...tablePlugins,
|
||||
);
|
||||
|
||||
const isResizing = _.flatMap(headerGroups, headerGroup => headerGroup.headers.map(col=>col.isResizing)).includes(true);
|
||||
|
||||
return (
|
||||
<Box className={containerClassName}>
|
||||
<Box className={classes.grid}>
|
||||
<Box className={classes.gridHeader}>
|
||||
<Box className={classes.gridHeaderText}>{props.label}</Box>
|
||||
<Box className={classes.gridControls}>
|
||||
{props.canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
|
||||
</Box>
|
||||
</Box>
|
||||
<div {...getTableProps()} className={classes.table}>
|
||||
<DataTableHeader headerGroups={headerGroups} />
|
||||
<div {...getTableBodyProps()}>
|
||||
{rows.map((row, i) => {
|
||||
prepareRow(row);
|
||||
return <DataTableRow key={i} row={row} totalRows={rows.length} canExpand={props.canEdit}
|
||||
value={value} viewHelperProps={viewHelperProps} formErr={formErr} isResizing={isResizing}
|
||||
schema={schema} accessPath={accessPath.concat([row.index])} dataDispatch={dataDispatch} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridView.propTypes = {
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
viewHelperProps: PropTypes.object,
|
||||
formErr: PropTypes.object,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func.isRequired,
|
||||
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
canEdit: PropTypes.bool,
|
||||
canAdd: PropTypes.bool,
|
||||
canDelete: PropTypes.bool,
|
||||
customDeleteTitle: PropTypes.string,
|
||||
customDeleteMsg: PropTypes.string,
|
||||
};
|
257
web/pgadmin/static/js/SchemaView/FormView.jsx
Normal file
257
web/pgadmin/static/js/SchemaView/FormView.jsx
Normal file
@ -0,0 +1,257 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import TabPanel from '../components/TabPanel';
|
||||
import DataGridView from './DataGridView';
|
||||
import { SCHEMA_STATE_ACTIONS } from '.';
|
||||
import { InputSQL } from '../components/FormComponents';
|
||||
import gettext from 'sources/gettext';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
fullSpace: {
|
||||
padding: 0,
|
||||
height: '100%'
|
||||
},
|
||||
controlRow: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
/* Optional SQL tab */
|
||||
function SQLTab({active, getSQLValue}) {
|
||||
const [sql, setSql] = useState('Loading...');
|
||||
useEffect(()=>{
|
||||
let unmounted = false;
|
||||
if(active) {
|
||||
setSql('Loading...');
|
||||
getSQLValue().then((value)=>{
|
||||
if(!unmounted) {
|
||||
setSql(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ()=>{unmounted=true;};
|
||||
}, [active]);
|
||||
|
||||
return <InputSQL
|
||||
value={sql}
|
||||
options={{
|
||||
readOnly: true,
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
SQLTab.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/* The first component of schema view form */
|
||||
export default function FormView({
|
||||
value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab, getSQLValue, onTabChange, firstEleRef}) {
|
||||
let defaultTab = 'General';
|
||||
let tabs = {};
|
||||
let tabsClassname = {};
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const classes = useStyles();
|
||||
const firstElement = useRef();
|
||||
|
||||
schema = schema || {fields: []};
|
||||
|
||||
/* Calculate the fields which depends on the current field
|
||||
deps has info on fields which the current field depends on. */
|
||||
const dependsOnField = useMemo(()=>{
|
||||
let res = {};
|
||||
schema.fields.forEach((field)=>{
|
||||
(field.deps || []).forEach((dep)=>{
|
||||
res[dep] = res[dep] || [];
|
||||
res[dep].push(field.id);
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
/* Prepare the array of components based on the types */
|
||||
schema.fields.forEach((f)=>{
|
||||
let modeSuppoted = true;
|
||||
if(f.mode) {
|
||||
modeSuppoted = (f.mode.indexOf(viewHelperProps.mode) > -1);
|
||||
}
|
||||
if(modeSuppoted) {
|
||||
let {visible, disabled, group, readonly, ...field} = f;
|
||||
group = group || defaultTab;
|
||||
|
||||
let verInLimit = (_.isUndefined(viewHelperProps.serverInfo) ? true :
|
||||
((_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)) &&
|
||||
(_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)) &&
|
||||
(_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version))));
|
||||
|
||||
let _readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');
|
||||
if(!_readonly) {
|
||||
_readonly = evalFunc(readonly, value);
|
||||
}
|
||||
|
||||
let _visible = true;
|
||||
|
||||
if(visible) {
|
||||
_visible = evalFunc(visible, value);
|
||||
}
|
||||
_visible = _visible && verInLimit;
|
||||
|
||||
disabled = evalFunc(disabled, value);
|
||||
|
||||
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
|
||||
/* Lets choose the path based on type */
|
||||
if(field.type === 'nested-tab') {
|
||||
/* Pass on the top schema */
|
||||
field.schema.top = schema.top;
|
||||
tabs[group].push(
|
||||
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps} formErr={formErr}
|
||||
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} />
|
||||
);
|
||||
} else if(field.type === 'collection') {
|
||||
/* Pass on the top schema */
|
||||
field.schema.top = schema.top;
|
||||
/* If its a collection, let data grid view handle it */
|
||||
tabs[group].push(
|
||||
useMemo(()=><DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
|
||||
schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
|
||||
{...field}/>, [value[field.id]])
|
||||
);
|
||||
} else {
|
||||
/* Its a form control */
|
||||
const hasError = field.id == formErr.name;
|
||||
/* When there is a change, the dependent values can change
|
||||
* lets pass the new changes to dependent and get the new values
|
||||
* from there as well.
|
||||
*/
|
||||
tabs[group].push(
|
||||
useMemo(()=><MappedFormControl
|
||||
inputRef={(ele)=>{
|
||||
if(firstEleRef && !firstEleRef.current) {
|
||||
firstEleRef.current = ele;
|
||||
}
|
||||
}}
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={field.id}
|
||||
value={value[field.id]}
|
||||
readonly={_readonly}
|
||||
disabled={disabled}
|
||||
visible={_visible}
|
||||
{...field}
|
||||
onChange={(value)=>{
|
||||
/* Get the changes on dependent fields as well */
|
||||
const depChange = (state)=>{
|
||||
field.depChange && _.merge(state, field.depChange(state) || {});
|
||||
(dependsOnField[field.id] || []).forEach((d)=>{
|
||||
d = _.find(schema.fields, (f)=>f.id==d);
|
||||
if(d.depChange) {
|
||||
_.merge(state, d.depChange(state) || {});
|
||||
}
|
||||
});
|
||||
return state;
|
||||
};
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat(field.id),
|
||||
value: value,
|
||||
depChange: depChange,
|
||||
});
|
||||
}}
|
||||
hasError={hasError}
|
||||
className={classes.controlRow}
|
||||
/>, [
|
||||
value[field.id],
|
||||
_readonly,
|
||||
disabled,
|
||||
_visible,
|
||||
hasError,
|
||||
classes.controlRow,
|
||||
...(field.deps || []).map((dep)=>value[dep])
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Add the SQL tab if required */
|
||||
let sqlTabActive = false;
|
||||
if(hasSQLTab) {
|
||||
let sqlTabName = gettext('SQL');
|
||||
sqlTabActive = (Object.keys(tabs).length === tabValue);
|
||||
/* Re-render and fetch the SQL tab when it is active */
|
||||
tabs[sqlTabName] = [
|
||||
useMemo(()=><SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />, [sqlTabActive]),
|
||||
];
|
||||
tabsClassname[sqlTabName] = classes.fullSpace;
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
firstElement.current && firstElement.current.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
onTabChange && onTabChange(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
|
||||
}, [tabValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(event, selTabValue) => {
|
||||
setTabValue(selTabValue);
|
||||
}}
|
||||
// indicatorColor="primary"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
action={(ref)=>ref && ref.updateIndicator()}
|
||||
>
|
||||
{Object.keys(tabs).map((tabName)=>{
|
||||
return <Tab key={tabName} label={tabName} />;
|
||||
})}
|
||||
</Tabs>
|
||||
</Box>
|
||||
{Object.keys(tabs).map((tabName, i)=>{
|
||||
return (
|
||||
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={isNested ? classes.fullSpace : tabsClassname[tabName]}>
|
||||
{tabs[tabName]}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</>);
|
||||
}
|
||||
|
||||
FormView.propTypes = {
|
||||
value: PropTypes.any,
|
||||
formErr: PropTypes.object,
|
||||
schema: CustomPropTypes.schemaUI.isRequired,
|
||||
viewHelperProps: PropTypes.object,
|
||||
isNested: PropTypes.bool,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func,
|
||||
hasSQLTab: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func,
|
||||
onTabChange: PropTypes.func,
|
||||
firstEleRef: CustomPropTypes.ref,
|
||||
};
|
199
web/pgadmin/static/js/SchemaView/MappedControl.jsx
Normal file
199
web/pgadmin/static/js/SchemaView/MappedControl.jsx
Normal file
@ -0,0 +1,199 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, InputSQL, FormInputColor, FormInputFileSelect, FormInputToggle, InputSwitch } from '../components/FormComponents';
|
||||
import { InputSelect, InputText } from '../components/FormComponents';
|
||||
import Privilege from '../components/Privilege';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
/* Control mapping for form view */
|
||||
function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, ...props}) {
|
||||
const name = id;
|
||||
const onTextChange = useCallback((e) => {
|
||||
let value = e;
|
||||
if(e && e.target) {
|
||||
value = e.target.value;
|
||||
}
|
||||
onChange && onChange(value);
|
||||
});
|
||||
|
||||
const onIntChange = useCallback((e) => {
|
||||
let value = e;
|
||||
if(e && e.target) {
|
||||
value = e.target.value;
|
||||
}
|
||||
if(!isNaN(parseInt(value))) {
|
||||
value = parseInt(value);
|
||||
}
|
||||
onChange && onChange(value);
|
||||
});
|
||||
|
||||
if(!visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/* The mapping uses Form* components as it comes with labels */
|
||||
switch (type) {
|
||||
case 'int':
|
||||
return <FormInputText name={name} value={value} onChange={onIntChange} className={className} inputRef={inputRef} {...props}/>;
|
||||
case 'text':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props}/>;
|
||||
case 'multiline':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} controlProps={{multiline: true}} {...props}/>;
|
||||
case 'password':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} type='password' inputRef={inputRef} {...props}/>;
|
||||
case 'select':
|
||||
return <FormInputSelect name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
case 'switch':
|
||||
return <FormInputSwitch name={name} value={value}
|
||||
onChange={(e)=>onTextChange(e.target.checked, e.target.name)} className={className}
|
||||
{...props} />;
|
||||
case 'checkbox':
|
||||
return <FormInputCheckbox name={name} value={value}
|
||||
onChange={(e)=>onTextChange(e.target.checked, e.target.name)} className={className}
|
||||
{...props} />;
|
||||
case 'toggle':
|
||||
return <FormInputToggle name={name} value={value}
|
||||
onChange={onTextChange} className={className}
|
||||
{...props} />;
|
||||
case 'color':
|
||||
return <FormInputColor name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
case 'file':
|
||||
return <FormInputFileSelect name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
case 'sql':
|
||||
return <InputSQL name={name} value={value} onChange={onTextChange} className={className} {...props}/>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
MappedFormControlBase.propTypes = {
|
||||
type: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.func,
|
||||
]).isRequired,
|
||||
value: PropTypes.any,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.object,
|
||||
]),
|
||||
visible: PropTypes.bool,
|
||||
inputRef: CustomPropTypes.ref,
|
||||
};
|
||||
|
||||
/* Control mapping for grid cell view */
|
||||
function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow,...props}) {
|
||||
const name = id;
|
||||
const onTextChange = useCallback((e) => {
|
||||
let value = e;
|
||||
if(e && e.target) {
|
||||
value = e.target.value;
|
||||
}
|
||||
|
||||
onCellChange(value);
|
||||
});
|
||||
|
||||
/* Some grid cells are based on options selected in other cells.
|
||||
* lets trigger a re-render for the row if optionsLoaded
|
||||
*/
|
||||
const optionsLoadedRerender = useCallback((res)=>{
|
||||
/* optionsLoaded is called when select options are fetched */
|
||||
optionsLoaded && optionsLoaded(res);
|
||||
reRenderRow && reRenderRow();
|
||||
});
|
||||
|
||||
if(!visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
/* The mapping does not need Form* components as labels are not needed for grid cells */
|
||||
switch(cell) {
|
||||
case 'int':
|
||||
case 'number':
|
||||
case 'text':
|
||||
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
case 'password':
|
||||
return <InputText name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
case 'select':
|
||||
return <InputSelect name={name} value={value} onChange={onTextChange} optionsLoaded={optionsLoadedRerender} {...props}/>;
|
||||
case 'switch':
|
||||
return <InputSwitch name={name} value={value}
|
||||
onChange={(e)=>onTextChange(e.target.checked, e.target.name)} {...props} />;
|
||||
case 'privilege':
|
||||
return <Privilege name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
MappedCellControlBase.propTypes = {
|
||||
cell: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.func,
|
||||
]).isRequired,
|
||||
value: PropTypes.any,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
reRenderRow: PropTypes.func,
|
||||
optionsLoaded: PropTypes.func,
|
||||
onCellChange: PropTypes.func,
|
||||
visible: PropTypes.bool
|
||||
};
|
||||
|
||||
const ALLOWED_PROPS_FIELD_COMMON = [
|
||||
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
|
||||
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
|
||||
'visible', 'autoFocus', 'helpMessage', 'className'
|
||||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_FORM = [
|
||||
'type', 'onChange'
|
||||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_CELL = [
|
||||
'cell', 'onCellChange', 'row', 'reRenderRow',
|
||||
];
|
||||
|
||||
|
||||
export const MappedFormControl = (props)=>{
|
||||
let newProps = {...props};
|
||||
let typeProps = evalFunc(newProps.type, newProps.value);
|
||||
if(typeof(typeProps) === 'object') {
|
||||
newProps = {
|
||||
...newProps,
|
||||
...typeProps,
|
||||
};
|
||||
} else {
|
||||
newProps.type = typeProps;
|
||||
}
|
||||
|
||||
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
|
||||
return <MappedFormControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM))}/>;
|
||||
};
|
||||
|
||||
export const MappedCellControl = (props)=>{
|
||||
let newProps = {...props};
|
||||
let cellProps = evalFunc(newProps.cell, newProps.row);
|
||||
if(typeof(cellProps) === 'object') {
|
||||
newProps = {
|
||||
...newProps,
|
||||
...cellProps,
|
||||
};
|
||||
} else {
|
||||
newProps.cell = cellProps;
|
||||
}
|
||||
|
||||
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
|
||||
return <MappedCellControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL))}/>;
|
||||
};
|
91
web/pgadmin/static/js/SchemaView/base_schema.ui.js
Normal file
91
web/pgadmin/static/js/SchemaView/base_schema.ui.js
Normal file
@ -0,0 +1,91 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* This is the base schema class for SchemaView.
|
||||
* A UI schema must inherit this to use SchemaView for UI.
|
||||
*/
|
||||
export default class BaseUISchema {
|
||||
constructor(defaults) {
|
||||
/* Pass the initial data to constructor so that
|
||||
they will set to defaults */
|
||||
this._defaults = defaults;
|
||||
|
||||
this.keys = null; // If set, other fields except keys will be filtered
|
||||
this.informText = null; // Inform text to show after save, this only saves it
|
||||
this._top = null;
|
||||
}
|
||||
|
||||
/* Top schema is helpful if this is used as child */
|
||||
set top(val) {
|
||||
this._top = val;
|
||||
}
|
||||
|
||||
get top() {
|
||||
/* If no top, I'm the top */
|
||||
return this._top || this;
|
||||
}
|
||||
|
||||
/* The original data before any changes */
|
||||
set origData(val) {
|
||||
this._origData = val;
|
||||
}
|
||||
|
||||
get origData() {
|
||||
return this._origData || {};
|
||||
}
|
||||
|
||||
/* Property allows to restrict setting this later */
|
||||
get defaults() {
|
||||
return this._defaults || {};
|
||||
}
|
||||
|
||||
/* ID key for the view state */
|
||||
get idAttribute() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
/* Schema fields, to be defined by inherited UI schema. Override this */
|
||||
get baseFields() {
|
||||
throw new Error('Property method \'baseFields()\' must be implemented.');
|
||||
}
|
||||
|
||||
/* Used by schema view component. Do not override this, it will
|
||||
concat base fields with extraFields.
|
||||
*/
|
||||
get fields() {
|
||||
/* Select only keys if specified */
|
||||
return this.baseFields
|
||||
.filter((field)=>this.keys ? this.keys.indexOf(field.id) > -1 : true);
|
||||
}
|
||||
|
||||
/* Check if current data is new or existing */
|
||||
isNew(state) {
|
||||
if(_.has(state, this.idAttribute)) {
|
||||
return _.isUndefined(state[this.idAttribute])
|
||||
|| _.isNull(state[this.idAttribute]);
|
||||
}
|
||||
/* Nested collection rows may or may not have idAttribute.
|
||||
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)
|
||||
*/
|
||||
if(_.has(state, 'cid')) {
|
||||
return !state.cid.startsWith('nn');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Called by SchemaView to validate data, return true indicates invalid.
|
||||
validate will receive two params state and setError func
|
||||
Eg - setError('fieldname', 'Some error').
|
||||
And return true if invalid, otherwise false.
|
||||
*/
|
||||
validate() {
|
||||
return false;
|
||||
}
|
||||
}
|
663
web/pgadmin/static/js/SchemaView/index.jsx
Normal file
663
web/pgadmin/static/js/SchemaView/index.jsx
Normal file
@ -0,0 +1,663 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import {Accordion, AccordionSummary, AccordionDetails} from '@material-ui/core';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import SettingsBackupRestoreIcon from '@material-ui/icons/SettingsBackupRestore';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import InfoIcon from '@material-ui/icons/InfoRounded';
|
||||
import HelpIcon from '@material-ui/icons/HelpRounded';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import diffArray from 'diff-arrays-of-objects';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {FormFooterMessage, MESSAGE_TYPE } from 'sources/components/FormComponents';
|
||||
import Theme from 'sources/Theme';
|
||||
import { PrimaryButton, DefaultButton, PgIconButton } from 'sources/components/Buttons';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import { minMaxValidator, numberValidator, integerValidator, emptyValidator, checkUniqueCol } from '../validators';
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import FormView from './FormView';
|
||||
import { pgAlertify } from '../helpers/legacyConnector';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import { parseApiError } from '../api_instance';
|
||||
|
||||
const useDialogStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
form: {
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
footer: {
|
||||
padding: theme.spacing(1),
|
||||
background: theme.otherVars.headerBg,
|
||||
display: 'flex',
|
||||
zIndex: 1010,
|
||||
...theme.mixins.panelBorder.top,
|
||||
},
|
||||
mappedControl: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
buttonMargin: {
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
}));
|
||||
|
||||
/* Compare the sessData with schema.origData
|
||||
schema.origData is set to incoming or default data
|
||||
*/
|
||||
function getChangedData(topSchema, mode, sessData, stringify=false) {
|
||||
let changedData = {};
|
||||
let isEdit = mode === 'edit';
|
||||
|
||||
/* The comparator and setter */
|
||||
const attrChanged = (currPath, change, force=false)=>{
|
||||
let origVal = _.get(topSchema.origData, currPath);
|
||||
let sessVal = _.get(sessData, currPath);
|
||||
let attrDefined = !_.isUndefined(origVal) && !_.isUndefined(sessVal) && !_.isNull(origVal) && !_.isNull(sessVal);
|
||||
|
||||
/* If the orig value was null and new one is empty string, then its a "no change" */
|
||||
/* If the orig value and new value are of different datatype but of same value(numeric) "no change" */
|
||||
if ((_.isEqual(origVal, sessVal)
|
||||
|| ((origVal === null || _.isUndefined(origVal)) && sessVal === '')
|
||||
|| (attrDefined ? _.isEqual(origVal.toString(), sessVal.toString()) : false))
|
||||
&& !force) {
|
||||
return;
|
||||
} else {
|
||||
change = change || _.get(sessData, currPath);
|
||||
_.set(changedData, currPath, stringify ? JSON.stringify(change) : change);
|
||||
}
|
||||
};
|
||||
|
||||
/* Will be called recursively as data can be nested */
|
||||
const parseChanges = (schema, accessPath, changedData)=>{
|
||||
schema.fields.forEach((field)=>{
|
||||
if(field.type === 'nested-tab') {
|
||||
/* its nested */
|
||||
parseChanges(field.schema, accessPath, changedData);
|
||||
} else {
|
||||
let currPath = accessPath.concat(field.id);
|
||||
/* Check for changes only if its in edit mode, otherwise everything is changed */
|
||||
if(isEdit && !_.isEqual(_.get(topSchema.origData, currPath), _.get(sessData, currPath))) {
|
||||
let change = null;
|
||||
if(field.type === 'collection') {
|
||||
/* Use diffArray package to get the array diff and extract the info
|
||||
cid is used to identify the rows uniquely */
|
||||
const changeDiff = diffArray(
|
||||
_.get(topSchema.origData, currPath),
|
||||
_.get(sessData, currPath),
|
||||
'cid'
|
||||
);
|
||||
change = {};
|
||||
if(changeDiff.added.length > 0) {
|
||||
change['added'] = cleanCid(changeDiff.added);
|
||||
}
|
||||
if(changeDiff.removed.length > 0) {
|
||||
change['deleted'] = cleanCid(changeDiff.removed.map((row)=>{
|
||||
/* Deleted records should be original, not the changed */
|
||||
return _.find(_.get(topSchema.origData, currPath), ['cid', row.cid]);
|
||||
}));
|
||||
}
|
||||
if(changeDiff.updated.length > 0) {
|
||||
change['changed'] = cleanCid(changeDiff.updated);
|
||||
}
|
||||
if(Object.keys(change).length > 0) {
|
||||
attrChanged(currPath, change, true);
|
||||
}
|
||||
} else {
|
||||
attrChanged(currPath);
|
||||
}
|
||||
} else if(!isEdit) {
|
||||
if(field.type === 'collection') {
|
||||
let change = cleanCid(_.get(sessData, currPath));
|
||||
attrChanged(currPath, change);
|
||||
} else {
|
||||
attrChanged(currPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
parseChanges(topSchema, [], changedData);
|
||||
return changedData;
|
||||
}
|
||||
|
||||
function validateSchema(schema, sessData, setError) {
|
||||
sessData = sessData || {};
|
||||
for(let field of schema.fields) {
|
||||
/* Skip id validation */
|
||||
if(schema.idAttribute == field.id) {
|
||||
continue;
|
||||
}
|
||||
/* If the field is has nested schema then validate the schema */
|
||||
if(field.schema && (field.schema instanceof BaseUISchema)) {
|
||||
/* A collection is an array */
|
||||
if(field.type === 'collection') {
|
||||
let rows = sessData[field.id] || [];
|
||||
|
||||
/* Validate duplicate rows */
|
||||
let dupInd = checkUniqueCol(rows, field.uniqueCol);
|
||||
if(dupInd > 0) {
|
||||
let uniqueColNames = _.filter(field.schema.fields, (uf)=>field.uniqueCol.indexOf(uf.id) > -1)
|
||||
.map((uf)=>uf.label).join(', ');
|
||||
setError(field.uniqueCol[0], gettext('%s in %s must be unique.', uniqueColNames, field.label));
|
||||
return true;
|
||||
}
|
||||
/* Loop through data */
|
||||
for(const row of rows) {
|
||||
if(validateSchema(field.schema, row, setError)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* A nested schema ? Recurse */
|
||||
if(validateSchema(field.schema, sessData, setError)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Normal field, default validations */
|
||||
let value = sessData[field.id];
|
||||
let message = null;
|
||||
if(field.noEmpty) {
|
||||
message = emptyValidator(field.label, value);
|
||||
}
|
||||
if(!message && (field.type == 'int' || field.type == 'numeric')) {
|
||||
message = minMaxValidator(field.label, value, field.min, field.max);
|
||||
}
|
||||
if(!message && field.type == 'int') {
|
||||
message = integerValidator(field.label, value);
|
||||
} else if(!message && field.type == 'numeric') {
|
||||
message = numberValidator(field.label, value);
|
||||
}
|
||||
if(message) {
|
||||
setError(field.id, message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return schema.validate(sessData, setError);
|
||||
}
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
RERENDER: 'rerender',
|
||||
};
|
||||
|
||||
/* The main function which manipulates the session state based on actions */
|
||||
/*
|
||||
The state is managed based on path array of a particular key
|
||||
For Eg. if the state is
|
||||
{
|
||||
key1: {
|
||||
ckey1: [
|
||||
{a: 0, b: 0},
|
||||
{a: 1, b: 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
The path for b in first row will be [key1, ckey1, 0, b]
|
||||
The path for second row of ckey1 will be [key1, ckey1, 1]
|
||||
The path for key1 is [key1]
|
||||
The state starts with path []
|
||||
*/
|
||||
const sessDataReducer = (state, action)=>{
|
||||
let data = _.cloneDeep(state);
|
||||
let rows, cid;
|
||||
switch(action.type) {
|
||||
case SCHEMA_STATE_ACTIONS.INIT:
|
||||
data = action.payload;
|
||||
break;
|
||||
case SCHEMA_STATE_ACTIONS.SET_VALUE:
|
||||
_.set(data, action.path, action.value);
|
||||
/* If there is any dep listeners get the changes */
|
||||
if(action.depChange) {
|
||||
_.set(data, action.depChange(data));
|
||||
}
|
||||
break;
|
||||
case SCHEMA_STATE_ACTIONS.ADD_ROW:
|
||||
/* Create id to identify a row uniquely, usefull when getting diff */
|
||||
cid = _.uniqueId('c');
|
||||
action.value['cid'] = cid;
|
||||
rows = (_.get(data, action.path)||[]).concat(action.value);
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
case SCHEMA_STATE_ACTIONS.DELETE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
rows.splice(action.value, 1);
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/* Remove cid key added by prepareData */
|
||||
function cleanCid(coll) {
|
||||
if(!coll) {
|
||||
return coll;
|
||||
}
|
||||
return coll.map((o)=>_.pickBy(o, (v, k)=>k!='cid'));
|
||||
}
|
||||
|
||||
function prepareData(origData) {
|
||||
_.forIn(origData, function (val) {
|
||||
if (_.isArray(val)) {
|
||||
val.forEach(function(el) {
|
||||
if (_.isObject(el)) {
|
||||
/* The each row in collection need to have an id to identify them uniquely
|
||||
This helps in easily getting what has changed */
|
||||
/* Nested collection rows may or may not have idAttribute.
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (_.isObject(val)) {
|
||||
prepareData(val);
|
||||
}
|
||||
});
|
||||
return origData;
|
||||
}
|
||||
|
||||
/* If its the dialog */
|
||||
function SchemaDialogView({
|
||||
getInitData, viewHelperProps, schema={}, ...props}) {
|
||||
const classes = useDialogStyles();
|
||||
/* Some useful states */
|
||||
const [dirty, setDirty] = useState(false);
|
||||
/* formErr has 2 keys - name and message.
|
||||
Footer message will be displayed if message is set.
|
||||
*/
|
||||
const [formErr, setFormErr] = useState({});
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [sqlTabActive, setSqlTabActive] = useState(false);
|
||||
const [formReady, setFormReady] = useState(false);
|
||||
const firstEleRef = useRef();
|
||||
const isNew = schema.isNew(schema.origData);
|
||||
/* The session data */
|
||||
const [sessData, sessDispatch] = useReducer(sessDataReducer, {});
|
||||
|
||||
useEffect(()=>{
|
||||
/* if sessData changes, validate the schema */
|
||||
if(!formReady) return;
|
||||
let isNotValid = validateSchema(schema, sessData, (name, message)=>{
|
||||
if(message) {
|
||||
setFormErr({
|
||||
name: name,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
});
|
||||
if(!isNotValid) setFormErr({});
|
||||
|
||||
/* check if anything changed */
|
||||
let dataChanged = Object.keys(getChangedData(schema, viewHelperProps.mode, sessData)).length > 0;
|
||||
setDirty(dataChanged);
|
||||
|
||||
/* tell the callbacks the data has changed */
|
||||
props.onDataChange && props.onDataChange(dataChanged);
|
||||
}, [sessData]);
|
||||
|
||||
useEffect(()=>{
|
||||
/* Docker on load focusses itself, so our focus should execute later */
|
||||
let focusTimeout = setTimeout(()=>{
|
||||
firstEleRef.current && firstEleRef.current.focus();
|
||||
}, 250);
|
||||
|
||||
/* Re-triggering focus on already focussed loses the focus */
|
||||
if(viewHelperProps.mode === 'edit') {
|
||||
setLoaderText('Loading...');
|
||||
/* If its an edit mode, get the initial data using getInitData
|
||||
getInitData should be a promise */
|
||||
if(!getInitData) {
|
||||
throw new Error('getInitData must be passed for edit');
|
||||
}
|
||||
getInitData && getInitData().then((data)=>{firstEleRef.current;
|
||||
data = data || {};
|
||||
/* Set the origData to incoming data, useful for comparing and reset */
|
||||
schema.origData = prepareData(data || {});
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: schema.origData,
|
||||
});
|
||||
setFormReady(true);
|
||||
setLoaderText('');
|
||||
|
||||
});
|
||||
} else {
|
||||
/* Use the defaults as the initital data */
|
||||
schema.origData = prepareData(schema.defaults);
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: schema.origData,
|
||||
});
|
||||
setFormReady(true);
|
||||
setLoaderText('');
|
||||
}
|
||||
|
||||
/* Clear the focus timeout it unmounted */
|
||||
return ()=>clearTimeout(focusTimeout);
|
||||
}, []);
|
||||
|
||||
const onResetClick = ()=>{
|
||||
const resetIt = ()=>{
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: schema.origData,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
/* Confirm before reset */
|
||||
if(props.confirmOnCloseReset) {
|
||||
pgAlertify().confirm(
|
||||
gettext('Warning'),
|
||||
gettext('Changes will be lost. Are you sure you want to reset?'),
|
||||
resetIt,
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
).set('labels', {
|
||||
ok: gettext('Yes'),
|
||||
cancel: gettext('No'),
|
||||
}).show();
|
||||
} else {
|
||||
resetIt();
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveClick = ()=>{
|
||||
setSaving(true);
|
||||
setLoaderText('Saving...');
|
||||
/* Get the changed data */
|
||||
let data = getChangedData(schema, viewHelperProps.mode, sessData);
|
||||
|
||||
/* Add the id when in edit mode */
|
||||
if(viewHelperProps.mode === 'edit') {
|
||||
data[schema.idAttribute] = schema.origData[schema.idAttribute];
|
||||
} else {
|
||||
/* If new then merge the changed data with origData */
|
||||
data = _.merge(schema.origData, data);
|
||||
}
|
||||
props.onSave(isNew, data)
|
||||
.then(()=>{
|
||||
if(schema.informText) {
|
||||
pgAlertify().alert(
|
||||
gettext('Warning'),
|
||||
schema.informText,
|
||||
);
|
||||
}
|
||||
}).catch((err)=>{
|
||||
setFormErr({
|
||||
name: 'apierror',
|
||||
message: parseApiError(err),
|
||||
});
|
||||
}).finally(()=>{
|
||||
setSaving(false);
|
||||
setLoaderText('');
|
||||
});
|
||||
};
|
||||
|
||||
const onErrClose = useCallback(()=>{
|
||||
/* Unset the error message, but not the name */
|
||||
setFormErr((prev)=>({
|
||||
...prev,
|
||||
message: '',
|
||||
}));
|
||||
});
|
||||
|
||||
const getSQLValue = ()=>{
|
||||
/* Called when SQL tab is active */
|
||||
if(dirty) {
|
||||
if(!formErr.name) {
|
||||
let changeData = getChangedData(schema, viewHelperProps.mode, sessData, true);
|
||||
/* Call the passed incoming getSQLValue func to get the SQL
|
||||
return of getSQLValue should be a promise.
|
||||
*/
|
||||
return props.getSQLValue(isNew, changeData);
|
||||
} else {
|
||||
return Promise.resolve('-- ' + gettext('Definition incomplete.'));
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve('-- ' + gettext('No updates.'));
|
||||
}
|
||||
};
|
||||
|
||||
/* I am Groot */
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.form}>
|
||||
<Loader message={loaderText}/>
|
||||
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
|
||||
schema={schema} accessPath={[]} dataDispatch={sessDispatch}
|
||||
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} onTabChange={(i, tabName, sqlActive)=>setSqlTabActive(sqlActive)}
|
||||
firstEleRef={firstEleRef} />
|
||||
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={sqlTabActive ? '' : formErr.message}
|
||||
onClose={onErrClose} />
|
||||
</Box>
|
||||
<Box className={classes.footer}>
|
||||
{useMemo(()=><Box>
|
||||
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
|
||||
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
|
||||
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
|
||||
</Box>, [])}
|
||||
<Box marginLeft="auto">
|
||||
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
|
||||
{gettext('Close')}
|
||||
</DefaultButton>
|
||||
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
|
||||
{gettext('Reset')}
|
||||
</DefaultButton>
|
||||
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
|
||||
{gettext('Save')}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaDialogView.propTypes = {
|
||||
getInitData: PropTypes.func,
|
||||
viewHelperProps: PropTypes.shape({
|
||||
mode: PropTypes.string.isRequired,
|
||||
serverInfo: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
version: PropTypes.number,
|
||||
}),
|
||||
inCatalog: PropTypes.bool,
|
||||
}).isRequired,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onHelp: PropTypes.func,
|
||||
onDataChange: PropTypes.func,
|
||||
confirmOnCloseReset: PropTypes.bool,
|
||||
hasSQL: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func,
|
||||
disableSqlHelp: PropTypes.bool,
|
||||
};
|
||||
|
||||
const usePropsStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
height: '100%',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
controlRow: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
form: {
|
||||
padding: theme.spacing(1),
|
||||
overflow: 'auto',
|
||||
flexGrow: 1,
|
||||
},
|
||||
toolbar: {
|
||||
padding: theme.spacing(0.5),
|
||||
background: theme.palette.background.default,
|
||||
...theme.mixins.panelBorder.bottom,
|
||||
},
|
||||
buttonMargin: {
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
}));
|
||||
|
||||
/* If its the properties tab */
|
||||
function SchemaPropertiesView({
|
||||
getInitData, viewHelperProps, schema={}, ...props}) {
|
||||
const classes = usePropsStyles();
|
||||
let defaultTab = 'General';
|
||||
let tabs = {};
|
||||
const [origData, setOrigData] = useState({});
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
|
||||
useEffect(()=>{
|
||||
setLoaderText('Loading...');
|
||||
getInitData().then((data)=>{
|
||||
data = data || {};
|
||||
setOrigData(data || {});
|
||||
setLoaderText('');
|
||||
});
|
||||
}, [getInitData]);
|
||||
|
||||
/* A simple loop to get all the controls for the fields */
|
||||
schema.fields.forEach((f)=>{
|
||||
let {visible, disabled, group, readonly, ...field} = f;
|
||||
group = group || defaultTab;
|
||||
|
||||
let verInLimit = (_.isUndefined(viewHelperProps.serverInfo) ? true :
|
||||
((_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)) &&
|
||||
(_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)) &&
|
||||
(_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version))));
|
||||
|
||||
let _visible = true;
|
||||
if(field.mode) {
|
||||
_visible = (field.mode.indexOf(viewHelperProps.mode) > -1);
|
||||
}
|
||||
if(_visible && visible) {
|
||||
_visible = evalFunc(visible, origData);
|
||||
}
|
||||
|
||||
disabled = evalFunc(disabled, origData);
|
||||
readonly = true;
|
||||
if(_visible && verInLimit) {
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
tabs[group].push(
|
||||
<MappedFormControl
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={field.id}
|
||||
value={origData[field.id]}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
visible={_visible}
|
||||
{...field}
|
||||
className={classes.controlRow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Loader message={loaderText}/>
|
||||
<Box className={classes.toolbar}>
|
||||
<PgIconButton
|
||||
data-test="help" onClick={()=>props.onHelp(true, false)} icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
title="SQL help for this object type." className={classes.buttonMargin} />
|
||||
<PgIconButton data-test="edit"
|
||||
onClick={props.onEdit} icon={<EditIcon />} title="Edit the object" />
|
||||
</Box>
|
||||
<Box className={classes.form}>
|
||||
<Box>
|
||||
{Object.keys(tabs).map((tabName)=>{
|
||||
let id = tabName.replace(' ', '');
|
||||
return (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`${id}-content`}
|
||||
id={`${id}-header`}
|
||||
>
|
||||
{tabName}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box style={{width: '100%'}}>
|
||||
{tabs[tabName]}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaPropertiesView.propTypes = {
|
||||
getInitData: PropTypes.func.isRequired,
|
||||
viewHelperProps: PropTypes.shape({
|
||||
mode: PropTypes.string.isRequired,
|
||||
serverInfo: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
version: PropTypes.number,
|
||||
}),
|
||||
inCatalog: PropTypes.bool,
|
||||
}).isRequired,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
onHelp: PropTypes.func,
|
||||
disableSqlHelp: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
};
|
||||
|
||||
export default function SchemaView({formType, ...props}) {
|
||||
/* Switch the view based on formType */
|
||||
if(formType === 'tab') {
|
||||
return (
|
||||
<Theme>
|
||||
<SchemaPropertiesView {...props}/>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Theme>
|
||||
<SchemaDialogView {...props}/>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
|
||||
SchemaView.propTypes = {
|
||||
formType: PropTypes.oneOf(['tab', 'dialog']),
|
||||
};
|
89
web/pgadmin/static/js/Theme/dark.js
Normal file
89
web/pgadmin/static/js/Theme/dark.js
Normal file
@ -0,0 +1,89 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The dark theme */
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import { darken } from '@material-ui/core/styles/colorManipulator';
|
||||
|
||||
export default function(basicSettings) {
|
||||
return createMuiTheme(basicSettings, {
|
||||
palette: {
|
||||
default: {
|
||||
main: '#6b6b6b',
|
||||
contrastText: '#fff',
|
||||
borderColor: '#2e2e2e',
|
||||
disabledBorderColor: '#2e2e2e',
|
||||
disabledContrastText: '#fff',
|
||||
hoverMain: '#303030',
|
||||
hoverContrastText: '#fff',
|
||||
hoverBorderColor: '#2e2e2e',
|
||||
},
|
||||
primary: {
|
||||
main: '#234d6e',
|
||||
light: '#d6effc',
|
||||
contrastText: '#fff',
|
||||
hoverMain: darken('#234d6e', 0.25),
|
||||
hoverBorderColor: darken('#234d6e', 0.25),
|
||||
disabledMain: '#234d6e',
|
||||
},
|
||||
success: {
|
||||
main: '#26852B',
|
||||
light: '#2B472C',
|
||||
contrastText: '#000',
|
||||
},
|
||||
error: {
|
||||
main: '#da6758',
|
||||
light: '#212121',
|
||||
contrastText: '#fff',
|
||||
lighter: '#212121',
|
||||
},
|
||||
warning: {
|
||||
main: '#eea236',
|
||||
light: '#b18d5a',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
info: {
|
||||
main: '#fde74c',
|
||||
},
|
||||
grey: {
|
||||
'200': '#424242',
|
||||
'400': '#303030',
|
||||
'600': '#2e2e2e',
|
||||
'800': '#212121',
|
||||
},
|
||||
text: {
|
||||
primary: '#d4d4d4',
|
||||
},
|
||||
background: {
|
||||
paper: '#212121',
|
||||
default: '#212121',
|
||||
}
|
||||
},
|
||||
custom: {
|
||||
icon: {
|
||||
main: '#6b6b6b',
|
||||
contrastText: '#fff',
|
||||
borderColor: '#2e2e2e',
|
||||
disabledMain: '#6b6b6b',
|
||||
disabledContrastText: '#fff',
|
||||
disabledBorderColor: '#2e2e2e',
|
||||
hoverMain: '#303030',
|
||||
hoverContrastText: '#fff',
|
||||
}
|
||||
},
|
||||
otherVars: {
|
||||
borderColor: '#4a4a4a',
|
||||
inputBorderColor: '#6b6b6b',
|
||||
inputDisabledBg: 'inherit',
|
||||
headerBg: '#424242',
|
||||
activeColor: '#d4d4d4',
|
||||
tableBg: '#424242',
|
||||
}
|
||||
});
|
||||
}
|
87
web/pgadmin/static/js/Theme/high_contrast.js
Normal file
87
web/pgadmin/static/js/Theme/high_contrast.js
Normal file
@ -0,0 +1,87 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The dark theme */
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
|
||||
export default function(basicSettings) {
|
||||
return createMuiTheme(basicSettings, {
|
||||
palette: {
|
||||
default: {
|
||||
main: 'transparent',
|
||||
contrastText: '#84d6ff',
|
||||
borderColor: '#84d6ff',
|
||||
disabledBorderColor: '#8B9CAD',
|
||||
disabledContrastText: '#8B9CAD',
|
||||
hoverMain: 'transparent',
|
||||
hoverContrastText: '#fff',
|
||||
hoverBorderColor: '#fff',
|
||||
},
|
||||
primary: {
|
||||
main: '#84D6FF',
|
||||
light: '#84D6FF',
|
||||
contrastText: '#010B15',
|
||||
hoverMain: '#fff',
|
||||
hoverBorderColor: '#fff',
|
||||
disabledMain: '#8B9CAD',
|
||||
},
|
||||
success: {
|
||||
main: '#45D48A',
|
||||
light: '#010B15',
|
||||
contrastText: '#000',
|
||||
},
|
||||
error: {
|
||||
main: '#EE7A55',
|
||||
light: '#EE7A55',
|
||||
contrastText: '#010B15',
|
||||
},
|
||||
warning: {
|
||||
main: '#F4D35E',
|
||||
light: '#F4D35E',
|
||||
contrastText: '#010B15',
|
||||
},
|
||||
info: {
|
||||
main: '#fde74c',
|
||||
},
|
||||
grey: {
|
||||
'200': '#8B9CAD',
|
||||
'400': '#2D3A48',
|
||||
'600': '#1F2932',
|
||||
'800': '#010B15',
|
||||
},
|
||||
text: {
|
||||
primary: '#fff',
|
||||
},
|
||||
background: {
|
||||
paper: '#010B15',
|
||||
default: '#010B15',
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
icon: {
|
||||
main: '#010B15',
|
||||
contrastText: '#fff',
|
||||
borderColor: '#fff',
|
||||
disabledMain: '#1F2932',
|
||||
disabledContrastText: '#8B9CAD',
|
||||
disabledBorderColor: '#8B9CAD',
|
||||
hoverMain: '#fff',
|
||||
hoverContrastText: '#010B15',
|
||||
}
|
||||
},
|
||||
otherVars: {
|
||||
borderColor: '#4a4a4a',
|
||||
inputBorderColor: '#6b6b6b',
|
||||
inputDisabledBg: '#1F2932',
|
||||
headerBg: '#010B15',
|
||||
activeColor: '#d4d4d4',
|
||||
tableBg: '#010B15',
|
||||
}
|
||||
});
|
||||
}
|
353
web/pgadmin/static/js/Theme/index.jsx
Normal file
353
web/pgadmin/static/js/Theme/index.jsx
Normal file
@ -0,0 +1,353 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The complete styling file for Material-UI components used
|
||||
* This will become the main theme file for pgAdmin. All the
|
||||
* custom themes info will come here.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
import getStandardTheme from './standard';
|
||||
import getDarkTheme from './dark';
|
||||
import getHightContrastTheme from './high_contrast';
|
||||
|
||||
/* Common settings across all themes */
|
||||
let basicSettings = createMuiTheme();
|
||||
basicSettings = createMuiTheme(basicSettings, {
|
||||
typography: {
|
||||
fontSize: 14,
|
||||
htmlFontSize: 14,
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
overrides: {
|
||||
MuiTabs: {
|
||||
root: {
|
||||
minHeight: 0,
|
||||
}
|
||||
},
|
||||
PrivateTabIndicator: {
|
||||
root: {
|
||||
height: '3px',
|
||||
transition: basicSettings.transitions.create(['all'], {duration: '150ms'}),
|
||||
}
|
||||
},
|
||||
MuiTab: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
minHeight: 0,
|
||||
padding: '5px 10px',
|
||||
[basicSettings.breakpoints.up('xs')]: {
|
||||
minWidth: 0,
|
||||
},
|
||||
[basicSettings.breakpoints.up('sm')]: {
|
||||
minWidth: 0,
|
||||
},
|
||||
[basicSettings.breakpoints.up('md')]: {
|
||||
minWidth: 0,
|
||||
},
|
||||
[basicSettings.breakpoints.up('lg')]: {
|
||||
minWidth: 0,
|
||||
},
|
||||
},
|
||||
textColorInherit: {
|
||||
textTransform: 'none',
|
||||
opacity: 1,
|
||||
}
|
||||
},
|
||||
MuiButton: {
|
||||
root: {
|
||||
textTransform: 'none,',
|
||||
padding: basicSettings.spacing(0.5, 1.5),
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.65,
|
||||
}
|
||||
},
|
||||
contained: {
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
boxShadow: 'none',
|
||||
}
|
||||
},
|
||||
outlined: {
|
||||
padding: basicSettings.spacing(0.375, 1),
|
||||
},
|
||||
startIcon: {
|
||||
marginRight: basicSettings.spacing(0.5),
|
||||
}
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
multiline: {
|
||||
padding: '0px',
|
||||
},
|
||||
input: {
|
||||
padding: basicSettings.spacing(0.75, 1.5),
|
||||
borderRadius: 'inherit',
|
||||
},
|
||||
inputMultiline: {
|
||||
padding: basicSettings.spacing(0.75, 1.5),
|
||||
},
|
||||
adornedEnd: {
|
||||
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',
|
||||
}
|
||||
},
|
||||
MuiAccordionSummary: {
|
||||
root: {
|
||||
minHeight: 0,
|
||||
'&.Mui-expanded': {
|
||||
minHeight: 0,
|
||||
},
|
||||
padding: basicSettings.spacing(0, 1),
|
||||
fontWeight: basicSettings.typography.fontWeightBold,
|
||||
},
|
||||
content: {
|
||||
margin: basicSettings.spacing(1),
|
||||
'&.Mui-expanded': {
|
||||
margin: basicSettings.spacing(1),
|
||||
}
|
||||
},
|
||||
expandIcon: {
|
||||
order: -1,
|
||||
}
|
||||
},
|
||||
MuiAccordionDetails: {
|
||||
root: {
|
||||
padding: basicSettings.spacing(1),
|
||||
}
|
||||
},
|
||||
MuiFormControlLabel: {
|
||||
root: {
|
||||
marginBottom: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
}
|
||||
},
|
||||
MuiFormHelperText: {
|
||||
root: {
|
||||
fontSize: '1em',
|
||||
}
|
||||
},
|
||||
MuiTypography: {
|
||||
body1: {
|
||||
fontSize: '1em',
|
||||
}
|
||||
}
|
||||
},
|
||||
transitions: {
|
||||
duration: {
|
||||
shortest: 50,
|
||||
shorter: 100,
|
||||
short: 150,
|
||||
standard: 200,
|
||||
complex: 175,
|
||||
enteringScreen: 125,
|
||||
leavingScreen: 95,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
MuiTextField: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
MuiButton: {
|
||||
disableTouchRipple: true,
|
||||
},
|
||||
MuiIconButton: {
|
||||
size: 'small',
|
||||
disableTouchRipple: true,
|
||||
},
|
||||
MuiAccordion: {
|
||||
defaultExpanded: true,
|
||||
},
|
||||
MuiTab: {
|
||||
textColor: 'inherit',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/* Get the final theme after merging base theme with selected theme */
|
||||
function getFinalTheme(baseTheme) {
|
||||
let mixins = {
|
||||
panelBorder: {
|
||||
border: '1px solid '+baseTheme.otherVars.borderColor,
|
||||
top: {
|
||||
borderTop: '1px solid '+baseTheme.otherVars.borderColor,
|
||||
},
|
||||
bottom: {
|
||||
borderBottom: '1px solid '+baseTheme.otherVars.borderColor,
|
||||
},
|
||||
right: {
|
||||
borderRight: '1px solid '+baseTheme.otherVars.borderColor,
|
||||
}
|
||||
},
|
||||
nodeIcon: {
|
||||
backgroundPosition: 'center',
|
||||
padding: baseTheme.spacing(0, 1.5),
|
||||
}
|
||||
};
|
||||
|
||||
return createMuiTheme({
|
||||
mixins: mixins,
|
||||
overrides: {
|
||||
MuiOutlinedInput: {
|
||||
notchedOutline: {
|
||||
borderColor: baseTheme.otherVars.inputBorderColor,
|
||||
}
|
||||
},
|
||||
MuiTabs: {
|
||||
root: {
|
||||
backgroundColor: baseTheme.otherVars.headerBg,
|
||||
...mixins.panelBorder.bottom
|
||||
},
|
||||
indicator: {
|
||||
backgroundColor: baseTheme.otherVars.activeColor,
|
||||
}
|
||||
},
|
||||
MuiFormLabel: {
|
||||
root: {
|
||||
color: baseTheme.palette.text.primary,
|
||||
fontSize: baseTheme.typography.fontSize,
|
||||
},
|
||||
asterisk: {
|
||||
color: baseTheme.palette.error.main,
|
||||
}
|
||||
},
|
||||
MuiInputBase: {
|
||||
root: {
|
||||
backgroundColor: baseTheme.palette.background.default,
|
||||
},
|
||||
inputMultiline: {
|
||||
fontSize: baseTheme.typography.fontSize,
|
||||
height: 'unset',
|
||||
backgroundColor: baseTheme.palette.background.default,
|
||||
'&[readonly]': {
|
||||
backgroundColor: baseTheme.palette.inputDisabledBg,
|
||||
}
|
||||
},
|
||||
input: {
|
||||
fontSize: baseTheme.typography.fontSize,
|
||||
height: 'unset',
|
||||
backgroundColor: baseTheme.palette.background.default,
|
||||
'&[readonly]': {
|
||||
backgroundColor: baseTheme.otherVars.inputDisabledBg,
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiIconButton: {
|
||||
root: {
|
||||
color: baseTheme.palette.text.primary,
|
||||
}
|
||||
},
|
||||
MuiAccordion: {
|
||||
root: {
|
||||
...mixins.panelBorder,
|
||||
}
|
||||
},
|
||||
MuiAccordionSummary: {
|
||||
root: {
|
||||
...mixins.panelBorder.bottom,
|
||||
backgroundColor: baseTheme.otherVars.headerBg,
|
||||
}
|
||||
},
|
||||
MuiSwitch: {
|
||||
root: {
|
||||
width: 54,
|
||||
height: 28,
|
||||
padding: '7px 12px',
|
||||
},
|
||||
switchBase: {
|
||||
padding: baseTheme.spacing(0.5),
|
||||
'&.Mui-checked': {
|
||||
color: baseTheme.palette.success.main,
|
||||
transform: 'translateX(24px)',
|
||||
},
|
||||
'&.Mui-checked + .MuiSwitch-track': {
|
||||
backgroundColor: baseTheme.palette.success.light,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCheckbox: {
|
||||
root: {
|
||||
padding: '0px',
|
||||
color: baseTheme.otherVars.inputBorderColor,
|
||||
}
|
||||
},
|
||||
MuiToggleButton: {
|
||||
root: {
|
||||
paddingRight: baseTheme.spacing(2.5),
|
||||
paddingLeft: baseTheme.spacing(0.5),
|
||||
color: 'abc',
|
||||
'&:hover':{
|
||||
backgroundColor: 'abc',
|
||||
},
|
||||
'&$selected': {
|
||||
color: 'abc',
|
||||
backgroundColor: 'abc',
|
||||
'&:hover':{
|
||||
backgroundColor: 'abc',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}, baseTheme);
|
||||
}
|
||||
|
||||
/* Theme wrapper used by DOM containers to apply theme */
|
||||
/* In future, this will be moved to App container */
|
||||
export default function Theme(props) {
|
||||
const theme = useMemo(()=>{
|
||||
/* We'll remove this in future, we can get the value from preferences directly */
|
||||
let themeName = document.querySelector('link[data-theme]')?.getAttribute('data-theme');
|
||||
let baseTheme = getStandardTheme(basicSettings);
|
||||
switch(themeName) {
|
||||
case 'dark':
|
||||
baseTheme = getDarkTheme(baseTheme);
|
||||
break;
|
||||
case 'high_contrast':
|
||||
baseTheme = getHightContrastTheme(baseTheme);
|
||||
break;
|
||||
}
|
||||
return getFinalTheme(baseTheme);
|
||||
}, []);
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
{props.children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Theme.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
};
|
95
web/pgadmin/static/js/Theme/standard.js
Normal file
95
web/pgadmin/static/js/Theme/standard.js
Normal file
@ -0,0 +1,95 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The standard theme */
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import { fade, darken } from '@material-ui/core/styles/colorManipulator';
|
||||
|
||||
export default function(basicSettings) {
|
||||
return createMuiTheme(basicSettings, {
|
||||
palette: {
|
||||
default: {
|
||||
main: '#fff',
|
||||
contrastText: '#222',
|
||||
borderColor: '#bac1cd',
|
||||
disabledBorderColor: '#bac1cd',
|
||||
disabledContrastText: '#222',
|
||||
hoverMain: '#ebeef3',
|
||||
hoverContrastText: '#222',
|
||||
hoverBorderColor: '#bac1cd',
|
||||
},
|
||||
primary: {
|
||||
main: '#326690',
|
||||
light: '#d6effc',
|
||||
contrastText: '#fff',
|
||||
hoverMain: darken('#326690', 0.25),
|
||||
hoverBorderColor: darken('#326690', 0.25),
|
||||
disabledMain: '#326690',
|
||||
},
|
||||
success: {
|
||||
main: '#26852B',
|
||||
light: '#D9ECDA',
|
||||
contrastText: '#000',
|
||||
},
|
||||
error: {
|
||||
main: '#CC0000',
|
||||
light: '#FAECEC',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
warning: {
|
||||
main: '#eea236',
|
||||
light: '#fce5c5',
|
||||
contrastText: '#000',
|
||||
},
|
||||
info: {
|
||||
main: '#fde74c',
|
||||
},
|
||||
grey: {
|
||||
'200': '#f3f5f9',
|
||||
'400': '#ebeef3',
|
||||
'600': '#bac1cd',
|
||||
'800': '#848ea0',
|
||||
},
|
||||
text: {
|
||||
primary: '#222',
|
||||
},
|
||||
background: {
|
||||
paper: '#fff',
|
||||
default: '#fff',
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
icon: {
|
||||
main: '#fff',
|
||||
contrastText: '#222',
|
||||
borderColor: '#bac1cd',
|
||||
disabledMain: '#fff',
|
||||
disabledContrastText: '#222',
|
||||
disabledBorderColor: '#bac1cd',
|
||||
hoverMain: '#ebeef3',
|
||||
hoverContrastText: '#222',
|
||||
}
|
||||
},
|
||||
otherVars: {
|
||||
reactSelect: {
|
||||
padding: '5px 8px',
|
||||
},
|
||||
borderColor: '#dde0e6',
|
||||
loader: {
|
||||
backgroundColor: fade('#000', 0.65),
|
||||
color: '#fff',
|
||||
},
|
||||
inputBorderColor: '#dde0e6',
|
||||
inputDisabledBg: '#f3f5f9',
|
||||
headerBg: '#fff',
|
||||
activeColor: '#326690',
|
||||
tableBg: '#fff',
|
||||
}
|
||||
});
|
||||
}
|
45
web/pgadmin/static/js/api_instance.js
Normal file
45
web/pgadmin/static/js/api_instance.js
Normal file
@ -0,0 +1,45 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import axios from 'axios';
|
||||
|
||||
/* Get the axios instance to call back end APIs.
|
||||
Do not import axios directly, instead use this */
|
||||
export default function getApiInstance(headers={}) {
|
||||
const api = axios.create({
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
[pgAdmin.csrf_token_header]: pgAdmin.csrf_token,
|
||||
...headers,
|
||||
}
|
||||
});
|
||||
return api;
|
||||
}
|
||||
|
||||
export function parseApiError(error) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if(error.response.headers['content-type'] == 'application/json') {
|
||||
return error.response.data.errormsg;
|
||||
} else {
|
||||
return error.response.statusText;
|
||||
}
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
return gettext('Connection to pgAdmin server has been lost');
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
return error.message;
|
||||
}
|
||||
}
|
106
web/pgadmin/static/js/components/Buttons.jsx
Normal file
106
web/pgadmin/static/js/components/Buttons.jsx
Normal file
@ -0,0 +1,106 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { Button, makeStyles, Tooltip } from '@material-ui/core';
|
||||
import React, { forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
primaryButton: {
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.disabledMain,
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.hoverMain,
|
||||
borderColor: theme.palette.primary.hoverBorderColor,
|
||||
},
|
||||
},
|
||||
defaultButton: {
|
||||
backgroundColor: theme.palette.default.main,
|
||||
color: theme.palette.default.contrastText,
|
||||
border: '1px solid '+theme.palette.default.borderColor,
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.default.disabledContrastText,
|
||||
borderColor: theme.palette.default.disabledBorderColor
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.default.hoverMain,
|
||||
color: theme.palette.default.hoverContrastText,
|
||||
borderColor: theme.palette.default.hoverBorderColor,
|
||||
}
|
||||
},
|
||||
iconButton: {
|
||||
padding: '3px 6px',
|
||||
borderColor: theme.custom.icon.borderColor,
|
||||
color: theme.custom.icon.contrastText,
|
||||
backgroundColor: theme.custom.icon.main,
|
||||
'&.Mui-disabled': {
|
||||
borderColor: theme.custom.icon.disabledBorderColor,
|
||||
backgroundColor: theme.custom.icon.disabledMain,
|
||||
color: theme.custom.icon.disabledContrastText,
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.custom.icon.hoverMain,
|
||||
color: theme.custom.icon.hoverContrastText,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/* pgAdmin primary button */
|
||||
export const PrimaryButton = forwardRef((props, ref)=>{
|
||||
let {children, className, ...otherProps} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button ref={ref} variant="contained" color="primary" className={clsx(classes.primaryButton, className)} {...otherProps}>{children}</Button>
|
||||
);
|
||||
});
|
||||
PrimaryButton.displayName = 'PrimaryButton';
|
||||
PrimaryButton.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
};
|
||||
|
||||
/* pgAdmin default button */
|
||||
export const DefaultButton = forwardRef((props, ref)=>{
|
||||
let {children, className, ...otherProps} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button ref={ref} variant="outlined" color="default" className={clsx(classes.defaultButton, className)} {...otherProps}>{children}</Button>
|
||||
);
|
||||
});
|
||||
DefaultButton.displayName = 'DefaultButton';
|
||||
DefaultButton.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
};
|
||||
|
||||
/* pgAdmin Icon button, takes Icon component as input */
|
||||
export const PgIconButton = forwardRef(({icon, title, className, ...props}, ref)=>{
|
||||
const classes = useStyles();
|
||||
|
||||
/* Tooltip does not work for disabled items */
|
||||
return (
|
||||
<Tooltip title={title || ''} aria-label={title || ''}>
|
||||
<DefaultButton ref={ref} style={{minWidth: 0}} className={clsx(classes.iconButton, className)} {...props}>
|
||||
{icon}
|
||||
</DefaultButton>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
PgIconButton.displayName = 'PgIconButton';
|
||||
PgIconButton.propTypes = {
|
||||
icon: CustomPropTypes.children,
|
||||
title: PropTypes.string.isRequired,
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
};
|
40
web/pgadmin/static/js/components/CodeMirror.jsx
Normal file
40
web/pgadmin/static/js/components/CodeMirror.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/* React wrapper for CodeMirror */
|
||||
export default function CodeMirror({name, value, options}) {
|
||||
const taRef = useRef();
|
||||
const cmObj = useRef();
|
||||
|
||||
useEffect(()=>{
|
||||
/* Create the object only once on mount */
|
||||
cmObj.current = new OrigCodeMirror.fromTextArea(
|
||||
taRef.current, options);
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
/* Refresh when value changes */
|
||||
if(cmObj.current) {
|
||||
cmObj.current.setValue(value);
|
||||
cmObj.current.refresh();
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return <textarea ref={taRef} name={name} />;
|
||||
}
|
||||
|
||||
CodeMirror.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.object
|
||||
};
|
842
web/pgadmin/static/js/components/FormComponents.jsx
Normal file
842
web/pgadmin/static/js/components/FormComponents.jsx
Normal file
@ -0,0 +1,842 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
/* Common form components used in pgAdmin */
|
||||
|
||||
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Box, FormControl, OutlinedInput, FormHelperText,
|
||||
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel } from '@material-ui/core';
|
||||
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
|
||||
import ReportProblemIcon from '@material-ui/icons/ReportProblemRounded';
|
||||
import InfoIcon from '@material-ui/icons/InfoRounded';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import CheckIcon from '@material-ui/icons/CheckCircleOutlineRounded';
|
||||
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
||||
import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded';
|
||||
import Select, {components as RSComponents} from 'react-select';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import Pickr from '@simonwep/pickr';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CodeMirror from './CodeMirror';
|
||||
import gettext from 'sources/gettext';
|
||||
import { showFileDialog } from '../helpers/legacyConnector';
|
||||
import _ from 'lodash';
|
||||
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
formRoot: {
|
||||
padding: '1rem'
|
||||
},
|
||||
img: {
|
||||
maxWidth: '100%',
|
||||
height: 'auto'
|
||||
},
|
||||
info: {
|
||||
color: theme.palette.info.main,
|
||||
marginLeft: '0.25rem',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
formLabel: {
|
||||
margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
|
||||
display: 'flex',
|
||||
},
|
||||
formLabelError: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
sql: {
|
||||
height: '100%',
|
||||
},
|
||||
optionIcon: {
|
||||
...theme.mixins.nodeIcon,
|
||||
},
|
||||
colorBtn: {
|
||||
height: theme.spacing(3.5),
|
||||
minHeight: theme.spacing(3.5),
|
||||
width: theme.spacing(3.5),
|
||||
minWidth: theme.spacing(3.5),
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
export const MESSAGE_TYPE = {
|
||||
SUCCESS: 'Success',
|
||||
ERROR: 'Error',
|
||||
INFO: 'Info',
|
||||
CLOSE: 'Close',
|
||||
};
|
||||
|
||||
/* Icon based on MESSAGE_TYPE */
|
||||
function FormIcon({type, close=false, ...props}) {
|
||||
let TheIcon = null;
|
||||
if(close) {
|
||||
TheIcon = CloseIcon;
|
||||
} else if(type === MESSAGE_TYPE.SUCCESS) {
|
||||
TheIcon = CheckIcon;
|
||||
} else if(type === MESSAGE_TYPE.ERROR) {
|
||||
TheIcon = ReportProblemIcon;
|
||||
} else if(type === MESSAGE_TYPE.INFO) {
|
||||
TheIcon = InfoIcon;
|
||||
}
|
||||
|
||||
return <TheIcon fontSize="small" {...props} />;
|
||||
}
|
||||
FormIcon.propTypes = {
|
||||
type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)),
|
||||
close: PropTypes.bool,
|
||||
};
|
||||
|
||||
/* Wrapper on any form component to add label, error indicator and help message */
|
||||
export function FormInput({children, error, className, label, helpMessage, required, testcid}) {
|
||||
const classes = useStyles();
|
||||
const cid = testcid || _.uniqueId('c');
|
||||
const helpid = `h${cid}`;
|
||||
return (
|
||||
<Grid container spacing={0} className={className}>
|
||||
<Grid item lg={3} md={3} sm={3} xs={12}>
|
||||
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error?classes.formLabelError : null)} required={required}>
|
||||
{label}
|
||||
{error && <FormIcon type={MESSAGE_TYPE.ERROR} style={{marginLeft: 'auto'}}/>}
|
||||
</InputLabel>
|
||||
</Grid>
|
||||
<Grid item lg={9} md={9} sm={9} xs={12}>
|
||||
<FormControl error={Boolean(error)} fullWidth>
|
||||
{React.cloneElement(children, {cid, helpid})}
|
||||
</FormControl>
|
||||
<FormHelperText id={helpid} variant="outlined">{helpMessage}</FormHelperText>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
FormInput.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
error: PropTypes.bool,
|
||||
className: CustomPropTypes.className,
|
||||
label: PropTypes.string,
|
||||
helpMessage: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
testcid: PropTypes.any,
|
||||
};
|
||||
|
||||
export function InputSQL({value, options}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<CodeMirror
|
||||
value={value||''}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
mode: 'text/x-pgsql',
|
||||
...options,
|
||||
}}
|
||||
className={classes.sql}
|
||||
/>
|
||||
);
|
||||
}
|
||||
InputSQL.propTypes = {
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.object,
|
||||
};
|
||||
|
||||
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps}) {
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputSQL value={value} options={controlProps}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputSQL.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
controlProps: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
/* Use forwardRef to pass ref prop to OutlinedInput */
|
||||
export const InputText = forwardRef(({
|
||||
cid, helpid, readonly, disabled, maxlength=255, value, onChange, controlProps, ...props}, ref)=>{
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return(
|
||||
<OutlinedInput
|
||||
ref={ref}
|
||||
color="primary"
|
||||
fullWidth
|
||||
className={classes.formInput}
|
||||
inputProps={{
|
||||
id: cid,
|
||||
maxLength: maxlength,
|
||||
'aria-describedby': helpid,
|
||||
}}
|
||||
readOnly={Boolean(readonly)}
|
||||
disabled={Boolean(disabled)}
|
||||
rows={4}
|
||||
notched={false}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
{...controlProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
InputText.displayName = 'InputText';
|
||||
InputText.propTypes = {
|
||||
cid: PropTypes.string,
|
||||
helpid: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
readonly: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
maxlength: PropTypes.number,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
onChange: PropTypes.func,
|
||||
controlProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) {
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputText label={label} {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputText.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
/* Using the existing file dialog functions using showFileDialog */
|
||||
export function InputFileSelect({controlProps, onChange, disabled, readonly, ...props}) {
|
||||
const inpRef = useRef();
|
||||
const onFileSelect = (value)=>{
|
||||
onChange && onChange(decodeURI(value));
|
||||
inpRef.current.focus();
|
||||
};
|
||||
return (
|
||||
<InputText ref={inpRef} disabled={disabled} readonly={readonly} {...props} endAdornment={
|
||||
<IconButton onClick={()=>showFileDialog(controlProps, onFileSelect)}
|
||||
disabled={disabled||readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
|
||||
} />
|
||||
);
|
||||
}
|
||||
InputFileSelect.propTypes = {
|
||||
controlProps: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
readonly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export function FormInputFileSelect({
|
||||
hasError, required, label, className, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputFileSelect required={required} label={label} {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputFileSelect.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
export function InputSwitch({cid, helpid, value, onChange, readonly, controlProps, ...props}) {
|
||||
return (
|
||||
<Switch color="primary"
|
||||
checked={Boolean(value)}
|
||||
onChange={
|
||||
readonly ? ()=>{} : onChange
|
||||
}
|
||||
id={cid}
|
||||
inputProps={{
|
||||
'aria-describedby': helpid,
|
||||
}}
|
||||
{...controlProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
InputSwitch.propTypes = {
|
||||
cid: PropTypes.string,
|
||||
helpid: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func,
|
||||
readonly: PropTypes.bool,
|
||||
controlProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputSwitch {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputSwitch.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
export function InputCheckbox({cid, helpid, value, onChange, controlProps, readonly, ...props}) {
|
||||
controlProps = controlProps || {};
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id={cid}
|
||||
checked={Boolean(value)}
|
||||
onChange={readonly ? ()=>{} : onChange}
|
||||
color="primary"
|
||||
inputProps={{
|
||||
'aria-describedby': helpid,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
label={controlProps.label}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
InputCheckbox.propTypes = {
|
||||
cid: PropTypes.string,
|
||||
helpid: PropTypes.string,
|
||||
value: PropTypes.bool,
|
||||
controlProps: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
readonly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export function FormInputCheckbox({hasError, required, label,
|
||||
className, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputCheckbox {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputCheckbox.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
export function InputToggle({cid, value, onChange, options, disabled, readonly, ...props}) {
|
||||
return (
|
||||
<ToggleButtonGroup
|
||||
id={cid}
|
||||
value={value}
|
||||
exclusive
|
||||
onChange={(e, val)=>{val!==null && onChange(val);}}
|
||||
{...props}
|
||||
>
|
||||
{
|
||||
(options||[]).map((option)=>{
|
||||
const isSelected = option.value === value;
|
||||
const isDisabled = disabled || (readonly && isSelected);
|
||||
return (
|
||||
<ToggleButton key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
|
||||
disabled={isDisabled} aria-label={option.label}>
|
||||
<CheckRoundedIcon style={{visibility: isSelected ? 'visible': 'hidden'}}/> {option.label}
|
||||
</ToggleButton>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ToggleButtonGroup>
|
||||
);
|
||||
}
|
||||
InputToggle.propTypes = {
|
||||
cid: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
||||
options: PropTypes.array,
|
||||
controlProps: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
readonly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export function FormInputToggle({hasError, required, label,
|
||||
className, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputToggle {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputToggle.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
/* react-select package is used for select input
|
||||
* Customizing the select styles to fit existing theme
|
||||
*/
|
||||
const customReactSelectStyles = (theme, readonly)=>({
|
||||
input: (provided) => {
|
||||
return {...provided, padding: 0, margin: 0, color: 'inherit'};
|
||||
},
|
||||
singleValue: (provided) => {
|
||||
return {
|
||||
...provided,
|
||||
color: 'inherit',
|
||||
};
|
||||
},
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
minHeight: '0',
|
||||
backgroundColor: readonly ? theme.otherVars.inputDisabledBg : theme.palette.background.default,
|
||||
borderColor: theme.otherVars.inputBorderColor,
|
||||
...(state.isFocused ? {
|
||||
borderColor: theme.palette.primary.main,
|
||||
boxShadow: 'inset 0 0 0 1px '+theme.palette.primary.main,
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
}
|
||||
} : {}),
|
||||
}),
|
||||
dropdownIndicator: (provided)=>({
|
||||
...provided,
|
||||
padding: '0rem 0.25rem',
|
||||
}),
|
||||
indicatorsContainer: (provided)=>({
|
||||
...provided,
|
||||
...(readonly ? {display: 'none'} : {})
|
||||
}),
|
||||
clearIndicator: (provided)=>({
|
||||
...provided,
|
||||
padding: '0rem 0.25rem',
|
||||
}),
|
||||
valueContainer: (provided)=>({
|
||||
...provided,
|
||||
padding: theme.otherVars.reactSelect.padding,
|
||||
}),
|
||||
menu: (provided)=>({
|
||||
...provided,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
boxShadow: 'none',
|
||||
border: '1px solid ' + theme.otherVars.inputBorderColor,
|
||||
}),
|
||||
menuPortal: (provided)=>({
|
||||
...provided, zIndex: 9999,
|
||||
backgroundColor: 'inherit',
|
||||
color: 'inherit',
|
||||
}),
|
||||
option: (provided, state)=>({
|
||||
...provided,
|
||||
padding: '0.5rem',
|
||||
backgroundColor: state.isFocused ? theme.palette.grey[200] :
|
||||
(state.isSelected ? theme.palette.primary.main : 'inherit'),
|
||||
}),
|
||||
multiValue: (provided)=>({
|
||||
...provided,
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
}),
|
||||
multiValueLabel: (provided)=>({
|
||||
...provided,
|
||||
fontSize: '1em',
|
||||
zIndex: 9999,
|
||||
color: theme.palette.text.primary
|
||||
}),
|
||||
multiValueRemove: (provided)=>({
|
||||
...provided,
|
||||
'&:hover': {
|
||||
backgroundColor: 'unset',
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
...(readonly ? {display: 'none'} : {})
|
||||
}),
|
||||
});
|
||||
|
||||
function OptionView({image, label}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
{image && <span className={clsx(classes.optionIcon, image)}></span>}
|
||||
<span>{label}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
OptionView.propTypes = {
|
||||
image: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
function CustomSelectOption(props) {
|
||||
return (
|
||||
<RSComponents.Option {...props}>
|
||||
<OptionView image={props.data.image} label={props.data.label} />
|
||||
</RSComponents.Option>
|
||||
);
|
||||
}
|
||||
CustomSelectOption.propTypes = {
|
||||
data: PropTypes.object,
|
||||
};
|
||||
|
||||
function CustomSelectSingleValue(props) {
|
||||
return (
|
||||
<RSComponents.SingleValue {...props}>
|
||||
<OptionView image={props.data.image} label={props.data.label} />
|
||||
</RSComponents.SingleValue>
|
||||
);
|
||||
}
|
||||
CustomSelectSingleValue.propTypes = {
|
||||
data: PropTypes.object,
|
||||
};
|
||||
|
||||
function getRealValue(options, value, creatable) {
|
||||
let realValue = null;
|
||||
if(_.isArray(value)) {
|
||||
if(creatable) {
|
||||
realValue = value.map((val)=>({label:val, value: val}));
|
||||
} else {
|
||||
realValue = value.map((val)=>(_.find(options, (option)=>option.value==val)));
|
||||
}
|
||||
} else {
|
||||
realValue = _.find(options, (option)=>option.value==value) || null;
|
||||
}
|
||||
return realValue;
|
||||
}
|
||||
|
||||
export function InputSelect({
|
||||
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, disabled, ...props}) {
|
||||
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(()=>{
|
||||
let optPromise = options, umounted=false;
|
||||
if(typeof options === 'function') {
|
||||
optPromise = options();
|
||||
}
|
||||
Promise.resolve(optPromise)
|
||||
.then((res)=>{
|
||||
/* If component unmounted, dont update state */
|
||||
if(!umounted) {
|
||||
optionsLoaded && optionsLoaded(res, value);
|
||||
setFinalOptions([res || [], false]);
|
||||
}
|
||||
});
|
||||
return ()=>umounted=true;
|
||||
}, []);
|
||||
|
||||
const onChangeOption = useCallback((selectVal, action)=>{
|
||||
if(_.isArray(selectVal)) {
|
||||
onChange && onChange(selectVal.map((option)=>option.value, action.name));
|
||||
} else {
|
||||
onChange && onChange(selectVal ? selectVal.value : null, action.name);
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
const realValue = getRealValue(finalOptions, value, controlProps.creatable);
|
||||
const otherProps = {
|
||||
isSearchable: !readonly,
|
||||
isClearable: !readonly && (!_.isUndefined(controlProps.allowClear) ? controlProps.allowClear : true),
|
||||
isDisabled: Boolean(disabled),
|
||||
};
|
||||
|
||||
const styles = customReactSelectStyles(theme, readonly || disabled);
|
||||
|
||||
const commonProps = {
|
||||
components: {
|
||||
Option: CustomSelectOption,
|
||||
SingleValue: CustomSelectSingleValue,
|
||||
},
|
||||
isMulti: Boolean(controlProps.multiple),
|
||||
openMenuOnClick: !readonly,
|
||||
onChange: onChangeOption,
|
||||
isLoading: isLoading,
|
||||
options: finalOptions,
|
||||
value: realValue,
|
||||
menuPortalTarget: document.body,
|
||||
styles: styles,
|
||||
inputId: cid,
|
||||
...otherProps,
|
||||
...props
|
||||
};
|
||||
if(!controlProps.creatable) {
|
||||
return (
|
||||
<Select {...commonProps}/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CreatableSelect {...commonProps}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
InputSelect.propTypes = {
|
||||
cid: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||
options: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
|
||||
controlProps: PropTypes.object,
|
||||
optionsLoaded: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
readonly: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
export function FormInputSelect({
|
||||
hasError, required, className, label, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputSelect {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputSelect.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
className: CustomPropTypes.className,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
/* React wrapper on color pickr */
|
||||
export function InputColor({value, controlProps, disabled, onChange, currObj}) {
|
||||
const pickrOptions = {
|
||||
showPalette: true,
|
||||
allowEmpty: true,
|
||||
colorFormat: 'HEX',
|
||||
defaultColor: null,
|
||||
position: 'right-middle',
|
||||
clearText: gettext('No color'),
|
||||
...controlProps,
|
||||
disabled: disabled,
|
||||
};
|
||||
const eleRef = useRef();
|
||||
const pickrObj = useRef();
|
||||
const classes = useStyles();
|
||||
|
||||
const setColor = (value)=>{
|
||||
pickrObj.current &&
|
||||
pickrObj.current.setColor((_.isUndefined(value) || value == '') ? pickrOptions.defaultColor : value);
|
||||
};
|
||||
|
||||
const destroyPickr = ()=>{
|
||||
if(pickrObj.current) {
|
||||
pickrObj.current.destroy();
|
||||
pickrObj.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const initPickr = ()=>{
|
||||
/* pickr does not have way to update options, need to
|
||||
destroy and recreate pickr to reflect options */
|
||||
destroyPickr();
|
||||
|
||||
pickrObj.current = new Pickr({
|
||||
el: eleRef.current,
|
||||
useAsButton: true,
|
||||
theme: 'monolith',
|
||||
swatches: [
|
||||
'#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0',
|
||||
'#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999',
|
||||
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666','#93c47d', '#76a5af', '#c27ba0',
|
||||
'#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130',
|
||||
],
|
||||
position: pickrOptions.position,
|
||||
strings: {
|
||||
clear: pickrOptions.clearText,
|
||||
},
|
||||
components: {
|
||||
palette: pickrOptions.showPalette,
|
||||
preview: true,
|
||||
hue: pickrOptions.showPalette,
|
||||
interaction: {
|
||||
clear: pickrOptions.allowEmpty,
|
||||
defaultRepresentation: pickrOptions.colorFormat,
|
||||
disabled: pickrOptions.disabled,
|
||||
},
|
||||
},
|
||||
}).on('init', instance => {
|
||||
setColor(value);
|
||||
disabled && instance.disable();
|
||||
|
||||
const {lastColor} = instance.getRoot().preview;
|
||||
const {clear} = instance.getRoot().interaction;
|
||||
|
||||
/* Cycle the keyboard navigation within the color picker */
|
||||
clear.addEventListener('keydown', (e)=>{
|
||||
if(e.keyCode === 9) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
lastColor.focus();
|
||||
}
|
||||
});
|
||||
|
||||
lastColor.addEventListener('keydown', (e)=>{
|
||||
if(e.keyCode === 9 && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clear.focus();
|
||||
}
|
||||
});
|
||||
}).on('clear', () => {
|
||||
onChange && onChange('');
|
||||
}).on('change', (color) => {
|
||||
onChange && onChange(color.toHEXA().toString());
|
||||
}).on('show', (color, instance) => {
|
||||
const {palette} = instance.getRoot().palette;
|
||||
palette.focus();
|
||||
}).on('hide', (instance) => {
|
||||
const button = instance.getRoot().button;
|
||||
button.focus();
|
||||
});
|
||||
|
||||
if(currObj) {
|
||||
currObj(pickrObj.current);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
initPickr();
|
||||
return ()=>{
|
||||
destroyPickr();
|
||||
};
|
||||
}, [...Object.values(pickrOptions)]);
|
||||
|
||||
useEffect(()=>{
|
||||
if(pickrObj.current) {
|
||||
setColor(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
let btnStyles = {backgroundColor: value};
|
||||
return (
|
||||
// <Button variant="contained" ref={eleRef} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}>
|
||||
// {(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
|
||||
// </Button>
|
||||
<PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}
|
||||
icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
InputColor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
controlProps: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
currObj: PropTypes.func,
|
||||
};
|
||||
|
||||
export function FormInputColor({
|
||||
hasError, required, className, label, helpMessage, testcid, ...props}) {
|
||||
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
||||
<InputColor {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
FormInputColor.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
className: CustomPropTypes.className,
|
||||
label: PropTypes.string,
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
const useStylesFormFooter = makeStyles((theme)=>({
|
||||
root: {
|
||||
padding: theme.spacing(0.5),
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
container: {
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(0.5),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
containerSuccess: {
|
||||
borderColor: theme.palette.success.main,
|
||||
backgroundColor: theme.palette.success.light,
|
||||
},
|
||||
iconSuccess: {
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
containerError: {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.light,
|
||||
},
|
||||
iconError: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
message: {
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
closeButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
}));
|
||||
|
||||
/* The form footer used mostly for showing error */
|
||||
export function FormFooterMessage({type=MESSAGE_TYPE.SUCCESS, message, closable=true, onClose=()=>{}}) {
|
||||
const classes = useStylesFormFooter();
|
||||
|
||||
if(!message) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={clsx(classes.container, classes[`container${type}`])}>
|
||||
<FormIcon type={type} className={classes[`icon${type}`]}/>
|
||||
<Box className={classes.message}>{message}</Box>
|
||||
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
|
||||
<FormIcon close={true}/>
|
||||
</IconButton>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
FormFooterMessage.propTypes = {
|
||||
type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)).isRequired,
|
||||
message: PropTypes.string,
|
||||
closable: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
};
|
57
web/pgadmin/static/js/components/Loader.jsx
Normal file
57
web/pgadmin/static/js/components/Loader.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { CircularProgress, Box, Typography, makeStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: theme.otherVars.loader.backgroundColor,
|
||||
color: theme.otherVars.loader.color,
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
},
|
||||
loaderRoot: {
|
||||
color: theme.otherVars.loader.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
margin: 'auto',
|
||||
'.MuiTypography-root': {
|
||||
marginLeft: theme.spacing(1),
|
||||
}
|
||||
},
|
||||
loader: {
|
||||
color: theme.otherVars.loader.color,
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Loader({message}) {
|
||||
const classes = useStyles();
|
||||
if(!message) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.loaderRoot}>
|
||||
<CircularProgress className={classes.loader} />
|
||||
<Typography>{message}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Loader.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
176
web/pgadmin/static/js/components/Privilege.jsx
Normal file
176
web/pgadmin/static/js/components/Privilege.jsx
Normal file
@ -0,0 +1,176 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { InputCheckbox, InputText } from './FormComponents';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const useStyles = makeStyles(()=>({
|
||||
/* Display the privs table only when focussed */
|
||||
root: {
|
||||
'&:not(:focus-within) .priv-table': {
|
||||
display: 'none',
|
||||
}
|
||||
},
|
||||
table: {
|
||||
borderSpacing: 0,
|
||||
width: '100%',
|
||||
fontSize: '0.8em',
|
||||
},
|
||||
tableCell: {
|
||||
textAlign: 'left',
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Privilege({value, onChange, controlProps}) {
|
||||
// All available privileges in the PostgreSQL database server for
|
||||
// generating the label for the specific Control
|
||||
const LABELS = {
|
||||
'C': 'CREATE',
|
||||
'T': 'TEMPORARY',
|
||||
'c': 'CONNECT',
|
||||
'a': 'INSERT',
|
||||
'r': 'SELECT',
|
||||
'w': 'UPDATE',
|
||||
'd': 'DELETE',
|
||||
'D': 'TRUNCATE',
|
||||
'x': 'REFERENCES',
|
||||
't': 'TRIGGER',
|
||||
'U': 'USAGE',
|
||||
'X': 'EXECUTE',
|
||||
};
|
||||
let all = false;
|
||||
let allWithGrant = false;
|
||||
const classes = useStyles();
|
||||
let textValue = '';
|
||||
for(const v of value||[]) {
|
||||
if(v.privilege) {
|
||||
textValue += v.privilege_type;
|
||||
if(v.with_grant) {
|
||||
textValue += '*';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkboxId = _.uniqueId();
|
||||
/* Calculate the real display value by merging all supported privs with incoming value */
|
||||
const realVal = (controlProps?.supportedPrivs || []).map((priv)=>{
|
||||
let inValue = _.find(value, (v)=>v.privilege_type===priv) || {
|
||||
privilege: false, with_grant: false,
|
||||
};
|
||||
return {
|
||||
privilege_type: priv,
|
||||
privilege: Boolean(inValue.privilege),
|
||||
with_grant: Boolean(inValue.with_grant),
|
||||
};
|
||||
});
|
||||
|
||||
const onCheckAll = (e, forWithGrant)=>{
|
||||
let newValue = [];
|
||||
/* Push all the privs or ignore if unchecked and return empty */
|
||||
realVal.forEach((v)=>{
|
||||
if(forWithGrant) {
|
||||
newValue.push({
|
||||
...v,
|
||||
privilege: true,
|
||||
with_grant: e.target.checked,
|
||||
});
|
||||
} else if(e.target.checked) {
|
||||
newValue.push({
|
||||
...v,
|
||||
privilege: e.target.checked,
|
||||
});
|
||||
}
|
||||
});
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const onCheck = (e, forWithGrant)=>{
|
||||
let exists = false;
|
||||
let newValue = [];
|
||||
|
||||
/* Calculate the newValue by pushing all selected and ignore if unchecked */
|
||||
(value||[]).forEach((v)=>{
|
||||
if(v.privilege_type === e.target.name) {
|
||||
exists = true;
|
||||
if(forWithGrant) {
|
||||
newValue.push({
|
||||
...v,
|
||||
with_grant: e.target.checked,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
newValue.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
if(!exists && e.target.checked) {
|
||||
newValue.push({
|
||||
privilege_type: e.target.name,
|
||||
privilege: e.target.checked,
|
||||
with_grant: false,
|
||||
});
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
/* If all supported privs and incoming value length matches, clearly all are selected */
|
||||
all = (realVal.length === (value || []).length);
|
||||
allWithGrant = (realVal.length === (value || []).length) && (value || []).every((d)=>d.with_grant);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<InputText value={textValue} readOnly/>
|
||||
<table className={clsx(classes.table, 'priv-table')}>
|
||||
{(realVal.length > 1) && <thead>
|
||||
<tr>
|
||||
<td className={classes.tableCell}>
|
||||
<InputCheckbox name="all" controlProps={{label: 'ALL'}} id={checkboxId} size="small"
|
||||
onChange={(e)=>onCheckAll(e, false)} value={all}/>
|
||||
</td>
|
||||
<td className={classes.tableCell}>
|
||||
<InputCheckbox name="all" controlProps={{label: 'WITH GRANT OPTION'}} id={checkboxId} size="small"
|
||||
disabled={!all} onChange={(e)=>onCheckAll(e, true)} value={allWithGrant}/>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>}
|
||||
<tbody>
|
||||
{
|
||||
realVal.map((d)=>{
|
||||
return (
|
||||
<tr key={d.privilege_type}>
|
||||
<td className={classes.tableCell}>
|
||||
<InputCheckbox name={d.privilege_type} controlProps={{label: LABELS[d.privilege_type]}}
|
||||
id={checkboxId} value={Boolean(d.privilege)} size="small"
|
||||
onChange={(e)=>onCheck(e, false)}/>
|
||||
</td>
|
||||
<td className={classes.tableCell}>
|
||||
<InputCheckbox name={d.privilege_type} controlProps={{label: 'WITH GRANT OPTION'}}
|
||||
id={checkboxId} value={Boolean(d.with_grant)} size="small" disabled={!d.privilege}
|
||||
onChange={(e)=>onCheck(e, true)}/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Privilege.propTypes = {
|
||||
value: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
controlProps: PropTypes.object,
|
||||
};
|
42
web/pgadmin/static/js/components/TabPanel.jsx
Normal file
42
web/pgadmin/static/js/components/TabPanel.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import {Box, makeStyles} from '@material-ui/core';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
height: '100%',
|
||||
padding: theme.spacing(1),
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
}
|
||||
}));
|
||||
|
||||
/* Material UI does not have any tabpanel component, we create one for us */
|
||||
export default function TabPanel({children, classNameRoot, className, value, index}) {
|
||||
const classes = useStyles();
|
||||
const active = value === index;
|
||||
return (
|
||||
<Box className={clsx(classes.root, classNameRoot)} component="div" hidden={!active}>
|
||||
<Box style={{height: '100%'}} className={className}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
classNameRoot: CustomPropTypes.className,
|
||||
className: CustomPropTypes.className,
|
||||
value: PropTypes.any.isRequired,
|
||||
index: PropTypes.any.isRequired,
|
||||
};
|
@ -1,10 +1,32 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import PropTypes from 'prop-types';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
|
||||
/* Common Prop types */
|
||||
const CustomPropTypes = {
|
||||
ref: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
|
||||
schemaUI: PropTypes.instanceOf(BaseUISchema),
|
||||
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]),
|
||||
|
||||
className: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
])
|
||||
};
|
||||
|
||||
export default CustomPropTypes;
|
||||
|
66
web/pgadmin/static/js/helpers/legacyConnector.js
Normal file
66
web/pgadmin/static/js/helpers/legacyConnector.js
Normal file
@ -0,0 +1,66 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* This file will have wrappers and connectors used by React components to
|
||||
* re-use any existing non-react components.
|
||||
* These functions may not be needed once all are migrated
|
||||
*/
|
||||
|
||||
import Alertify from 'pgadmin.alertifyjs';
|
||||
import gettext from 'sources/gettext';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
export function confirmDeleteRow(onOK, onCancel, title, message) {
|
||||
Alertify.confirm(
|
||||
title || gettext('Delete Row'),
|
||||
message || gettext('Are you sure you wish to delete this row?'),
|
||||
function() {
|
||||
onOK();
|
||||
return true;
|
||||
},
|
||||
function() {
|
||||
onCancel();
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* Don't import alertfiy directly in react files. Not good for testability */
|
||||
export function pgAlertify() {
|
||||
return Alertify;
|
||||
}
|
||||
|
||||
/* Used by file select component to re-use existing logic */
|
||||
export function showFileDialog(dialogParams, onFileSelect) {
|
||||
let params = {
|
||||
supported_types: dialogParams.supportedTypes || [],
|
||||
dialog_type: dialogParams.dialogType || 'select_file',
|
||||
dialog_title: dialogParams.dialogTitle || '',
|
||||
btn_primary: dialogParams.btnPrimary || '',
|
||||
};
|
||||
pgAdmin.FileManager.init();
|
||||
pgAdmin.FileManager.show_dialog(params);
|
||||
|
||||
const onDialogClose = ()=>removeListeners();
|
||||
pgAdmin.Browser.Events.on('pgadmin-storage:finish_btn:' + params.dialog_type, onFileSelect);
|
||||
pgAdmin.Browser.Events.on('pgadmin-storage:cancel_btn:' + params.dialog_type, onDialogClose);
|
||||
|
||||
const removeListeners = ()=>{
|
||||
pgAdmin.Browser.Events.off('pgadmin-storage:finish_btn:' + params.dialog_type, onFileSelect);
|
||||
pgAdmin.Browser.Events.off('pgadmin-storage:cancel_btn:' + params.dialog_type, onDialogClose);
|
||||
};
|
||||
}
|
||||
|
||||
export function onPgadminEvent(eventName, handler) {
|
||||
pgAdmin.Browser.Events.on(eventName, handler);
|
||||
}
|
||||
|
||||
export function offPgadminEvent(eventName, handler) {
|
||||
pgAdmin.Browser.Events.off(eventName, handler);
|
||||
}
|
@ -436,4 +436,10 @@ export function registerDetachEvent(panel){
|
||||
$((this.$container)[0].ownerDocument).find('.wcIFrameFloating').find('.wcIFrameFloating').css('height', height);
|
||||
});
|
||||
|
||||
/* If a function, then evaluate */
|
||||
export function evalFunc(func, param) {
|
||||
if(_.isFunction(func)) {
|
||||
return func.apply(null, [param]);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
70
web/pgadmin/static/js/validators.js
Normal file
70
web/pgadmin/static/js/validators.js
Normal file
@ -0,0 +1,70 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import {sprintf} from 'sources/utils';
|
||||
import _ from 'lodash';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
/* Validate value for min max range */
|
||||
export function minMaxValidator(label, value, minValue, maxValue) {
|
||||
if((_.isUndefined(value) || _.isNull(value) || String(value) === ''))
|
||||
return null;
|
||||
if (!_.isUndefined(minValue) && value < minValue) {
|
||||
return sprintf(pgAdmin.Browser.messages.MUST_GR_EQ, label, minValue);
|
||||
} else if (!_.isUndefined(maxValue) && value > maxValue) {
|
||||
return sprintf(pgAdmin.Browser.messages.MUST_LESS_EQ, label, maxValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Validate value to check if it is a number */
|
||||
export function numberValidator(label, value) {
|
||||
if((_.isUndefined(value) || _.isNull(value) || String(value) === ''))
|
||||
return null;
|
||||
var pattern = new RegExp('^-?[0-9]+(\.?[0-9]*)?$');
|
||||
if (!pattern.test(value)) {
|
||||
return sprintf(pgAdmin.Browser.messages.MUST_BE_NUM, label);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Validate value to check if it is an integer */
|
||||
export function integerValidator(label, value) {
|
||||
if((_.isUndefined(value) || _.isNull(value) || String(value) === ''))
|
||||
return null;
|
||||
var pattern = new RegExp('^-?[0-9]*$');
|
||||
if (!pattern.test(value)) {
|
||||
return sprintf(pgAdmin.Browser.messages.MUST_BE_INT, label);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Validate value to check if it is empty */
|
||||
export function emptyValidator(label, value) {
|
||||
if(isEmptyString(value) || String(value).replace(/^\s+|\s+$/g, '') == '') {
|
||||
return sprintf(pgAdmin.Browser.messages.CANNOT_BE_EMPTY, label);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isEmptyString(string) {
|
||||
return _.isUndefined(string) || _.isNull(string) || String(string).trim() === '';
|
||||
}
|
||||
|
||||
/* Validate rows to check for any duplicate rows based on uniqueCols-columns array */
|
||||
export function checkUniqueCol(rows, uniqueCols) {
|
||||
if(uniqueCols) {
|
||||
for(const [i, row] of rows.entries()) {
|
||||
for(const checkRow of rows.slice(0, i)) {
|
||||
if(_.isEqual(_.pick(checkRow, uniqueCols), _.pick(row, uniqueCols))) return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
@ -107,7 +107,7 @@ legend {
|
||||
color: $btn-secondary-hover-fg !important;
|
||||
} &.disabled, &:disabled {
|
||||
color: $btn-secondary-disabled-fg !important;
|
||||
border-color: $btn-secondary-disabled-bg !important;
|
||||
border-color: $btn-secondary-disabled-border-color !important;
|
||||
}
|
||||
color: $btn-secondary-fg !important;
|
||||
|
||||
|
@ -264,6 +264,7 @@
|
||||
|
||||
.obj_properties {
|
||||
padding: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.obj_properties .pgadmin-control .uneditable-input {
|
||||
|
@ -189,7 +189,7 @@
|
||||
.wcFrameEdge {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
z-index: 5;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.wcFrameEdgeN, .wcFrameEdgeS {
|
||||
|
@ -286,7 +286,7 @@ $btn-secondary-fg: $color-fg !default;
|
||||
$btn-secondary-hover-fg: $color-fg !default;
|
||||
$btn-secondary-border: $color-gray !default;
|
||||
$btn-secondary-hover-bg: $color-gray-light !default;
|
||||
$btn-secondary-disabled-bg: $color-gray !default;
|
||||
$btn-secondary-disabled-border-color: $color-gray !default;
|
||||
$btn-secondary-disabled-fg: $color-fg !default;
|
||||
|
||||
$btn-frame-close-bg:$btn-secondary-hover-bg !default;
|
||||
|
@ -151,7 +151,7 @@ $btn-secondary-bg: transparent;
|
||||
$btn-secondary-fg: $color-primary;
|
||||
$btn-secondary-hover-fg: $color-fg;
|
||||
$btn-secondary-border: $color-primary;
|
||||
$btn-secondary-disabled-bg: $color-gray-lighter;
|
||||
$btn-secondary-disabled-border-color: $color-gray-lighter;
|
||||
$btn-secondary-disabled-fg: $color-gray-lighter;
|
||||
|
||||
$btn-frame-close-bg: $color-gray-light;
|
||||
|
@ -21,7 +21,8 @@
|
||||
<!-- Base template stylesheets -->
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/generated/style.css')}}"/>
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='js/generated/pgadmin.style.css')}}"/>
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename=('js/generated/'+get_theme_css())) }}"/>
|
||||
{% set theme = get_theme_css() %}
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename=('js/generated/'+theme[0])) }}" data-theme="{{theme[1]}}"/>
|
||||
|
||||
<!--View specified stylesheets-->
|
||||
{% block css_link %}{% endblock %}
|
||||
|
501
web/regression/javascript/SchemaView/SchemaView.spec.js
Normal file
501
web/regression/javascript/SchemaView/SchemaView.spec.js
Normal file
@ -0,0 +1,501 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import {TestSchema, TestSchemaAllTypes} from './TestSchema.ui';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import * as legacyConnector from 'sources/helpers/legacyConnector';
|
||||
|
||||
const initData = {
|
||||
id: 1,
|
||||
field1: 'field1val',
|
||||
field2: 1,
|
||||
fieldcoll: [
|
||||
{field3: 1, field4: 'field4val1', field5: 'field5val1'},
|
||||
{field3: 2, field4: 'field4val2', field5: 'field5val2'},
|
||||
],
|
||||
field3: 3,
|
||||
field4: 'field4val',
|
||||
};
|
||||
|
||||
function getInitData() {
|
||||
return Promise.resolve(initData);
|
||||
}
|
||||
|
||||
function getSchema() {
|
||||
return new TestSchema();
|
||||
}
|
||||
|
||||
function getSchemaAllTypes() {
|
||||
return new TestSchemaAllTypes();
|
||||
}
|
||||
|
||||
describe('SchemaView', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
});
|
||||
|
||||
describe('SchemaDialogView', ()=>{
|
||||
let ctrl,
|
||||
onSave=jasmine.createSpy('onSave').and.returnValue(Promise.resolve()),
|
||||
onClose=jasmine.createSpy('onClose'),
|
||||
onHelp=jasmine.createSpy('onHelp'),
|
||||
onEdit=jasmine.createSpy('onEdit'),
|
||||
onDataChange=jasmine.createSpy('onDataChange'),
|
||||
getSQLValue=jasmine.createSpy('onEdit').and.returnValue(Promise.resolve('select 1;')),
|
||||
ctrlMount = (props)=>{
|
||||
ctrl?.unmount();
|
||||
ctrl = mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={getSchema()}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
onHelp={onHelp}
|
||||
onEdit={onEdit}
|
||||
onDataChange={onDataChange}
|
||||
confirmOnCloseReset={true}
|
||||
hasSQL={true}
|
||||
getSQLValue={getSQLValue}
|
||||
disableSqlHelp={false}
|
||||
{...props}
|
||||
/>);
|
||||
},
|
||||
simulateValidData = ()=>{
|
||||
ctrl.find('MappedFormControl[id="field1"]').find('input').simulate('change', {target: {value: 'val1'}});
|
||||
ctrl.find('MappedFormControl[id="field2"]').find('input').simulate('change', {target: {value: '2'}});
|
||||
ctrl.find('MappedFormControl[id="field5"]').find('textarea').simulate('change', {target: {value: 'val5'}});
|
||||
/* Add a row */
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
ctrl.find('MappedCellControl[id="field5"]').at(0).find('input').simulate('change', {target: {value: 'rval51'}});
|
||||
ctrl.find('MappedCellControl[id="field5"]').at(1).find('input').simulate('change', {target: {value: 'rval52'}});
|
||||
};
|
||||
beforeEach(()=>{
|
||||
ctrlMount();
|
||||
});
|
||||
|
||||
it('init', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('MappedFormControl').length).toBe(6);
|
||||
expect(ctrl.find('FormView').length).toBe(2);
|
||||
expect(ctrl.find('DataGridView').length).toBe(1);
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('change text', (done)=>{
|
||||
ctrl.find('MappedFormControl[id="field2"]').find('input').simulate('change', {target: {value: '2'}});
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
/* Error should come for field1 as it is empty and noEmpty true */
|
||||
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('\'Field1\' cannot be empty.');
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('close error on click', (done)=>{
|
||||
ctrl.find('FormFooterMessage').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('valid form data', (done)=>{
|
||||
simulateValidData();
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('FormFooterMessage').prop('message')).toBeFalsy();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
describe('DataGridView', ()=>{
|
||||
it('add row', (done)=>{
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DataGridView').find('DataTableRow').length).toBe(1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('remove row', (done)=>{
|
||||
simulateValidData();
|
||||
|
||||
/* Press OK */
|
||||
let confirmSpy = spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{
|
||||
yesFn();
|
||||
});
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
|
||||
expect(confirmSpy.calls.argsFor(0)[2]).toBe('Custom delete title');
|
||||
expect(confirmSpy.calls.argsFor(0)[3]).toBe('Custom delete message');
|
||||
|
||||
/* Press Cancel */
|
||||
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn, cancelFn)=>{
|
||||
cancelFn();
|
||||
});
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DataGridView').find('DataTableRow').length).toBe(1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('expand row', (done)=>{
|
||||
simulateValidData();
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="expand-row"]').at(0).find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DataGridView').find('FormView').length).toBe(1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('unique col test', (done)=>{
|
||||
simulateValidData();
|
||||
ctrl.find('MappedCellControl[id="field5"]').at(1).find('input').simulate('change', {target: {value: 'rval51'}});
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('Field5 in FieldColl must be unique.');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SQL tab', ()=>{
|
||||
it('no changes', (done)=>{
|
||||
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
/* Dont show error message */
|
||||
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('');
|
||||
expect(ctrl.find('CodeMirror').prop('value')).toBe('-- No updates.');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('data invalid', (done)=>{
|
||||
ctrl.find('MappedFormControl[id="field2"]').find('input').simulate('change', {target: {value: '2'}});
|
||||
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('CodeMirror').prop('value')).toBe('-- Definition incomplete.');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('valid data', (done)=>{
|
||||
simulateValidData();
|
||||
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('CodeMirror').prop('value')).toBe('select 1;');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('onSave click', (done)=>{
|
||||
simulateValidData();
|
||||
let alertSpy = spyOn(legacyConnector.pgAlertify(), 'alert');
|
||||
onSave.calls.reset();
|
||||
ctrl.find('PrimaryButton[data-test="Save"]').simulate('click');
|
||||
setTimeout(()=>{
|
||||
expect(onSave.calls.argsFor(0)[0]).toBe(true);
|
||||
expect(onSave.calls.argsFor(0)[1]).toEqual({
|
||||
id: undefined,
|
||||
field1: 'val1',
|
||||
field2: 2,
|
||||
field5: 'val5',
|
||||
fieldcoll: [
|
||||
{field3: null, field4: null, field5: 'rval51'},
|
||||
{field3: null, field4: null, field5: 'rval52'},
|
||||
]
|
||||
});
|
||||
expect(alertSpy).toHaveBeenCalledWith('Warning', 'some inform text');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
describe('onReset', ()=>{
|
||||
it('with confirm check and yes click', (done)=>{
|
||||
simulateValidData();
|
||||
onDataChange.calls.reset();
|
||||
let confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough();
|
||||
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
|
||||
/* Press OK */
|
||||
confirmSpy.calls.argsFor(0)[2]();
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
expect(onDataChange).toHaveBeenCalledWith(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('with confirm check and cancel click', (done)=>{
|
||||
simulateValidData();
|
||||
let confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough();
|
||||
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
|
||||
/* Press cancel */
|
||||
confirmSpy.calls.argsFor(0)[3]();
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeFalse();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeFalse();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
|
||||
it('no confirm check', (done)=>{
|
||||
ctrlMount({
|
||||
confirmOnCloseReset: false,
|
||||
});
|
||||
ctrl.update();
|
||||
simulateValidData();
|
||||
onDataChange.calls.reset();
|
||||
let confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough();
|
||||
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(confirmSpy).not.toHaveBeenCalled();
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
expect(onDataChange).toHaveBeenCalledWith(false);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('onHelp SQL', (done)=>{
|
||||
ctrl.find('PgIconButton[data-test="sql-help"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(onHelp).toHaveBeenCalledWith(true, true);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('onHelp Dialog', (done)=>{
|
||||
ctrl.find('PgIconButton[data-test="dialog-help"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(onHelp).toHaveBeenCalledWith(false, true);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
describe('edit mode', ()=>{
|
||||
let simulateChanges = ()=>{
|
||||
ctrl.find('MappedFormControl[id="field1"]').find('input').simulate('change', {target: {value: 'val1'}});
|
||||
ctrl.find('MappedFormControl[id="field5"]').find('textarea').simulate('change', {target: {value: 'val5'}});
|
||||
|
||||
/* Add a row */
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
ctrl.find('MappedCellControl[id="field5"]').at(2).find('input').simulate('change', {target: {value: 'rval53'}});
|
||||
|
||||
/* Remove the 1st row */
|
||||
ctrl.find('DataTableRow').find('PgIconButton[data-test="delete-row"]').at(0).find('button').simulate('click');
|
||||
|
||||
/* Edit the 2nd row which is first now*/
|
||||
ctrl.find('MappedCellControl[id="field5"]').at(0).find('input').simulate('change', {target: {value: 'rvalnew'}});
|
||||
|
||||
};
|
||||
beforeEach(()=>{
|
||||
ctrlMount({
|
||||
getInitData: getInitData,
|
||||
viewHelperProps: {
|
||||
mode: 'edit',
|
||||
}
|
||||
});
|
||||
|
||||
/* Press OK */
|
||||
spyOn(legacyConnector, 'confirmDeleteRow').and.callFake((yesFn)=>{
|
||||
yesFn();
|
||||
});
|
||||
});
|
||||
it('init', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('MappedFormControl').length).toBe(4);
|
||||
expect(ctrl.find('FormView').length).toBe(2);
|
||||
expect(ctrl.find('DataGridView').length).toBe(1);
|
||||
expect(ctrl.find('DataTableRow').length).toBe(2);
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('onSave after change', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
simulateChanges();
|
||||
|
||||
let alertSpy = spyOn(legacyConnector.pgAlertify(), 'alert');
|
||||
onSave.calls.reset();
|
||||
ctrl.find('PrimaryButton[data-test="Save"]').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(onSave.calls.argsFor(0)[0]).toBe(false);
|
||||
expect(onSave.calls.argsFor(0)[1]).toEqual({
|
||||
id: 1,
|
||||
field1: 'val1',
|
||||
field5: 'val5',
|
||||
fieldcoll: {
|
||||
added: [
|
||||
{ field3: null, field4: null, field5: 'rval53'}
|
||||
],
|
||||
changed: [
|
||||
{ field3: 2, field4: 'field4val2', field5: 'rvalnew'}
|
||||
],
|
||||
deleted: [
|
||||
{ field3: 1, field4: 'field4val1', field5: 'field5val1'}
|
||||
]
|
||||
}
|
||||
});
|
||||
expect(alertSpy).toHaveBeenCalledWith('Warning', 'some inform text');
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('onReset after change', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
simulateChanges();
|
||||
onDataChange.calls.reset();
|
||||
let confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough();
|
||||
ctrl.find('DefaultButton[data-test="Reset"]').simulate('click');
|
||||
/* Press OK */
|
||||
confirmSpy.calls.argsFor(0)[2]();
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('DefaultButton[data-test="Reset"]').prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('PrimaryButton[data-test="Save"]').prop('disabled')).toBeTrue();
|
||||
expect(onDataChange).toHaveBeenCalledWith(false);
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('all types', ()=>{
|
||||
let ctrl;
|
||||
beforeEach(()=>{
|
||||
ctrl?.unmount();
|
||||
ctrl = mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={getSchemaAllTypes()}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={true}
|
||||
getSQLValue={()=>'select 1;'}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
/* Add a row */
|
||||
ctrl.find('DataGridView').find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SchemaPropertiesView', ()=>{
|
||||
let onEdit = jasmine.createSpy('onEdit'),
|
||||
onHelp = jasmine.createSpy('onHelp'),
|
||||
ctrl = null;
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={getSchema()}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={onHelp}
|
||||
disableSqlHelp={false}
|
||||
onEdit={onEdit}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find('MappedFormControl').length).toBe(6);
|
||||
expect(ctrl.find('ForwardRef(Accordion)').length).toBe(2);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('onHelp', (done)=>{
|
||||
ctrl.find('PgIconButton[data-test="help"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(onHelp).toHaveBeenCalled();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('onEdit', (done)=>{
|
||||
ctrl.find('PgIconButton[data-test="edit"]').find('button').simulate('click');
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(onEdit).toHaveBeenCalled();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
190
web/regression/javascript/SchemaView/TestSchema.ui.js
Normal file
190
web/regression/javascript/SchemaView/TestSchema.ui.js
Normal file
@ -0,0 +1,190 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
|
||||
class TestSubSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
field3: null,
|
||||
field4: null,
|
||||
});
|
||||
|
||||
this.keys = ['field3', 'field4', 'field5'];
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'field3', label: 'Field3', type: 'int', group: null,
|
||||
mode: ['properties'], visible: true, cell: 'int'
|
||||
},
|
||||
{
|
||||
id: 'field4', label: 'Field4', type: 'select', group: null,
|
||||
cell: ()=>({cell: 'select', options: []}),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
disabled: (state)=>this.isNew(state), deps: ['field5'],
|
||||
depChange: ()=>{}, optionsLoaded: ()=>{},
|
||||
},
|
||||
{
|
||||
id: 'field5', label: 'Field5', type: 'multiline', group: null,
|
||||
cell: 'text', mode: ['properties', 'edit', 'create'], disabled: false,
|
||||
noEmpty: true, minWidth: '50%',
|
||||
},
|
||||
{
|
||||
id: 'fieldskip', label: 'FieldSkip', type: 'text', group: null,
|
||||
cell: 'text', mode: ['properties', 'edit', 'create'], disabled: false,
|
||||
noEmpty: true,
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export class TestSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
id: undefined,
|
||||
field1: null,
|
||||
field2: null,
|
||||
fieldcoll: null,
|
||||
});
|
||||
|
||||
this.informText = 'some inform text';
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'id', label: 'ID', type: 'int', group: null,
|
||||
mode: ['properties'],
|
||||
},{
|
||||
id: 'field1', label: 'Field1', type: 'text', group: null,
|
||||
mode: ['properties', 'edit', 'create'], disabled: false,
|
||||
noEmpty: true, visible: true,
|
||||
},{
|
||||
id: 'field2', label: 'Field2', type: ()=>({
|
||||
type: 'int', min: 0, max: 255,
|
||||
}), group: null, mode: ['properties', 'create'],
|
||||
disabled: ()=>false, visible: ()=>true, deps: ['field1'],
|
||||
depChange: ()=>({})
|
||||
},{
|
||||
id: 'fieldcoll', label: 'FieldColl', type: 'collection', group: null,
|
||||
mode: ['edit', 'create'], schema: new TestSubSchema(),
|
||||
canAdd: true, canEdit: true, canDelete: true, uniqueCol: ['field5'],
|
||||
customDeleteTitle: 'Custom delete title',
|
||||
customDeleteMsg: 'Custom delete message',
|
||||
},{
|
||||
type: 'nested-tab', group: null,
|
||||
mode: ['edit', 'create'], schema: new TestSubSchema(),
|
||||
},{
|
||||
id: 'field6', label: 'Field6', type: 'numeric', group: null,
|
||||
mode: ['properties', 'create'],
|
||||
disabled: false,
|
||||
},{
|
||||
id: 'field7', label: 'Field7', type: 'text', group: 'Advanced',
|
||||
mode: ['properties'],
|
||||
},{
|
||||
id: 'fieldinvis', label: 'fieldinvis', type: 'text', group: 'Advanced',
|
||||
mode: ['properties', 'edit', 'create'], visible: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
validate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestSubSchemaAllTypes extends BaseUISchema {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'int', label: 'int', type: 'int', group: null,
|
||||
mode: ['create'], cell: 'int'
|
||||
},{
|
||||
id: 'text', label: 'text', type: 'text', group: null,
|
||||
mode: ['create'], cell: 'text'
|
||||
},{
|
||||
id: 'password', label: 'password', type: 'password', group: null,
|
||||
mode: ['create'], cell: 'password',
|
||||
},{
|
||||
id: 'select', label: 'select', type: 'select', group: null,
|
||||
mode: ['create'], options: [], cell: 'select'
|
||||
},{
|
||||
id: 'switch', label: 'switch', type: 'switch', group: null,
|
||||
mode: ['create'], options: [], cell: 'switch'
|
||||
},{
|
||||
id: 'privilege', label: 'privilege', type: 'privilege', group: null,
|
||||
mode: ['create'], options: [], cell: 'privilege',
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class TestSchemaAllTypes extends BaseUISchema {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [
|
||||
{
|
||||
id: 'int', label: 'int', type: 'int', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'text', label: 'text', type: 'text', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'multiline', label: 'multiline', type: 'multiline', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'password', label: 'password', type: 'password', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'select', label: 'select', type: 'select', group: null,
|
||||
mode: ['create'], options: [],
|
||||
},{
|
||||
id: 'switch', label: 'switch', type: 'switch', group: null,
|
||||
mode: ['create'], options: [],
|
||||
},{
|
||||
id: 'checkbox', label: 'checkbox', type: 'checkbox', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'toggle', label: 'toggle', type: 'toggle', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'color', label: 'color', type: 'color', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'file', label: 'file', type: 'file', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'sql', label: 'sql', type: 'sql', group: null,
|
||||
mode: ['create'],
|
||||
},{
|
||||
id: 'function', label: 'function', type: ()=>'text',
|
||||
group: null,mode: ['create'],
|
||||
},{
|
||||
id: 'collection', label: 'collection', type: 'collection', group: null,
|
||||
mode: ['edit', 'create'], schema: new TestSubSchemaAllTypes(),
|
||||
canAdd: true, canEdit: true, canDelete: true,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
validate() {
|
||||
return false;
|
||||
}
|
||||
}
|
56
web/regression/javascript/components/Buttons.spec.js
Normal file
56
web/regression/javascript/components/Buttons.spec.js
Normal file
@ -0,0 +1,56 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { withTheme } from '../fake_theme';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import InfoIcon from '@material-ui/icons/InfoRounded';
|
||||
|
||||
import {PrimaryButton, DefaultButton, PgIconButton} from 'sources/components/Buttons';
|
||||
|
||||
/* MUI Components need to be wrapped in Theme for theme vars */
|
||||
describe('components Buttons', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
it('PrimaryButton', ()=>{
|
||||
let ThemedBtn = withTheme(PrimaryButton);
|
||||
let btn = mount(<ThemedBtn>Test</ThemedBtn>);
|
||||
expect(btn.getDOMNode().classList.contains('MuiButton-containedPrimary')).toBe(true);
|
||||
});
|
||||
|
||||
it('DefaultButton', ()=>{
|
||||
let ThemedBtn = withTheme(DefaultButton);
|
||||
let btn = mount(<ThemedBtn className="testClass">Test</ThemedBtn>);
|
||||
expect(btn.getDOMNode().classList.contains('MuiButton-outlined')).toBe(true);
|
||||
expect(btn.getDOMNode().classList.contains('testClass')).toBe(true);
|
||||
});
|
||||
|
||||
it('PgIconButton', ()=>{
|
||||
let Icon = <InfoIcon />;
|
||||
let ThemedBtn = withTheme(PgIconButton);
|
||||
let btn = mount(<ThemedBtn title="The icon button" icon={Icon} className="testClass"></ThemedBtn>);
|
||||
expect(btn.find(InfoIcon)).not.toBe(null);
|
||||
});
|
||||
});
|
46
web/regression/javascript/components/CodeMirror.spec.js
Normal file
46
web/regression/javascript/components/CodeMirror.spec.js
Normal file
@ -0,0 +1,46 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
||||
|
||||
import CodeMirror from 'sources/components/CodeMirror';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
describe('CodeMirror', ()=>{
|
||||
let cmInstance, options={
|
||||
lineNumbers: true,
|
||||
mode: 'text/x-pgsql',
|
||||
}, cmObj = jasmine.createSpyObj('cmObj', {'setValue': ()=>{}, 'refresh': ()=>{}});
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
spyOn(OrigCodeMirror, 'fromTextArea').and.returnValue(cmObj);
|
||||
cmInstance = mount(
|
||||
<CodeMirror
|
||||
value={'Init text'}
|
||||
options={options}
|
||||
className="testClass"
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
/* textarea ref passed to fromTextArea */
|
||||
expect(OrigCodeMirror.fromTextArea).toHaveBeenCalledWith(cmInstance.getDOMNode(), options);
|
||||
expect(cmObj.setValue).toHaveBeenCalledWith('Init text');
|
||||
expect(cmObj.refresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('change value', ()=>{
|
||||
cmInstance.setProps({value: 'the new text'});
|
||||
expect(cmObj.setValue).toHaveBeenCalledWith('the new text');
|
||||
expect(cmObj.refresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
606
web/regression/javascript/components/FormComponents.spec.js
Normal file
606
web/regression/javascript/components/FormComponents.spec.js
Normal file
@ -0,0 +1,606 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { withTheme } from '../fake_theme';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import { OutlinedInput, FormHelperText, IconButton, FormControlLabel,
|
||||
Switch, Checkbox, Button, InputLabel } from '@material-ui/core';
|
||||
import Select from 'react-select';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
||||
import ReportProblemIcon from '@material-ui/icons/ReportProblemRounded';
|
||||
import InfoIcon from '@material-ui/icons/InfoRounded';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import CheckIcon from '@material-ui/icons/CheckCircleOutlineRounded';
|
||||
|
||||
|
||||
import {FormInputText, FormInputFileSelect, FormInputSQL,
|
||||
FormInputSwitch, FormInputCheckbox, FormInputToggle, FormInputSelect,
|
||||
FormInputColor,
|
||||
FormFooterMessage,
|
||||
MESSAGE_TYPE} from '../../../pgadmin/static/js/components/FormComponents';
|
||||
import * as legacyConnector from '../../../pgadmin/static/js/helpers/legacyConnector';
|
||||
import CodeMirror from '../../../pgadmin/static/js/components/CodeMirror';
|
||||
import { ToggleButton } from '@material-ui/lab';
|
||||
import { DefaultButton, PrimaryButton } from '../../../pgadmin/static/js/components/Buttons';
|
||||
|
||||
/* MUI Components need to be wrapped in Theme for theme vars */
|
||||
describe('FormComponents', ()=>{
|
||||
let mount;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
});
|
||||
|
||||
describe('FormInputText', ()=>{
|
||||
let ThemedFormInputText = withTheme(FormInputText), ctrl;
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputText
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputText */
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
maxlength={50}
|
||||
value={'thevalue'}
|
||||
controlProps={{
|
||||
extraprop: 'test'
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(OutlinedInput).prop('extraprop')).toEqual('test');
|
||||
expect( ctrl.find(OutlinedInput).prop('inputProps')).toEqual(jasmine.objectContaining({
|
||||
maxLength: 50,
|
||||
}));
|
||||
expect(ctrl.find(OutlinedInput).prop('readOnly')).toBe(false);
|
||||
expect(ctrl.find(OutlinedInput).prop('disabled')).toBe(false);
|
||||
expect(ctrl.find(OutlinedInput).prop('value')).toBe('thevalue');
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
|
||||
it('props change', ()=>{
|
||||
let onChange = ()=>{};
|
||||
ctrl.setProps({
|
||||
readonly: true,
|
||||
disabled: true,
|
||||
value: 'new value',
|
||||
onChange: onChange,
|
||||
});
|
||||
|
||||
expect(ctrl.find(OutlinedInput).prop('readOnly')).toBe(true);
|
||||
expect(ctrl.find(OutlinedInput).prop('disabled')).toBe(true);
|
||||
expect(ctrl.find(OutlinedInput).prop('value')).toBe('new value');
|
||||
expect(ctrl.find(OutlinedInput).prop('onChange')).toBe(onChange);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
let inputProps = ctrl.find(OutlinedInput).prop('inputProps');
|
||||
expect(inputProps).toEqual(jasmine.objectContaining({
|
||||
id: 'inpCid',
|
||||
'aria-describedby': 'hinpCid',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputFileSelect', ()=>{
|
||||
let ThemedFormInputFileSelect = withTheme(FormInputFileSelect), ctrl;
|
||||
|
||||
beforeEach(()=>{
|
||||
spyOn(legacyConnector, 'showFileDialog').and.callFake((controlProps, onFileSelect)=>{
|
||||
onFileSelect('selected/file');
|
||||
});
|
||||
ctrl = mount(
|
||||
<ThemedFormInputFileSelect
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputText */
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
value={'thevalue'}
|
||||
controlProps={{
|
||||
dialogType: 'select_file', supportedTypes: ['*'],
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(OutlinedInput).prop('readOnly')).toBe(false);
|
||||
expect(ctrl.find(OutlinedInput).prop('disabled')).toBe(false);
|
||||
expect(ctrl.find(OutlinedInput).prop('value')).toBe('thevalue');
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
|
||||
it('props change', ()=>{
|
||||
ctrl.setProps({
|
||||
readonly: true,
|
||||
disabled: true,
|
||||
value: 'new value',
|
||||
});
|
||||
|
||||
expect(ctrl.find(OutlinedInput).prop('readOnly')).toBe(true);
|
||||
expect(ctrl.find(OutlinedInput).prop('disabled')).toBe(true);
|
||||
expect(ctrl.find(OutlinedInput).prop('value')).toBe('new value');
|
||||
expect(ctrl.find(IconButton).prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('file select', (done)=>{
|
||||
let onChange = jasmine.createSpy();
|
||||
ctrl.setProps({
|
||||
onChange: onChange,
|
||||
});
|
||||
ctrl.find(IconButton).simulate('click');
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith('selected/file');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
let inputProps = ctrl.find(OutlinedInput).prop('inputProps');
|
||||
expect(inputProps).toEqual(jasmine.objectContaining({
|
||||
id: 'inpCid',
|
||||
'aria-describedby': 'hinpCid',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputSQL', ()=>{
|
||||
let ThemedFormInputSQL = withTheme(FormInputSQL), ctrl;
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputSQL
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputSQL */
|
||||
value={'thevalue'}
|
||||
controlProps={{
|
||||
op1: 'test'
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(CodeMirror).prop('value')).toEqual('thevalue');
|
||||
expect(ctrl.find(CodeMirror).prop('options')).toEqual(jasmine.objectContaining({
|
||||
op1: 'test'
|
||||
}));
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputSwitch', ()=>{
|
||||
let ThemedFormInputSwitch = withTheme(FormInputSwitch), ctrl, onChange=()=>{return 1;};
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputSwitch
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputSwitch */
|
||||
readonly={false}
|
||||
value={false}
|
||||
onChange={onChange}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(Switch).prop('checked')).toBe(false);
|
||||
expect(ctrl.find(Switch).prop('onChange')).toBe(onChange);
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
|
||||
it('props change', ()=>{
|
||||
ctrl.setProps({
|
||||
readonly: true,
|
||||
value: true,
|
||||
});
|
||||
|
||||
expect(ctrl.find(Switch).prop('checked')).toBe(true);
|
||||
expect(ctrl.find(Switch).prop('onChange')).not.toBe(onChange);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
expect(ctrl.find(Switch).prop('id')).toBe('inpCid');
|
||||
let inputProps = ctrl.find(Switch).prop('inputProps');
|
||||
expect(inputProps).toEqual(jasmine.objectContaining({
|
||||
'aria-describedby': 'hinpCid',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputCheckbox', ()=>{
|
||||
let ThemedFormInputCheckbox = withTheme(FormInputCheckbox), ctrl, onChange=()=>{return 1;};
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputCheckbox
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputCheckbox */
|
||||
disabled={false}
|
||||
value={false}
|
||||
onChange={onChange}
|
||||
controlProps={{
|
||||
label: 'Second'
|
||||
}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(FormControlLabel).prop('label')).toBe('Second');
|
||||
expect(ctrl.find(Checkbox).prop('checked')).toBe(false);
|
||||
expect(ctrl.find(Checkbox).prop('onChange')).toBe(onChange);
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
|
||||
it('props change', ()=>{
|
||||
ctrl.setProps({
|
||||
readonly: true,
|
||||
value: true,
|
||||
});
|
||||
|
||||
expect(ctrl.find(Checkbox).prop('checked')).toBe(true);
|
||||
expect(ctrl.find(Checkbox).prop('onChange')).not.toBe(onChange);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
expect(ctrl.find(Checkbox).prop('id')).toBe('inpCid');
|
||||
let inputProps = ctrl.find(Checkbox).prop('inputProps');
|
||||
expect(inputProps).toEqual(jasmine.objectContaining({
|
||||
'aria-describedby': 'hinpCid',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputToggle', ()=>{
|
||||
let ThemedFormInputToggle = withTheme(FormInputToggle), ctrl, onChange=()=>{return 1;};
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputToggle
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputToggle */
|
||||
disabled={false}
|
||||
options={[
|
||||
{label: 'Op1', value: 1},
|
||||
{label: 'Op2', value: 2},
|
||||
{label: 'Op3', value: 3},
|
||||
]}
|
||||
value={2}
|
||||
onChange={onChange}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(InputLabel).text()).toBe('First');
|
||||
expect(ctrl.find(ToggleButton).length).toBe(3);
|
||||
expect(ctrl.find(PrimaryButton).length).toBe(1);
|
||||
expect(ctrl.find(DefaultButton).length).toBe(2);
|
||||
expect(ctrl.find(ToggleButton).at(1).prop('component')).toBe(PrimaryButton);
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
});
|
||||
|
||||
it('props change', ()=>{
|
||||
ctrl.setProps({
|
||||
value: 1,
|
||||
});
|
||||
expect(ctrl.find(ToggleButton).at(0).prop('component')).toBe(PrimaryButton);
|
||||
expect(ctrl.find(ToggleButton).at(0)
|
||||
.find(CheckRoundedIcon)
|
||||
.prop('style')).toEqual(jasmine.objectContaining({
|
||||
visibility: 'visible'
|
||||
}));
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputSelect', ()=>{
|
||||
let ThemedFormInputSelect = withTheme(FormInputSelect), ctrl, onChange=jasmine.createSpy('onChange'),
|
||||
ctrlMount = (props)=>{
|
||||
ctrl?.unmount();
|
||||
ctrl = mount(
|
||||
<ThemedFormInputSelect
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputSelect */
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
options={[
|
||||
{label: 'Op1', value: 1},
|
||||
{label: 'Op2', value: 2},
|
||||
{label: 'Op3', value: 3},
|
||||
]}
|
||||
value={1}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>);
|
||||
};
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrlMount();
|
||||
});
|
||||
|
||||
it('init', (done)=>{
|
||||
expect(ctrl.find(Select).exists()).toBe(true);
|
||||
expect(ctrl.find(CreatableSelect).exists()).toBe(false);
|
||||
expect(ctrl.find(FormHelperText).text()).toBe('some help message');
|
||||
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Select).props()).toEqual(jasmine.objectContaining({
|
||||
isMulti: false,
|
||||
value: {label: 'Op1', value: 1},
|
||||
inputId: 'inpCid',
|
||||
isSearchable: true,
|
||||
isClearable: true,
|
||||
isDisabled: false,
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('readonly disabled', (done)=>{
|
||||
ctrl.setProps({
|
||||
readonly: true,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Select).props()).toEqual(jasmine.objectContaining({
|
||||
isSearchable: false,
|
||||
isClearable: false,
|
||||
isDisabled: true,
|
||||
openMenuOnClick: false,
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('no-clear with multi', (done)=>{
|
||||
ctrl.setProps({
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
multiple: true,
|
||||
},
|
||||
value: [2, 3],
|
||||
});
|
||||
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Select).props()).toEqual(jasmine.objectContaining({
|
||||
isMulti: true,
|
||||
isClearable: false,
|
||||
value: [{label: 'Op2', value: 2}, {label: 'Op3', value: 3}]
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('creatable with multi', (done)=>{
|
||||
ctrl.setProps({
|
||||
controlProps: {
|
||||
creatable: true,
|
||||
multiple: true,
|
||||
},
|
||||
value: ['val1', 'val2'],
|
||||
});
|
||||
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Select).exists()).toBe(false);
|
||||
expect(ctrl.find(CreatableSelect).exists()).toBe(true);
|
||||
|
||||
expect(ctrl.find(CreatableSelect).props()).toEqual(jasmine.objectContaining({
|
||||
isMulti: true,
|
||||
value: [{label: 'val1', value: 'val1'}, {label: 'val2', value: 'val2'}]
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('promise options', (done)=>{
|
||||
let optionsLoaded = jasmine.createSpy();
|
||||
let res = [
|
||||
{label: 'PrOp1', value: 1},
|
||||
{label: 'PrOp2', value: 2},
|
||||
{label: 'PrOp3', value: 3},
|
||||
];
|
||||
/* For options change, remount needed */
|
||||
ctrlMount({
|
||||
options: ()=>Promise.resolve(res),
|
||||
value: 3,
|
||||
optionsLoaded: optionsLoaded,
|
||||
});
|
||||
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(optionsLoaded).toHaveBeenCalledWith(res, 3);
|
||||
expect(ctrl.find(Select).props()).toEqual(jasmine.objectContaining({
|
||||
value: {label: 'PrOp3', value: 3},
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormInputColor', ()=>{
|
||||
let pickrObj = React.createRef();
|
||||
let ThemedFormInputColor = withTheme(FormInputColor), ctrl, onChange=jasmine.createSpy('onChange');
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormInputColor
|
||||
label="First"
|
||||
className="someClass"
|
||||
testcid="inpCid"
|
||||
helpMessage="some help message"
|
||||
/* InputColor */
|
||||
disabled={false}
|
||||
value="#f0f"
|
||||
onChange={onChange}
|
||||
currObj={(obj)=>pickrObj.current=obj}
|
||||
/>);
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
ctrl.unmount();
|
||||
});
|
||||
|
||||
it('init', (done)=>{
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Button).prop('style')).toEqual(jasmine.objectContaining({
|
||||
backgroundColor: '#f0f',
|
||||
}));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('no color', (done)=>{
|
||||
ctrl.setProps({
|
||||
value: null,
|
||||
});
|
||||
setTimeout(()=>{
|
||||
ctrl.update();
|
||||
expect(ctrl.find(Button).prop('style')).toEqual(jasmine.objectContaining({
|
||||
backgroundColor: null,
|
||||
}));
|
||||
expect(ctrl.find(Button).find(CloseIcon).exists()).toBe(true);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('other events', (done)=>{
|
||||
pickrObj.current.applyColor(false);
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('accessibility', ()=>{
|
||||
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
|
||||
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FormFooterMessage', ()=>{
|
||||
let ThemedFormFooterMessage = withTheme(FormFooterMessage), ctrl, onClose=jasmine.createSpy('onClose');
|
||||
|
||||
beforeEach(()=>{
|
||||
ctrl = mount(
|
||||
<ThemedFormFooterMessage
|
||||
type={MESSAGE_TYPE.SUCCESS}
|
||||
message="Some message"
|
||||
closable={false}
|
||||
onClose={onClose}
|
||||
/>);
|
||||
});
|
||||
|
||||
// if(close) {
|
||||
// TheIcon = CloseIcon;
|
||||
// } else if(type === MESSAGE_TYPE.SUCCESS) {
|
||||
// TheIcon = CheckIcon;
|
||||
// } else if(type === MESSAGE_TYPE.ERROR) {
|
||||
// TheIcon = ReportProblemIcon;
|
||||
// } else if(type === MESSAGE_TYPE.INFO) {
|
||||
// TheIcon = InfoIcon;
|
||||
// }
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find(CheckIcon).exists()).toBeTrue();
|
||||
expect(ctrl.text()).toBe('Some message');
|
||||
});
|
||||
|
||||
it('change types', ()=>{
|
||||
ctrl.setProps({
|
||||
type: MESSAGE_TYPE.ERROR,
|
||||
});
|
||||
expect(ctrl.find(ReportProblemIcon).exists()).toBeTrue();
|
||||
|
||||
ctrl.setProps({
|
||||
type: MESSAGE_TYPE.INFO,
|
||||
});
|
||||
expect(ctrl.find(InfoIcon).exists()).toBeTrue();
|
||||
});
|
||||
|
||||
it('closable', ()=>{
|
||||
ctrl.setProps({
|
||||
closable: true,
|
||||
});
|
||||
expect(ctrl.find(CloseIcon).exists()).toBeTrue();
|
||||
ctrl.find(IconButton).simulate('click');
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('no message', ()=>{
|
||||
ctrl.setProps({
|
||||
message: '',
|
||||
});
|
||||
expect(ctrl.isEmptyRender()).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
52
web/regression/javascript/components/Loader.spec.js
Normal file
52
web/regression/javascript/components/Loader.spec.js
Normal file
@ -0,0 +1,52 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import { withTheme } from '../fake_theme';
|
||||
import Loader from 'sources/components/Loader';
|
||||
|
||||
|
||||
/* MUI Components need to be wrapped in Theme for theme vars */
|
||||
describe('Loader', ()=>{
|
||||
let mount, loaderInst, ThemedLoader;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
/* Loader need Mui Theme context as well */
|
||||
ThemedLoader = withTheme(Loader);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
loaderInst = mount(<ThemedLoader message={'loading'} />);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(loaderInst.find('.MuiTypography-root').text()).toBe('loading');
|
||||
});
|
||||
|
||||
it('no message', ()=>{
|
||||
loaderInst.setProps({message: ''});
|
||||
expect(loaderInst.isEmptyRender()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('change message', ()=>{
|
||||
loaderInst.setProps({message: 'test message'});
|
||||
expect(loaderInst.find('.MuiTypography-root').text()).toBe('test message');
|
||||
});
|
||||
});
|
204
web/regression/javascript/components/Privilege.spec.js
Normal file
204
web/regression/javascript/components/Privilege.spec.js
Normal file
@ -0,0 +1,204 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import Privilege from 'sources/components/Privilege';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
describe('Privilege', ()=>{
|
||||
let ctrl, onChange = jasmine.createSpy('onChange');
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
ctrl = mount(
|
||||
<Privilege
|
||||
value={[{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
}]}
|
||||
controlProps={{
|
||||
supportedPrivs: ['C', 'a', 'r']
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(ctrl.find('InputText').prop('value')).toBe('Ca*');
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(0).prop('value')).toBeTrue();
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(1).prop('value')).toBeFalse();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(0).prop('value')).toBeTrue();
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(1).prop('value')).toBeTrue();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(0).prop('value')).toBeFalse();
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(1).prop('value')).toBeFalse();
|
||||
});
|
||||
|
||||
it('change prop value', ()=>{
|
||||
ctrl.setProps({value: [{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'r',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
}]});
|
||||
|
||||
expect(ctrl.find('InputText').prop('value')).toBe('C*r');
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(0).prop('value')).toBeTrue();
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(1).prop('value')).toBeTrue();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(0).prop('value')).toBeFalse();
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(1).prop('value')).toBeFalse();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(0).prop('value')).toBeTrue();
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(1).prop('value')).toBeFalse();
|
||||
});
|
||||
|
||||
it('no prop value', ()=>{
|
||||
ctrl.setProps({value: null});
|
||||
|
||||
expect(ctrl.find('InputText').prop('value')).toBe('');
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(0).prop('value')).toBeFalse();
|
||||
expect(ctrl.find('InputCheckbox[name="C"]').at(1).prop('value')).toBeFalse();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(0).prop('value')).toBeFalse();
|
||||
expect(ctrl.find('InputCheckbox[name="a"]').at(1).prop('value')).toBeFalse();
|
||||
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(0).prop('value')).toBeFalse();
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(1).prop('value')).toBeFalse();
|
||||
});
|
||||
|
||||
it('with grant disabled', ()=>{
|
||||
expect(ctrl.find('InputCheckbox[name="all"]').at(1).prop('disabled')).toBeTrue();
|
||||
expect(ctrl.find('InputCheckbox[name="r"]').at(1).prop('disabled')).toBeTrue();
|
||||
});
|
||||
|
||||
it('on check click', (done)=>{
|
||||
onChange.calls.reset();
|
||||
ctrl.find('InputCheckbox[name="C"]').at(0).find('input').
|
||||
simulate('change', {target: {checked: false, name: 'C'}});
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith([{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
}]);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('on new check click', (done)=>{
|
||||
onChange.calls.reset();
|
||||
ctrl.find('InputCheckbox[name="r"]').at(0).find('input').
|
||||
simulate('change', {target: {checked: true, name: 'r'}});
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith([{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'r',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
}]);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('on check grant click', (done)=>{
|
||||
onChange.calls.reset();
|
||||
ctrl.find('InputCheckbox[name="C"]').at(1).find('input').
|
||||
simulate('change', {target: {checked: true, name: 'C'}});
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith([{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
}]);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('on all click', (done)=>{
|
||||
ctrl.find('InputCheckbox[name="all"]').at(0).find('input').simulate('change', {target: {checked: true}});
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith([{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'r',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
}]);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('on all with grant click', (done)=>{
|
||||
ctrl.setProps({
|
||||
value: [{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'r',
|
||||
privilege: true,
|
||||
with_grant: false,
|
||||
}]
|
||||
});
|
||||
ctrl.find('InputCheckbox[name="all"]').at(1).find('input').simulate('change', {target: {checked: true}});
|
||||
|
||||
setTimeout(()=>{
|
||||
expect(onChange).toHaveBeenCalledWith([{
|
||||
privilege_type: 'C',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'a',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
},{
|
||||
privilege_type: 'r',
|
||||
privilege: true,
|
||||
with_grant: true,
|
||||
}]);
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
});
|
48
web/regression/javascript/components/TabPanel.spec.js
Normal file
48
web/regression/javascript/components/TabPanel.spec.js
Normal file
@ -0,0 +1,48 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import { withTheme } from '../fake_theme';
|
||||
import TabPanel from 'sources/components/TabPanel';
|
||||
|
||||
|
||||
/* MUI Components need to be wrapped in Theme for theme vars */
|
||||
describe('TabPanel', ()=>{
|
||||
let mount, panelInst, ThemedPanel;
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
/* Need Mui Theme context as well */
|
||||
ThemedPanel = withTheme(TabPanel);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
panelInst = mount(<ThemedPanel value={1} index={0}><h1>test</h1></ThemedPanel>);
|
||||
});
|
||||
|
||||
it('init', ()=>{
|
||||
expect(panelInst.getDOMNode().hidden).toBeTrue();
|
||||
expect(panelInst.find('h1')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('tab select', ()=>{
|
||||
panelInst.setProps({value: 0});
|
||||
expect(panelInst.getDOMNode().hidden).toBeFalse();
|
||||
});
|
||||
});
|
@ -9,6 +9,7 @@
|
||||
|
||||
define(function () {
|
||||
return {
|
||||
'id': 'pgadmin4@pgadmin.org',
|
||||
'current_auth_source': 'internal'
|
||||
};
|
||||
});
|
||||
|
20
web/regression/javascript/fake_messages.js
Normal file
20
web/regression/javascript/fake_messages.js
Normal file
@ -0,0 +1,20 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
define('pgadmin.browser.messages',['sources/pgadmin'], function(pgAdmin) {
|
||||
var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
|
||||
if (pgBrowser.messages)
|
||||
return pgBrowser.messages;
|
||||
|
||||
pgBrowser.messages = {
|
||||
'CANNOT_BE_EMPTY': '\'%s\' cannot be empty.',
|
||||
};
|
||||
return pgBrowser;
|
||||
});
|
17
web/regression/javascript/fake_supported_servers.js
Normal file
17
web/regression/javascript/fake_supported_servers.js
Normal file
@ -0,0 +1,17 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
define(function () {
|
||||
return [
|
||||
{label: 'Greenplum Database', value: 'gpdb'},
|
||||
{label: 'EDB Advanced Server', value: 'ppas'},
|
||||
{label: 'PostgreSQL', value: 'pg'},
|
||||
{label: 'Unknown', value: ''},
|
||||
];
|
||||
});
|
9
web/regression/javascript/fake_theme.js
Normal file
9
web/regression/javascript/fake_theme.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import Theme from 'sources/Theme';
|
||||
|
||||
export function withTheme(WrappedComp) {
|
||||
let NewComp = (props)=>{
|
||||
return <Theme><WrappedComp {...props}/></Theme>;
|
||||
};
|
||||
return NewComp;
|
||||
}
|
120
web/regression/javascript/schema_ui_files/database.ui.spec.js
Normal file
120
web/regression/javascript/schema_ui_files/database.ui.spec.js
Normal file
@ -0,0 +1,120 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import DatabaseSchema from '../../../pgadmin/browser/server_groups/servers/databases/static/js/database.ui';
|
||||
|
||||
class MockSchema extends BaseUISchema {
|
||||
get baseFields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
describe('DatabaseSchema', ()=>{
|
||||
let mount;
|
||||
let schemaObj = new DatabaseSchema(
|
||||
()=>new MockSchema(),
|
||||
()=>new MockSchema(),
|
||||
{
|
||||
role: ()=>[],
|
||||
encoding: ()=>[],
|
||||
template: ()=>[],
|
||||
spcname: ()=>[],
|
||||
datcollate: ()=>[],
|
||||
datctype: ()=>[],
|
||||
},
|
||||
{
|
||||
datowner: 'postgres',
|
||||
}
|
||||
);
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
|
||||
pgAdmin.Browser.utils.support_ssh_tunnel = true;
|
||||
});
|
||||
|
||||
it('create', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('schema_res depChange', ()=>{
|
||||
let depChange = _.find(schemaObj.fields, (f)=>f.id=='schema_res').depChange;
|
||||
depChange({schema_res: 'abc'});
|
||||
expect(schemaObj.informText).toBe('Please refresh the Schemas node to make changes to the schema restriction take effect.');
|
||||
});
|
||||
});
|
130
web/regression/javascript/schema_ui_files/privilege.ui.spec.js
Normal file
130
web/regression/javascript/schema_ui_files/privilege.ui.spec.js
Normal file
@ -0,0 +1,130 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import PrivilegeRoleSchema, {getNodePrivilegeRoleSchema} from '../../../pgadmin/browser/server_groups/servers/static/js/privilege.ui';
|
||||
import {DefaultPrivSchema} from '../../../pgadmin/browser/server_groups/servers/databases/static/js/database.ui';
|
||||
import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax';
|
||||
|
||||
describe('PrivilegeSchema', ()=>{
|
||||
let mount;
|
||||
let schemaObj = new PrivilegeRoleSchema(
|
||||
()=>[],
|
||||
()=>[],
|
||||
null,
|
||||
{server: {user: {name: 'postgres'}}},
|
||||
['X']
|
||||
);
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
|
||||
pgAdmin.Browser.utils.support_ssh_tunnel = true;
|
||||
});
|
||||
|
||||
it('create', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('validate', ()=>{
|
||||
let state = {};
|
||||
let setError = jasmine.createSpy('setError');
|
||||
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('privileges', 'At least one privilege should be selected.');
|
||||
});
|
||||
|
||||
it('DefaultPrivSchema', ()=>{
|
||||
spyOn(nodeAjax, 'getNodeListByName').and.returnValue([]);
|
||||
let defPrivObj = new DefaultPrivSchema((privileges)=>getNodePrivilegeRoleSchema({}, {server: {user: {name: 'postgres'}}}, {}, privileges));
|
||||
let ctrl = mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={defPrivObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
/* Make sure you hit every corner */
|
||||
ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
});
|
||||
});
|
139
web/regression/javascript/schema_ui_files/server.ui.spec.js
Normal file
139
web/regression/javascript/schema_ui_files/server.ui.spec.js
Normal file
@ -0,0 +1,139 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import ServerSchema from '../../../pgadmin/browser/server_groups/servers/static/js/server.ui';
|
||||
|
||||
describe('ServerSchema', ()=>{
|
||||
let mount;
|
||||
let schemaObj = new ServerSchema([{
|
||||
label: 'Servers', value: 1,
|
||||
}], {
|
||||
user_id: 'jasmine',
|
||||
});
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
|
||||
pgAdmin.Browser.utils.support_ssh_tunnel = true;
|
||||
});
|
||||
|
||||
it('create', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('validate', ()=>{
|
||||
let state = {};
|
||||
let setError = jasmine.createSpy('setError');
|
||||
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('host', 'Either Host name, Address or Service must be specified.');
|
||||
|
||||
state.hostaddr = 'incorrectip';
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('hostaddr', 'Host address must be valid IPv4 or IPv6 address.');
|
||||
|
||||
state.host = '127.0.0.1';
|
||||
state.hostaddr = null;
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('username', 'Username must be specified.');
|
||||
|
||||
state.username = 'postgres';
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('port', 'Port must be specified.');
|
||||
|
||||
state.port = 5432;
|
||||
state.use_ssh_tunnel = true;
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('tunnel_host', 'SSH Tunnel host must be specified.');
|
||||
|
||||
state.service = 'pgservice';
|
||||
state.tunnel_host = 'localhost';
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('tunnel_port', 'SSH Tunnel port must be specified.');
|
||||
|
||||
state.tunnel_port = 8080;
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('tunnel_username', 'SSH Tunnel username must be specified.');
|
||||
|
||||
state.tunnel_username = 'jasmine';
|
||||
state.tunnel_authentication = true;
|
||||
schemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('tunnel_identity_file', 'SSH Tunnel identity file must be specified.');
|
||||
|
||||
state.tunnel_identity_file = '/file/path/xyz.pem';
|
||||
expect(schemaObj.validate(state, setError)).toBeFalse();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import ServerGroupSchema from '../../../pgadmin/browser/server_groups/static/js/server_group.ui';
|
||||
|
||||
describe('ServerGroupSchema', ()=>{
|
||||
let mount;
|
||||
let schemaObj = new ServerGroupSchema();
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
});
|
||||
|
||||
it('create', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
});
|
155
web/regression/javascript/schema_ui_files/variable.ui.spec.js
Normal file
155
web/regression/javascript/schema_ui_files/variable.ui.spec.js
Normal file
@ -0,0 +1,155 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import jasmineEnzyme from 'jasmine-enzyme';
|
||||
import React from 'react';
|
||||
import '../helper/enzyme.helper';
|
||||
import { createMount } from '@material-ui/core/test-utils';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import {messages} from '../fake_messages';
|
||||
import SchemaView from '../../../pgadmin/static/js/SchemaView';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import VariableSchema, {getNodeVariableSchema} from '../../../pgadmin/browser/server_groups/servers/static/js/variable.ui';
|
||||
import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax';
|
||||
|
||||
|
||||
/* Used to check collection mode */
|
||||
class MockSchema extends BaseUISchema {
|
||||
constructor(getVariableSchema) {
|
||||
super();
|
||||
this.getVariableSchema = getVariableSchema;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [{
|
||||
id: 'variables', label: '', type: 'collection',
|
||||
schema: this.getVariableSchema(),
|
||||
editable: false,
|
||||
group: 'Parameters', mode: ['edit', 'create'],
|
||||
canAdd: true, canEdit: false, canDelete: true, hasRole: true,
|
||||
node: 'role',
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
describe('PrivilegeSchema', ()=>{
|
||||
let mount;
|
||||
let schemaObj = new VariableSchema(
|
||||
()=>[],
|
||||
()=>[],
|
||||
()=>[],
|
||||
null
|
||||
);
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
/* Use createMount so that material ui components gets the required context */
|
||||
/* https://material-ui.com/guides/testing/#api */
|
||||
beforeAll(()=>{
|
||||
mount = createMount();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mount.cleanUp();
|
||||
});
|
||||
|
||||
beforeEach(()=>{
|
||||
jasmineEnzyme();
|
||||
/* messages used by validators */
|
||||
pgAdmin.Browser = pgAdmin.Browser || {};
|
||||
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
|
||||
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
|
||||
pgAdmin.Browser.utils.support_ssh_tunnel = true;
|
||||
});
|
||||
|
||||
it('create', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={schemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('getValueFieldProps', ()=>{
|
||||
expect(schemaObj.getValueFieldProps({vartype: 'bool'})).toBe('switch');
|
||||
expect(schemaObj.getValueFieldProps({vartype: 'enum', enumvals: []})).toEqual(jasmine.objectContaining({
|
||||
cell: 'select',
|
||||
}));
|
||||
expect(schemaObj.getValueFieldProps({vartype: 'integer'})).toBe('int');
|
||||
expect(schemaObj.getValueFieldProps({vartype: 'real'})).toBe('number');
|
||||
expect(schemaObj.getValueFieldProps({vartype: 'string'})).toBe('text');
|
||||
expect(schemaObj.getValueFieldProps({})).toBe('');
|
||||
});
|
||||
|
||||
it('variable collection', ()=>{
|
||||
spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue([]);
|
||||
spyOn(nodeAjax, 'getNodeListByName').and.returnValue([]);
|
||||
let varCollObj = new MockSchema(()=>getNodeVariableSchema({}, {server: {user: {name: 'postgres'}}}, {}, true, true));
|
||||
let ctrl = mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={varCollObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
/>);
|
||||
/* Make sure you hit every corner */
|
||||
ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click');
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -152,7 +152,9 @@ var webpackShimConfig = {
|
||||
'dagre': path.join(__dirname, 'node_modules/dagre'),
|
||||
'graphlib': path.join(__dirname, 'node_modules/graphlib'),
|
||||
'react': path.join(__dirname, 'node_modules/react'),
|
||||
'react-dom': path.join(__dirname, 'node_modules/react-dom'),
|
||||
'stylis': path.join(__dirname, 'node_modules/stylis'),
|
||||
'popper.js': path.join(__dirname, 'node_modules/popper.js'),
|
||||
|
||||
// AciTree
|
||||
'jquery.acitree': path.join(__dirname, './node_modules/acitree/js/jquery.aciTree.min'),
|
||||
|
@ -160,6 +160,8 @@ module.exports = {
|
||||
'backgrid.filter': path.join(__dirname, './node_modules/backgrid-filter/backgrid-filter'),
|
||||
'sources': sourcesDir + '/js',
|
||||
'translations': regressionDir + '/javascript/fake_translations',
|
||||
'pgadmin.browser.messages': regressionDir + '/javascript/fake_messages',
|
||||
'pgadmin.server.supported_servers': regressionDir + '/javascript/fake_supported_servers',
|
||||
'pgadmin.browser.endpoints': regressionDir + '/javascript/fake_endpoints',
|
||||
'slickgrid': nodeModulesDir + '/slickgrid/',
|
||||
'slickgrid.plugins': nodeModulesDir + '/slickgrid/plugins/',
|
||||
|
1313
web/yarn.lock
1313
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user