Port Subscriptions node to react. Fixes #6634

This commit is contained in:
Pradip Parkale 2021-08-04 11:24:15 +05:30 committed by Akshay Joshi
parent 8dac933a66
commit 1f430227aa
11 changed files with 856 additions and 444 deletions

View File

@ -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']) {

View File

@ -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{

View File

@ -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):

View File

@ -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([
'<label class="<%=Backform.controlLabelClassName%>" for="<%=cId%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">',
'<div class="input-group">',
' <select title="<%=name%>" id="<%=cId%>" class="<%=Backform.controlClassName%> <%=extraClasses.join(\' \')%>"',
' name="<%=name%>" value="<%-value%>" <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%>',
' <%=required ? "required" : ""%><%= select2.multiple ? " multiple>" : ">" %>',
' <%=select2.first_empty ? " <option></option>" : ""%>',
' <% for (var i=0; i < options.length; i++) {%>',
' <% var option = options[i]; %>',
' <option ',
' <% if (option.image) { %> data-image=<%=option.image%> <%}%>',
' value=<%- formatter.fromRaw(option.value) %>',
' <% if (option.selected) {%>selected="selected"<%} else {%>',
' <% if (!select2.multiple && option.value === rawValue) {%>selected="selected"<%}%>',
' <% if (select2.multiple && rawValue && rawValue.indexOf(option.value) != -1){%>selected="selected" data-index="rawValue.indexOf(option.value)"<%}%>',
' <%}%>',
' <%= disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%>><%-option.label%></option>',
' <%}%>',
' </select>',
'<div class="input-group-append">',
'<button class="btn btn-primary-icon fa fa-sync get_publication" <%=disabled ? "disabled" : ""%> <%=readonly ? "disabled" : ""%> aria-hidden="true" aria-label="' + gettext('Get Publication') + '" title="' + gettext('Get Publication') + '"></button>',
'</div>',
'</div>',
'<% if (helpMessage && helpMessage.length) { %>',
'<span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
'<% } %>',
'</div>',
].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'];

View File

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

View File

@ -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,

View File

@ -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 <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 'select-refresh':
return <SelectRefresh 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}

View File

@ -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 (
<FormInput required={required} label={label} className={className} helpMessage={helpMessage} testcid={testcid}>
<Box display="flex" >
<Box flexGrow="1">
<InputSelect {...props} options={options} optionsReloadBasis={optionsReloadBasis} controlProps={selectControlProps}/>
</Box>
<Box>
<PgIconButton onClick={onRefreshClick} icon={<RefreshIcon />} title={label||''}/>
</Box>
</Box>
</FormInput>
);
}
SelectRefresh.propTypes = {
required: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
controlProps: PropTypes.object,
};

View File

@ -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(
<ThemedSelectRefresh
label="First"
className="someClass"
testcid="inpCid"
helpMessage="some help message"
/* InputSelect */
readonly={false}
disabled={false}
value={1}
onChange={onChange}
controlProps={{
getOptionsOnRefresh: ()=>{}
}}
{...props}
/>);
};
beforeEach(()=>{
ctrlMount();
});
it('accessibility', ()=>{
expect(ctrl.find(InputLabel)).toHaveProp('htmlFor', 'inpCid');
expect(ctrl.find(FormHelperText)).toHaveProp('id', 'hinpCid');
});
});
});

View File

@ -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');

View File

@ -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(<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('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();
});
});