diff --git a/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.js b/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.js index c4919b924..2dfe7c2c8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.js +++ b/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.js @@ -10,12 +10,13 @@ import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../static/js/node_ajax'; import LanguageSchema from './language.ui'; import { getNodePrivilegeRoleSchema } from '../../../../static/js/privilege.ui'; +import _ from 'lodash'; define('pgadmin.node.language', [ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', + 'sources/gettext', 'sources/url_for', 'jquery', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.collection', 'pgadmin.browser.server.privilege', -], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform) { +], function(gettext, url_for, $, pgAdmin, pgBrowser, Backform) { // Extend the browser's collection class for languages collection if (!pgBrowser.Nodes['coll-language']) { diff --git a/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.ui.js b/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.ui.js index 21bdd607b..da137561e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/languages/static/js/language.ui.js @@ -10,6 +10,7 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import { isEmptyString } from 'sources/validators'; import SecLabelSchema from '../../../../static/js/sec_label.ui'; +import _ from 'lodash'; export default class LanguageSchema extends BaseUISchema { constructor(getPrivilegeRoleSchema, fieldOptions={}, node_info, initValues) { @@ -46,7 +47,7 @@ export default class LanguageSchema extends BaseUISchema { } isDisabled(state){ - if (this.templateList.some(code => code.tmplname === state.name)){ + if (this.templateList.some(template => template.tmplname === state.name)){ this.isTemplate = false; return true; }else{ diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py index a69c10665..6e7e9ea9e 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/__init__.py @@ -356,7 +356,7 @@ class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): def _fetch_properties(self, did, subid): """ - This function fetch the properties of the extension. + This function fetch the properties of the subscription. :param did: :param subid: :return: @@ -372,11 +372,6 @@ class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): if len(res['rows']) == 0: return False, gone(self._NOT_FOUND_PUB_INFORMATION) - if 'cur_pub' in res['rows'][0]: - res['rows'][0]['cur_pub'] = ", ".join(str(elem) for elem in - res['rows'][0]['cur_pub']) - res['rows'][0]['pub'] = ", ".join(str(elem) for elem in - res['rows'][0]['pub']) return True, res['rows'][0] @@ -469,9 +464,6 @@ class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): ) try: - data['pub'] = json.loads( - data['pub'], encoding='utf-8' - ) sql = render_template("/".join([self.template_path, self._CREATE_SQL]), @@ -676,36 +668,38 @@ class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): passfile = connection_details['passfile'] if \ 'passfile' in connection_details and \ connection_details['passfile'] != '' else None + try: + conn = psycopg2.connect( + host=connection_details['host'], + database=connection_details['db'], + user=connection_details['username'], + password=connection_details[ + 'password'] if 'password' in connection_details else None, + port=connection_details['port'] if + connection_details['port'] else None, + passfile=get_complete_file_path(passfile), + connect_timeout=connection_details['connect_timeout'] if + 'connect_timeout' in connection_details and + connection_details['connect_timeout'] else 0, + sslmode=connection_details['sslmode'], + sslcert=get_complete_file_path(connection_details['sslcert']), + sslkey=get_complete_file_path(connection_details['sslkey']), + sslrootcert=get_complete_file_path( + connection_details['sslrootcert']), + sslcompression=True if connection_details[ + 'sslcompression'] else False, + ) + # create a cursor + cur = conn.cursor() + cur.execute('SELECT pubname from pg_catalog.pg_publication') - conn = psycopg2.connect( - host=connection_details['host'], - database=connection_details['db'], - user=connection_details['username'], - password=connection_details[ - 'password'] if 'password' in connection_details else None, - port=connection_details['port'] if - connection_details['port'] else None, - passfile=get_complete_file_path(passfile), - connect_timeout=connection_details['connect_timeout'] if - 'connect_timeout' in connection_details and - connection_details['connect_timeout'] else 0, - sslmode=connection_details['sslmode'], - sslcert=get_complete_file_path(connection_details['sslcert']), - sslkey=get_complete_file_path(connection_details['sslkey']), - sslrootcert=get_complete_file_path( - connection_details['sslrootcert']), - sslcompression=True if connection_details[ - 'sslcompression'] else False, - ) - # create a cursor - cur = conn.cursor() - cur.execute('SELECT pubname from pg_catalog.pg_publication') + publications = cur.fetchall() + # Close the connection + conn.close() - publications = cur.fetchall() - # Close the connection - conn.close() - - return publications + return publications, True + except Exception as error: + return error, False @check_precondition def get_publications(self, gid, sid, did, *args, **kwargs): @@ -733,19 +727,27 @@ class SubscriptionView(PGChildNodeView, SchemaDiffObjectCompare): if arg not in url_params and arg in params: url_params[arg] = params[arg] - res = self.get_connection(url_params) + res, status = self.get_connection(url_params) + + if status: + result = [] + for pub in res: + result.append({ + "value": pub[0], + "label": pub[0] + }) + return make_json_response( + data=result, + status=200 + ) + else: + result = res.args[0] + return make_json_response( + errormsg=result, + status=200 + ) - result = [] - for pub in res: - result.append({ - "value": pub[0], - "label": pub[0] - }) - return make_json_response( - data=result, - status=200 - ) @check_precondition def sql(self, gid, sid, did, subid, json_resp=True): diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js index d1986c1fe..0b684ee42 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.js @@ -6,13 +6,16 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// +import { getNodeListByName } from '../../../../../../static/js/node_ajax'; +import SubscriptionSchema from './subscription.ui'; +import getApiInstance from '../../../../../../../static/js/api_instance'; +import { pgAlertify } from '../../../../../../../../pgadmin/static/js/helpers/legacyConnector'; +import _ from 'lodash'; define('pgadmin.node.subscription', [ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', - 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', - 'sources/browser/server_groups/servers/model_validation', 'pgadmin.alertifyjs', 'pgadmin.browser.collection', -], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, modelValidation, Alertify) { - var SSL_MODES = ['prefer', 'require', 'verify-ca', 'verify-full']; + 'sources/gettext', 'sources/url_for', 'jquery', + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.browser.collection', +], function(gettext, url_for, $, pgAdmin, pgBrowser, Backform) { // Extend the browser's collection class for subscriptions collection if (!pgBrowser.Nodes['coll-subscription']) { @@ -137,406 +140,23 @@ define('pgadmin.node.subscription', [ return false; }, }, - { - id: 'host', label: gettext('Host name/address'), 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; - } - }, - }), - },{ - id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], min: 1, max: 65535, - 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; - } - }, - }), - },{ - 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; - } - }, - }), - },{ - id: 'password', label: gettext('Password'), type: 'password', maxlength: null, - group: gettext('Connection'), control: 'input', mode: ['create', 'edit'], deps: ['connect_now'], - },{ - id: 'db', label: gettext('Database'), type: 'text', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], - }, - { - id: 'connect_timeout', label: gettext('Connection timeout'), type: 'text', - mode: ['properties', 'edit', 'create'], - group: gettext('Connection'), - }, - { - id: 'passfile', label: gettext('Passfile'), type: 'text', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], - }, - { - id: 'pub', label: gettext('Publication'), type: 'text', group: gettext('Connection'), - mode: ['properties'], - }, - { - id: 'cur_pub', label: gettext('Current publication'), type: 'text', group: gettext('Connection'), - mode: ['edit'], disabled:true, - }, - { - id: 'pub', label: gettext('Publication'), type: 'array', select2: { allowClear: true, multiple: true, width: '92%'}, - group: gettext('Connection'), mode: ['create', 'edit'], controlsClassName: 'pgadmin-controls pg-el-sm-11 pg-el-12', - deps: ['all_table', 'host', 'port', 'username', 'db', 'password'], disabled: 'isAllConnectionDataEnter', - helpMessage: gettext('Click the refresh button to get the publications'), - control: Backform.Select2Control.extend({ - defaults: _.extend(Backform.Select2Control.prototype.defaults, { - select2: { - allowClear: true, - selectOnBlur: true, - tags: true, - placeholder: gettext('Select an item...'), - width: 'style', - }, - }), - template: _.template([ - '', - '
', - '
', - ' ', - '
', - '', - '
', - '
', - '<% if (helpMessage && helpMessage.length) { %>', - '<%=helpMessage%>', - '<% } %>', - '
', - ].join('\n')), - - events: _.extend({}, Backform.Select2Control.prototype.events(), { - 'click .get_publication': 'getPublication', - }), - - render: function(){ - return Backform.Select2Control.prototype.render.apply(this, arguments); - }, - - getPublication: function() { - var self = this; - var publication_url = pgBrowser.Nodes['database'].generate_url.apply( - pgBrowser.Nodes['subscription'], [ - null, 'get_publications', this.field.get('node_data'), null, - this.field.get('node_info'), pgBrowser.Nodes['database'].url_jump_after_node, - ]); - var result = ''; - - $.ajax({ - url: publication_url, - type: 'GET', - data: self.model.toJSON(true, 'GET'), - dataType: 'json', - contentType: 'application/json', - }) - .done(function(res) { - result = res.data; - self.field.set('options', result); - Backform.Select2Control.prototype.render.apply(self, arguments); - - var transform = self.field.get('transform') || self.defaults.transform; - if (transform && _.isFunction(transform)) { - self.field.set('options', transform.bind(self, result)); - } else { - self.field.set('options', result); - } - Alertify.info( - gettext('Publication fetched successfully.') - ); - - - }) - .fail(function(res) { - Alertify.alert( - gettext('Check connection?'), - gettext(res.responseJSON.errormsg) - ); - }); - }, - }), - }, - { - id: 'sslmode', label: gettext('SSL mode'), control: 'select2', group: gettext('SSL'), - select2: { - allowClear: false, - minimumResultsForSearch: Infinity, - }, - mode: ['properties', 'edit', 'create'], - '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', 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', 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', 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', 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', - },{ - 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: 'copy_data_after_refresh', label: gettext('Copy data?'), - type: 'switch', mode: ['edit'], - group: gettext('With'), - readonly: 'isRefresh', deps :['refresh_pub'], - helpMessage: gettext('Specifies whether the existing data in the publications that are being subscribed to should be copied once the replication starts.'), - }, - { - id: 'copy_data', label: gettext('Copy data?'), - type: 'switch', mode: ['create'], - group: gettext('With'), - readonly: 'isConnect', deps :['connect'], - helpMessage: gettext('Specifies whether the existing data in the publications that are being subscribed to should be copied once the replication starts.'), - }, - { - id: 'create_slot', label: gettext('Create slot?'), - type: 'switch', mode: ['create'], - group: gettext('With'), - disabled: 'isSameDB', - readonly: 'isConnect', deps :['connect', 'host', 'port'], - helpMessage: gettext('Specifies whether the command should create the replication slot on the publisher.This field will be disabled and set to false if subscription connects to same database.Otherwise, the CREATE SUBSCRIPTION call will hang.'), - - }, { id: 'enabled', label: gettext('Enabled?'), - type: 'switch', mode: ['create','edit', 'properties'], + type: 'switch', mode: ['properties'], group: gettext('With'), readonly: 'isConnect', deps :['connect'], helpMessage: gettext('Specifies whether the subscription should be actively replicating, or whether it should be just setup but not started yet.'), }, { - id: 'refresh_pub', label: gettext('Refresh publication?'), - type: 'switch', mode: ['edit'], - group: gettext('With'), - helpMessage: gettext('Fetch missing table information from publisher.'), - deps:['enabled'], disabled: function(m){ - if (m.get('enabled')) - return false; - setTimeout( function() { - m.set('refresh_pub', false); - }, 10); - return true; - }, - },{ - id: 'connect', label: gettext('Connect?'), - type: 'switch', mode: ['create'], - group: gettext('With'), - disabled: 'isDisable', deps:['enabled', 'create_slot', 'copy_data'], - helpMessage: gettext('Specifies whether the CREATE SUBSCRIPTION should connect to the publisher at all. Setting this to false will change default values of enabled, create_slot and copy_data to false.'), - }, - { - id: 'slot_name', label: gettext('Slot name'), - type: 'text', mode: ['create','edit', 'properties'], - group: gettext('With'), - helpMessage: gettext('Name of the replication slot to use. The default behavior is to use the name of the subscription for the slot name.'), - }, - { - id: 'sync', label: gettext('Synchronous commit'), control: 'select2', deps:['event'], - group: gettext('With'), type: 'text', - helpMessage: gettext('The value of this parameter overrides the synchronous_commit setting. The default value is off.'), - select2: { - width: '100%', - allowClear: false, - }, - options:[ - {label: 'local', value: 'local'}, - {label: 'remote_write', value: 'remote_write'}, - {label: 'remote_apply', value: 'remote_apply'}, - {label: 'on', value: 'on'}, - {label: 'off', value: 'off'}, - ], + id: 'pub', label: gettext('Publication'), type: 'text', group: gettext('Connection'), + mode: ['properties'], }, ], - isDisable:function(m){ - if (m.isNew()) - return false; - return true; - }, - isSameDB:function(m){ - let host = m.attributes['host'], - port = m.attributes['port']; - - if ((m.attributes['host'] == 'localhost' || m.attributes['host'] == '127.0.0.1') && - (m.node_info.server.host == 'localhost' || m.node_info.server.host == '127.0.0.1')){ - host = m.node_info.server.host; - } - if (host == m.node_info.server.host && port == m.node_info.server.port){ - setTimeout( function() { - m.set('create_slot', false); - }, 10); - return true; - } - return false; - }, - isAllConnectionDataEnter: function(m){ - let host = m.get('host'), - db = m.get('db'), - port = m.get('port'), - username = m.get('username'); - if ((!_.isUndefined(host) && host) && (!_.isUndefined(db) && db) && (!_.isUndefined(port) && port) && (!_.isUndefined(username) && username)) - return false; - return true; - }, - isConnect: function(m){ - if(!m.get('connect')){ - setTimeout( function() { - m.set('copy_data', false); - m.set('create_slot', false); - m.set('enabled', false); - }, 10); - return true; - } - return false; - }, - isRefresh: function(m){ - if (!m.get('refresh_pub') || _.isUndefined(m.get('refresh_pub'))){ - setTimeout( function() { - m.set('copy_data_after_refresh', false); - }, 10); - return true; - } - return false; - }, - isSSL: function(model) { - var ssl_mode = model.get('sslmode'); - return _.indexOf(SSL_MODES, ssl_mode) == -1; - }, sessChanged: function() { if (!this.isNew() && _.isUndefined(this.attributes['refresh_pub'])) return false; return pgBrowser.DataModel.prototype.sessChanged.apply(this); }, - /* validate function is used to validate the input given by - * the user. In case of error, message will be displayed on - * the GUI for the respective control. - */ - validate: function() { - var msg; - this.errorModel.clear(); - var name = this.get('name'), - slot_name = this.get('slot_name'); - - if (_.isUndefined(name) || _.isNull(name) || - String(name).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Name cannot be empty.'); - this.errorModel.set('name', msg); - return msg; - } - - if (!_.isUndefined(slot_name) && !_.isNull(slot_name)){ - if(/^[a-zA-Z0-9_]+$/.test(slot_name) == false){ - msg = gettext('Replication slot name may only contain lower case letters, numbers, and the underscore character.'); - this.errorModel.set('name', msg); - return msg; - } - } - - const validateModel = new modelValidation.ModelValidation(this); - return validateModel.validate(); - }, canCreate: function(itemData, item) { var treeData = this.getTreeNodeHierarchy(item), server = treeData['server']; @@ -548,8 +168,58 @@ define('pgadmin.node.subscription', [ // by default we want to allow create menu return true; }, - }), + getSchema: function(treeNodeInfo, itemNodeData){ + return new SubscriptionSchema( + { + role:()=>getNodeListByName('role', treeNodeInfo, itemNodeData), + getPublication: (host, password, port, username, db, + connectTimeout, passfile, sslmode, + sslcompression, sslcert, sslkey, + sslrootcert, sslcrl) => + { + return new Promise((resolve, reject)=>{ + const api = getApiInstance(); + if(host != undefined && port!= undefined && username!= undefined && db != undefined){ + var _url = pgBrowser.Nodes['cast'].generate_url.apply( + pgBrowser.Nodes['subscription'], [ + null, 'get_publications', itemNodeData, false, + treeNodeInfo, + ]); + api.get(_url, { + params: {host, password, port, username, db, + connectTimeout, passfile, sslmode, + sslcompression, sslcert, sslkey, + sslrootcert, sslcrl}, + }) + .then(res=>{ + if ((res.data.errormsg === '') && !_.isNull(res.data.data)){ + resolve(res.data.data); + pgAlertify().info( + gettext('Publication fetched successfully.') + ); + }else if(!_.isNull(res.data.errormsg) && _.isNull(res.data.data)){ + reject(res.data.errormsg); + pgAlertify().alert( + gettext('Check connection?'), + gettext(res.data.errormsg) + ); + } + }) + .catch((err)=>{ + reject(err); + }); + } + }); + }, + },{ + node_info: treeNodeInfo.server, + }, + { + subowner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name, + }, + ); + }, }); } return pgBrowser.Nodes['coll-subscription']; diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js new file mode 100644 index 000000000..d9e64d6b9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js @@ -0,0 +1,448 @@ +///////////////////////////////////////////////////////////// +// +// 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 { isEmptyString } from 'sources/validators'; +import _ from 'lodash'; + +export default class SubscriptionSchema extends BaseUISchema{ + constructor(fieldOptions={}, node_info, initValues) { + super({ + name: undefined, + subowner: undefined, + pubtable: undefined, + connect_timeout: 10, + pub:[], + enabled:true, + create_slot: true, + copy_data:true, + connect:true, + copy_data_after_refresh:false, + sync:'off', + refresh_pub: false, + password: '', + sslmode: 'prefer', + sslcompression: false, + sslcert: '', + sslkey: '', + sslrootcert: '', + sslcrl: '', + host: '', + port: 5432, + db: 'postgres', + ...initValues, + }); + + this.fieldOptions = { + role: [], + publicationTable: [], + ...fieldOptions, + }; + this.node_info = node_info; + } + get idAttribute() { + return 'oid'; + } + + get SSL_MODES() { return ['prefer', 'require', 'verify-ca', 'verify-full']; } + + isDisable(){ + if (this.isNew()) + return false; + return true; + } + isSameDB(state){ + let host = state.host, + port = state.port; + + if ((state.host == 'localhost' || state.host == '127.0.0.1') && + (this.node_info['node_info'].host == 'localhost' || this.node_info['node_info'].host == '127.0.0.1')){ + host = this.node_info['node_info'].host; + } + if (host == this.node_info['node_info'].host && port == this.node_info['node_info'].port){ + state.create_slot = false; + return true; + } + return false; + } + isAllConnectionDataEnter(state){ + let host = state.host, + db = state.db, + port = state.port, + username = state.username; + if ((!_.isUndefined(host) && host) && (!_.isUndefined(db) && db) && (!_.isUndefined(port) && port) && (!_.isUndefined(username) && username)) + return false; + return true; + } + isConnect(state){ + if(!_.isUndefined(state.connect) && !state.connect){ + state.copy_data = false; + state.create_slot = false; + state.enabled = false; + return true; + } + return false; + } + isRefresh(state){ + if (!state.refresh_pub || _.isUndefined(state.refresh_pub)){ + state.copy_data_after_refresh = false; + return true; + } + return false; + } + isSSL(state) { + return this.SSL_MODES.indexOf(state.sslmode) == -1; + } + + get baseFields() { + let obj = this; + return [{ + id: 'name', label: gettext('Name'), type: 'text', + mode: ['properties', 'create', 'edit'], noEmpty: true, + visible: function() { + if(!_.isUndefined(this.node_info['node_info']) + && !_.isUndefined(this.node_info['node_info'].version) + && this.node_info['node_info'].version >= 100000) { + return true; + } + return false; + }, + },{ + id: 'oid', label: gettext('OID'), cell: 'string', mode: ['properties'], + type: 'text', + }, + { + id: 'subowner', label: gettext('Owner'), + options: this.fieldOptions.role, + type: 'select', + mode: ['edit', 'properties', 'create'], controlProps: { allowClear: false}, + disabled: function(){ + if(obj.isNew()) + return true; + return false; + }, + },{ + id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], + }, + { + id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], min: 1, max: 65535, + 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('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: 'password', label: gettext('Password'), type: 'password', maxlength: null, + group: gettext('Connection'), + mode: ['create', 'edit'], + deps: ['connect_now'], + }, + { + id: 'connect_timeout', label: gettext('Connection timeout'), type: 'text', + mode: ['properties', 'edit', 'create'], + group: gettext('Connection'), + }, + { + id: 'passfile', label: gettext('Passfile'), type: 'text', group: gettext('Connection'), + mode: ['properties', 'edit', 'create'], + }, + { + id: 'pub', label: gettext('Publication'), type: 'text', group: gettext('Connection'), + mode: ['properties'], + }, + { + id: 'pub', label: gettext('Publication'), + group: gettext('Connection'), mode: ['create', 'edit'], + deps: ['all_table', 'host', 'port', 'username', 'db', 'password'], disabled: obj.isAllConnectionDataEnter, + helpMessage: gettext('Click the refresh button to get the publications'), + type: (state)=>{ + return { + type: 'select-refresh', + controlProps: { allowClear: true, multiple: true, creatable: true, getOptionsOnRefresh: ()=>{ + return obj.fieldOptions.getPublication(state.host, state.password, state.port, state.username, state.db, + state.connect_timeout, state.passfile, state.sslmode, + state.sslcompression, state.sslcert, state.sslkey, + state.sslrootcert, state.sslcrl); + }}, + }; + }, + }, + + { + id: 'sslmode', label: gettext('SSL mode'), type: 'select', group: gettext('SSL'), + controlProps: { + allowClear: false, + }, + mode: ['properties', 'edit', 'create'], + 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, + 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, + controlProps: { + dialogType: 'select_file', supportedTypes: ['*'], + }, + deps: ['sslmode'], + },{ + id: 'sslrootcert', label: gettext('Root certificate'), type: 'file', + group: gettext('SSL'), mode: ['edit', 'create'], + disabled: obj.isSSL, + 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, + controlProps: { + dialogType: 'select_file', supportedTypes: ['*'], + }, + deps: ['sslmode'], + }, + { + id: 'sslcompression', label: gettext('SSL compression?'), type: 'switch', + mode: ['edit', 'create'], group: gettext('SSL'), + disabled: obj.isSSL, + 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: 'copy_data_after_refresh', label: gettext('Copy data?'), + type: 'switch', mode: ['edit'], + group: gettext('With'), + readonly: obj.isRefresh, deps :['refresh_pub'], + helpMessage: gettext('Specifies whether the existing data in the publications that are being subscribed to should be copied once the replication starts.'), + }, + { + id: 'copy_data', label: gettext('Copy data?'), + type: 'switch', mode: ['create'], + group: gettext('With'), + readonly: obj.isConnect, deps :['connect'], + helpMessage: gettext('Specifies whether the existing data in the publications that are being subscribed to should be copied once the replication starts.'), + }, + { + id: 'create_slot', label: gettext('Create slot?'), + type: 'switch', mode: ['create'], + group: gettext('With'), + disabled: obj.isSameDB, + readonly: obj.isConnect, deps :['connect', 'host', 'port'], + helpMessage: gettext('Specifies whether the command should create the replication slot on the publisher.This field will be disabled and set to false if subscription connects to same database.Otherwise, the CREATE SUBSCRIPTION call will hang.'), + + }, + { + id: 'enabled', label: gettext('Enabled?'), + type: 'switch', mode: ['create','edit', 'properties'], + group: gettext('With'), + readonly: obj.isConnect, deps :['connect'], + helpMessage: gettext('Specifies whether the subscription should be actively replicating, or whether it should be just setup but not started yet.'), + }, + { + id: 'refresh_pub', label: gettext('Refresh publication?'), + type: 'switch', mode: ['edit'], + group: gettext('With'), + helpMessage: gettext('Fetch missing table information from publisher.'), + deps:['enabled'], disabled: function(state){ + if (state.enabled) + return false; + state.refresh_pub = false; + return true; + }, + },{ + id: 'connect', label: gettext('Connect?'), + type: 'switch', mode: ['create'], + group: gettext('With'), + disabled: obj.isDisable, deps:['enabled', 'create_slot', 'copy_data'], + helpMessage: gettext('Specifies whether the CREATE SUBSCRIPTION should connect to the publisher at all. Setting this to false will change default values of enabled, create_slot and copy_data to false.'), + }, + { + id: 'slot_name', label: gettext('Slot name'), + type: 'text', mode: ['create','edit', 'properties'], + group: gettext('With'), + helpMessage: gettext('Name of the replication slot to use. The default behavior is to use the name of the subscription for the slot name.'), + }, + { + id: 'sync', label: gettext('Synchronous commit'), control: 'select2', deps:['event'], + group: gettext('With'), type: 'select', + helpMessage: gettext('The value of this parameter overrides the synchronous_commit setting. The default value is off.'), + controlProps: { + width: '100%', + allowClear: false, + }, + options:[ + {label: 'local', value: 'local'}, + {label: 'remote_write', value: 'remote_write'}, + {label: 'remote_apply', value: 'remote_apply'}, + {label: 'on', value: 'on'}, + {label: 'off', value: 'off'}, + ], + }, + ]; + } + + validate(state, setError) { + let errmsg = null; + errmsg = gettext('Either Host name, Address must be specified.'); + if(isEmptyString(state.host)) { + setError('host', errmsg); + return true; + } else { + errmsg = null; + setError('host', errmsg); + } + 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); + } + + if(isEmptyString(state.pub)) { + errmsg = gettext('Publication must be specified.'); + setError('pub', errmsg); + return true; + } else { + errmsg = null; + setError('pub', 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; + } + +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql index 1971bd0a4..f7a6baf3b 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/templates/subscriptions/sql/default/properties.sql @@ -2,7 +2,6 @@ SELECT sub.oid as oid, subname as name, subpublications as pub, sub.subsynccommit as sync, - subpublications as cur_pub, pga.rolname as subowner, subslotname as slot_name, subenabled as enabled, diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index 6719a941b..9e40f8e01 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -17,6 +17,7 @@ import Privilege from '../components/Privilege'; import { evalFunc } from 'sources/utils'; import PropTypes from 'prop-types'; import CustomPropTypes from '../custom_prop_types'; +import { SelectRefresh} from '../components/SelectRefresh'; /* Control mapping for form view */ function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, noLabel, ...props}) { @@ -74,6 +75,8 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i return ; case 'select': return ; + case 'select-refresh': + return ; case 'switch': return onTextChange(e.target.checked, e.target.name)} className={className} diff --git a/web/pgadmin/static/js/components/SelectRefresh.jsx b/web/pgadmin/static/js/components/SelectRefresh.jsx new file mode 100644 index 000000000..2d8c1c228 --- /dev/null +++ b/web/pgadmin/static/js/components/SelectRefresh.jsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import { Box} from '@material-ui/core'; +import {InputSelect, FormInput} from './FormComponents'; +import PropTypes from 'prop-types'; +import CustomPropTypes from '../custom_prop_types'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import { PgIconButton } from './Buttons'; + +export function SelectRefresh({ required, className, label, helpMessage, testcid, controlProps, ...props }){ + const [options, setOptions] = useState([]); + const [optionsReloadBasis, setOptionsReloadBasis] = useState(false); + const {getOptionsOnRefresh, ...selectControlProps} = controlProps; + + const onRefreshClick = ()=>{ + getOptionsOnRefresh && getOptionsOnRefresh() + .then((res)=>{ + setOptions(res); + setOptionsReloadBasis((prevVal)=>!prevVal); + }); + }; + return ( + + + + + + + } title={label||''}/> + + + + ); +} + +SelectRefresh.propTypes = { + required: PropTypes.bool, + label: PropTypes.string, + className: CustomPropTypes.className, + helpMessage: PropTypes.string, + testcid: PropTypes.string, + controlProps: PropTypes.object, +}; diff --git a/web/regression/javascript/components/SelectRefresh.spec.js b/web/regression/javascript/components/SelectRefresh.spec.js new file mode 100644 index 000000000..cd802ca13 --- /dev/null +++ b/web/regression/javascript/components/SelectRefresh.spec.js @@ -0,0 +1,69 @@ +///////////////////////////////////////////////////////////// +// +// 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 { FormHelperText, InputLabel } from '@material-ui/core'; + +import {SelectRefresh} from 'sources/components/SelectRefresh'; + +/* MUI Components need to be wrapped in Theme for theme vars */ +describe('components SelectRefresh', ()=>{ + 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('SelectRefresh', ()=>{ + let ThemedSelectRefresh = withTheme(SelectRefresh), ctrl, onChange=jasmine.createSpy('onChange'), + ctrlMount = (props)=>{ + ctrl?.unmount(); + ctrl = mount( + {} + }} + {...props} + />); + }; + + beforeEach(()=>{ + ctrlMount(); + }); + + it('accessibility', ()=>{ + expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid'); + expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid'); + }); + }); + +}); diff --git a/web/regression/javascript/schema_ui_files/cast.ui.spec.js b/web/regression/javascript/schema_ui_files/cast.ui.spec.js index e954c4400..1825d202d 100644 --- a/web/regression/javascript/schema_ui_files/cast.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/cast.ui.spec.js @@ -95,6 +95,16 @@ describe('CastSchema', ()=>{ />); }); + it('srctyp depChange', ()=>{ + let depChange = _.find(schemaObj.fields, (f)=>f.id=='srctyp').depChange; + depChange({srctyp: 'abc', trgtyp: 'abc'}); + }); + + it('trgtyp depChange', ()=>{ + let depChange = _.find(schemaObj.fields, (f)=>f.id=='trgtyp').depChange; + depChange({srctyp: 'abc', trgtyp: 'abc'}); + }); + it('validate', ()=>{ let state = {}; let setError = jasmine.createSpy('setError'); diff --git a/web/regression/javascript/schema_ui_files/subscription.ui.spec.js b/web/regression/javascript/schema_ui_files/subscription.ui.spec.js new file mode 100644 index 000000000..d84d130a7 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/subscription.ui.spec.js @@ -0,0 +1,167 @@ +///////////////////////////////////////////////////////////// +// +// 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 SubscriptionSchema from '../../../pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui'; + +describe('SubscriptionSchema', ()=>{ + let mount; + let schemaObj = new SubscriptionSchema( + { + getPublication: ()=>[], + role: ()=>[], + }, + { + node_info: { + connected: true, + user: {id: 10, name: 'postgres', is_superuser: true, can_create_role: true, can_create_db: true}, + user_id: 1, + user_name: 'postgres', + version: 130005, + server: {host: '127.0.0.1', port: 5432}, + }, + }, + { + subowner : '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 || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + + it('copy_data_after_refresh readonly', ()=>{ + let isReadonly = _.find(schemaObj.fields, (f)=>f.id=='copy_data_after_refresh').readonly; + isReadonly({host: '127.0.0.1', port : 5432}); + }); + + it('copy_data_after_refresh readonly', ()=>{ + let isReadonly = _.find(schemaObj.fields, (f)=>f.id=='copy_data_after_refresh').readonly; + isReadonly({refresh_pub : true}); + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + + state.host = null; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('host', 'Either Host name, Address must be specified.'); + + state.host = '127.0.0.1'; + 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; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('pub', 'Publication must be specified.'); + + state.pub = 'testPub'; + state.use_ssh_tunnel = 'Require'; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('tunnel_host', 'SSH Tunnel host must be specified.'); + + 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(); + }); + + + + +}); +