Port Role Reassign dialog to React. Fixes #7344

pull/90/head
Yogesh Mahajan 2022-08-05 16:04:15 +05:30 committed by Akshay Joshi
parent 4c6e7d4f4f
commit fa6b77b42c
9 changed files with 273 additions and 453 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -14,6 +14,7 @@ New features
Housekeeping
************
| `Issue #7344 <https://redmine.postgresql.org/issues/7344>`_ - Port Role Reassign dialog to React.
| `Issue #7567 <https://redmine.postgresql.org/issues/7567>`_ - Port About dialog to React.
| `Issue #7590 <https://redmine.postgresql.org/issues/7590>`_ - Port change ownership dialog to React.
| `Issue #7595 <https://redmine.postgresql.org/issues/7595>`_ - Update the container base image to Alpine 3.16 (with Python 3.10.5).

View File

@ -10,15 +10,12 @@ import RoleSchema from './role.ui';
import { getNodeVariableSchema } from '../../../static/js/variable.ui';
import { getNodeListByName } from '../../../../../static/js/node_ajax';
import { getMembershipSchema } from '../../../static/js/membership.ui';
import Notify from '../../../../../../static/js/helpers/Notifier';
import { showRoleReassign } from './roleReassign';
define('pgadmin.node.role', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'alertify',
'pgadmin.backform', 'axios', 'sources/utils', 'backbone', 'select2',
'pgadmin.browser.collection', 'pgadmin.browser.node.ui',
'pgadmin.browser.server.variable',
], function(gettext, url_for, $, _, pgAdmin, pgBrowser, alertify, Backform, axios, utils, Backbone) {
'sources/gettext', 'sources/url_for', 'underscore',
'sources/pgadmin', 'pgadmin.browser'
], function(gettext, url_for, _, pgAdmin, pgBrowser) {
if (!pgBrowser.Nodes['coll-role']) {
pgAdmin.Browser.Nodes['coll-role'] =
@ -127,450 +124,7 @@ define('pgadmin.node.role', [
return server.connected && node.can_login;
},
reassign_role: function() {
var tree = pgBrowser.tree,
_i = tree.selected(),
_d = _i ? tree.itemData(_i) : undefined,
obj = this, finalUrl, old_role_name;
//RoleReassign Model (Objects like role, database)
var RoleReassignObjectModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
role_op: undefined,
did: undefined,
new_role_id: undefined,
new_role_name: undefined,
old_role_name: undefined,
drop_with_cascade: false
},
// Default values!
initialize: function() {
// Set default options according to node type selection by user
Backbone.Model.prototype.initialize.apply(this, arguments);
},
schema: [
{
id: 'role_op',
label: gettext('Operation'),
cell: 'string',
type: 'radioModern',
controlsClassName: 'pgadmin-controls col-12 col-sm-8',
controlLabelClassName: 'control-label col-sm-4 col-12',
group: gettext('General'),
options: [{
'label': 'Reassign',
'value': 'reassign',
},
{
'label': 'Drop',
'value': 'drop',
},
],
helpMessage: gettext('Change the ownership or\ndrop the database objects owned by a database role'),
},
{
id: 'new_role_name',
label: gettext('Reassign objects to'),
controlsClassName: 'pgadmin-controls col-12 col-sm-8',
controlLabelClassName: 'control-label col-sm-4 col-12',
url: 'nodes',
helpMessage: gettext('New owner of the affected objects'),
transform: function(data, cell) {
var res = [],
control = cell || this,
node = control.field.get('schema_node');
// remove the current role from list
let current_label = control.field.attributes.node_info.role.label;
if (data && _.isArray(data)) {
let CURRENT_USER = {
label: 'CURRENT_USER', value: 'CURRENT_USER',
image: 'icon-' + node.type, _id: null,
},
SESSION_USER = {
label: 'SESSION_USER', value: 'SESSION_USER', image: 'icon-' + node.type, _id: null,
};
CURRENT_USER.value = JSON.stringify(CURRENT_USER);
SESSION_USER.value = JSON.stringify(SESSION_USER);
res.push(CURRENT_USER, SESSION_USER);
if(control.field.attributes.node_data.version >= 140000) {
let CURRENT_ROLE = {
label: 'CURRENT_ROLE', value: 'CURRENT_ROLE',
image: 'icon-' + node.type, _id: null,
};
CURRENT_ROLE.value = JSON.stringify(CURRENT_ROLE);
res.push(CURRENT_ROLE);
}
_.each(data, function(d) {
/*
* d contains json data and sets into
* select's option control
*
* We need to stringify data because formatter will
* convert Array Object as [Object] string
*/
if (current_label != d.label)
res.push({label: d.label, image: d.icon, value: JSON.stringify(d)});
});
}
return res;
},
control: Backform.NodeListByIdControl.extend({
getValueFromDOM: function() {
var data = this.formatter.toRaw(
_.unescape(this.$el.find('select').val()), this.model);
/*
* return null if data is empty to prevent it from
* throwing parsing error. Adds check as name can be empty
*/
if (data === '') {
return null;
}
else if (typeof(data) === 'string') {
data=JSON.parse(data);
}
return data.label;
},
/*
* When name is changed, extract value from its select option and
* set attributes values into the model
*/
onChange: function() {
Backform.NodeAjaxOptionsControl.prototype.onChange.apply(
this, arguments
);
var selectedValue = this.$el.find('select').val();
if (selectedValue.trim() != '') {
var d = this.formatter.toRaw(selectedValue, this.model);
if(typeof(d) === 'string')
d=JSON.parse(d);
this.model.set({
'new_role_id' : d._id,
'new_role_name': d.label,
});
}
}
}),
node: 'role',
group: gettext('General'),
select2: {
allowClear: false,
},
disabled: 'isDisabled',
deps: ['role_op'],
filter: function(d) {
// Exclude the currently selected
let ltree = pgBrowser.tree,
_idx = ltree.selected(),
_data = _idx && _idx.length == 1 ? ltree.itemData(_idx) : undefined;
if(!_data)
return true;
return d && (d.label != _data.label);
},
},
{
id: 'drop_with_cascade',
label: gettext('Cascade?'),
cell: 'string',
type: 'switch',
controlsClassName: 'pgadmin-controls col-12 col-sm-8',
controlLabelClassName: 'control-label col-sm-4 col-12',
disabled: 'isDisabled',
group: gettext('General'),
options: {
'onText': gettext('Yes'),
'offText': gettext('No'), 'size': 'mini'
},
deps: ['role_op'],
helpMessage: gettext('Note: CASCADE will automatically drop objects that depend on the affected objects, and in turn all objects that depend on those objects'),
},
{
id: 'did',
label: gettext('From database'),
controlsClassName: 'pgadmin-controls col-12 col-sm-8',
controlLabelClassName: 'control-label col-sm-4 col-12',
node: 'database',
group: gettext('General'),
disabled: 'isDisabled',
control: Backform.NodeListByIdControl.extend({
onChange: function() {
Backform.NodeListByIdControl.prototype.onChange.apply(
this, arguments
);
let did = this.model.get('did');
this.model.set('did', parseInt(did));
},
}),
select2: {
allowClear: false,
},
events: {
'select2:select': 'onChange'
},
first_empty: false,
helpMessage: gettext('Target database on which the operation will be carried out'),
},
{
id: 'sqltab', label: gettext('SQL'), group: gettext('SQL'),
type: 'text', disabled: false, control: Backform.SqlTabControl.extend({
initialize: function() {
// Initialize parent class
Backform.SqlTabControl.prototype.initialize.apply(this, arguments);
},
onTabChange: function(sql_tab_obj) {
// Fetch the information only if the SQL tab is visible at the moment.
if (this.dialog && sql_tab_obj.shown == this.tabIndex) {
var self = this,
roleReassignData = self.model.toJSON(),
getUrl;
// Add existing role
roleReassignData.old_role_name = old_role_name;
getUrl = obj.generate_url(_i, 'reassign' , _d, true);
$.ajax({
url: getUrl,
type: 'GET',
cache: false,
data: roleReassignData,
dataType: 'json',
contentType: 'application/json',
}).done(function(res) {
self.sqlCtrl.setValue(res.data);
});
}
},
}),
}
],
validate: function() {
return null;
},
isDisabled: function(m) {
let self_local = this;
switch(this.name) {
case 'new_role_name':
return (m.get('role_op') != 'reassign');
case 'drop_with_cascade':
return (m.get('role_op') != 'drop');
case 'did':
setTimeout(function() {
if(_.isUndefined(m.get('did'))) {
let db = self_local.options[0];
m.set('did', db.value);
}
}, 10);
return false;
default:
return false;
}
},
});
if (!_d)
return;
if (!alertify.roleReassignDialog) {
alertify.dialog('roleReassignDialog', function factory() {
return {
main: function(title) {
this.set('title', title);
},
setup: function() {
return {
buttons:[{
text: '', key: 112,
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
attrs:{name:'dialog_help', type:'button', label: gettext('Users'),
url: url_for('help.static', {'filename': 'role_reassign_dialog.html'})},
},{
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button',
}, {
text: gettext('OK'),
key: 13,
className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button',
}],
focus: {
element: 0,
},
options: {
//disable both padding and overflow control.
padding : !1,
overflow: !1,
modal: false,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
},
};
},
build: function() {
alertify.pgDialogBuild.apply(this);
},
hooks:{
onclose: function() {
if (this.view) {
// clear our backform model/view
this.view.remove({data: true, internal: true, silent: true});
}
},
},
prepare:function() {
var self = this,
$container = $('<div class=\'role_reassign_own\'></div>');
//Disable Okay button
self.__internal.buttons[2].element.disabled = true;
// Find current/selected node
var ltree = pgBrowser.tree,
_idx = ltree.selected(),
_data = _idx ? ltree.itemData(_idx) : undefined,
node = _data && pgBrowser.Nodes[_data._type];
finalUrl = obj.generate_url(_idx, 'reassign' , _data, true);
old_role_name = _data.label;
if (!_data)
return;
// Create treeInfo
var treeInfo = pgBrowser.tree.getTreeNodeHierarchy(_idx);
// Instance of backbone model
var newModel = new RoleReassignObjectModel({}, {node_info: treeInfo}),
fields = Backform.generateViewSchema(
treeInfo, newModel, 'create', node,
treeInfo.server, true
);
var view = self.view = new Backform.Dialog({
el: $container, model: newModel, schema: fields,
});
// Add our class to alertify
$(self.elements.body.childNodes[0]).addClass(
'alertify_tools_dialog_properties obj_properties'
);
// Render dialog
view.render();
self.elements.content.append($container.get(0));
const statusBar = $(
'<div class=\'pg-prop-status-bar pg-prop-status-bar-absolute pg-el-xs-12 d-none\'>' +
' <div class="error-in-footer"> ' +
' <div class="d-flex px-2 py-1"> ' +
' <div class="pr-2"> ' +
' <i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i> ' +
' </div> ' +
' <div class="alert-text" role="alert"></div> ' +
' <div class="ml-auto close-error-bar"> ' +
' <a aria-label="' + gettext('Close error bar') + '" class="close-error fa fa-times text-danger"></a> ' +
' </div> ' +
' </div> ' +
' </div> ' +
'</div>').appendTo($container);
// Listen to model & if filename is provided then enable Backup button
this.view.model.on('change', function() {
const ctx = this;
const showError = function(errorField, errormsg) {
ctx.errorModel.set(errorField, errormsg);
statusBar.removeClass('d-none');
statusBar.find('.alert-text').html(errormsg);
self.elements.dialog.querySelector('.close-error').addEventListener('click', ()=>{
statusBar.addClass('d-none');
ctx.errorModel.set(errorField, errormsg);
});
};
statusBar.addClass('d-none');
if ((this.get('role_op') == 'reassign')
&& !_.isUndefined(this.get('new_role_name')
&& this.get('new_role_name') !== '')
) {
this.errorModel.clear();
self.__internal.buttons[2].element.disabled = false;
} else if(this.get('role_op') == 'drop') {
this.errorModel.clear();
this.set({'new_role_name': undefined, silent: true});
this.set({'new_role_id': undefined, silent: true});
self.__internal.buttons[2].element.disabled = false;
} else if(_.isUndefined(this.get('new_role_name'))) {
let errmsg = gettext('Please provide a new role name');
this.errorModel.set('new_role_name', errmsg);
showError('new_role_name', errmsg);
self.__internal.buttons[2].element.disabled = true;
}
else {
self.__internal.buttons[2].element.disabled = true;
}
});
// set default role operation as reassign
this.view.model.set({'role_op': 'reassign'});
},
// Callback functions when click on the buttons of the alertify dialogs
callback: function(e) {
if (e.button.element.name == 'dialog_help') {
e.cancel = true;
pgBrowser.showHelp(e.button.element.name, e.button.element.getAttribute('url'),
null, null);
return;
}
if (e.button.text === gettext('OK')) {
let roleReassignData = this.view.model.toJSON(),
roleOp = roleReassignData.role_op,
confirmBoxTitle = utils.titleize(roleOp);
Notify.confirm(
gettext('%s Objects', confirmBoxTitle),
gettext('Are you sure you wish to %s all the objects owned by the selected role?', roleOp),
function() {
axios.post(
finalUrl,
roleReassignData
).then(function (response) {
if(response.data)
Notify.success(response.data.info);
}).catch(function (error) {
try {
const err = error.response.data;
Notify.alert(
gettext('Role reassign/drop failed.'),
err.errormsg
);
} catch (ex) {
console.warn(ex.stack || ex);
}
});
},
function() { return true; }
).set('labels', {
ok: gettext('Yes'),
cancel: gettext('No'),
});
}
},
};
});
}
alertify.roleReassignDialog(
gettext('Reassign/Drop Owned - \'%s\'', _d.label)
).resizeTo(pgAdmin.Browser.stdW.md, pgAdmin.Browser.stdH.lg);
showRoleReassign();
},
getSchema: function(treeNodeInfo, itemNodeData) {
return new RoleSchema(

View File

@ -0,0 +1,232 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, 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 url_for from 'sources/url_for';
import { getNodeListByName, generateNodeUrl } from '../../../../../static/js/node_ajax';
import pgBrowser from 'top/browser/static/js/browser';
import { getUtilityView } from '../../../../../static/js/utility_view';
import Notify from '../../../../../../static/js/helpers/Notifier';
import { isEmptyString } from 'sources/validators';
import pgAdmin from 'sources/pgadmin';
export default class RoleReassign extends BaseUISchema{
constructor(fieldOptions={}, initValues={}){
super({
role_op: 'reassign',
did: undefined,
new_role_id: undefined,
new_role_name: undefined,
drop_with_cascade: false,
old_role_name: initValues.old_role_name,
...initValues
});
this.fieldOptions = {
roleList: fieldOptions.roleList,
databaseList: fieldOptions.databaseList,
nodeInfo: fieldOptions.nodeInfo,
...fieldOptions,
};
this.nodeInfo = this.fieldOptions.nodeInfo;
this.warningText = null;
}
get idAttribute() {
return 'oid';
}
get baseFields(){
let obj = this;
return [
{
id: 'role_op',
label: gettext('Operation'),
group: gettext('General'),
type: 'toggle',
options: [
{ 'label': gettext('Reassign'), 'value': 'reassign' },
{ 'label': gettext('Drop'), 'value': 'drop' },
],
helpMessage: gettext('Change the ownership or\ndrop the database objects owned by a database role')
},
{
id: 'new_role_id',
label: gettext('Reassign objects to'),
group: gettext('General'),
type: ()=>{
return{
type: 'select',
options: this.fieldOptions.roleList,
optionsLoaded: (options) => { obj.roleNameIdList = options; },
controlProps: {
allowClear: false,
filter: (options)=>{
let data = [];
let CURRENT_USER = {
label: 'CURRENT_USER', value: 'CURRENT_USER', image: 'icon-role'
},
SESSION_USER = {
label: 'SESSION_USER', value: 'SESSION_USER', image: 'icon-role'
};
data.push(CURRENT_USER, SESSION_USER);
if (obj.getServerVersion() >= 140000){
let CURRENT_ROLE = {
label: 'CURRENT_ROLE', value: 'CURRENT_ROLE', image: 'icon-role'
};
data.push(CURRENT_ROLE);
}
if (options && _.isArray(options)){
_.each(options, function(d) {
// omit currently selected role
if(d._id != obj.nodeInfo.role._id){
data.push({label: d.label, value: d._id, image: d.image});
}
});
}
return data;
}
}
};
},
helpMessage: gettext('New owner of the affected objects'),
deps: ['role_op'],
disabled: (state)=>{
return state.role_op == 'drop'? true: false;
},
depChange: (state) =>{
if (state.role_op == 'drop'){
return {new_role_id:''};
}
}
},
{ /* this is dummy field not shown on UI but added as API require this value */
id: 'new_role_name',
visible: false,
type: '',
deps:['new_role_id'],
depChange: (state)=>{
let new_role_name;
if (['CURRENT_USER','SESSION_USER','CURRENT_ROLE'].includes(state.new_role_id)){
new_role_name = state.new_role_id;
}else{
new_role_name = obj.roleNameIdList.find(o=> o._id === state.new_role_id).label;
}
return {new_role_name: new_role_name};
}
},
{
id: 'drop_with_cascade',
label: gettext('Cascade?'),
group: gettext('General'),
type: 'switch',
deps: ['role_op'],
helpMessage: gettext('Note: CASCADE will automatically drop objects that depend on the affected objects, and in turn all objects that depend on those objects')
},
{
id: 'did',
label: gettext('From database'),
group: gettext('General'),
helpMessage: gettext('Target database on which the operation will be carried out'),
type: ()=>{
return {
type: 'select',
options: this.fieldOptions.databaseList,
controlProps: {
allowClear: false,
filter: (options)=>{
let data = [];
if (options && _.isArray(options)){
_.each(options, function(d) {
data.push({label: d.label, value: d._id, image: d.image});
});
}
return data;
}
},
};
}
}
];
}
validate(state, setError) {
let errmsg = null;
let obj = this;
if (state.role_op == 'reassign' && isEmptyString(state.new_role_id)) {
errmsg = gettext('\'Reassign objects to\' can not be empty');
setError('new_role_id', errmsg);
return true;
}
if (isEmptyString(state.did)) {
errmsg = gettext('\'From database \' can not be empty');
setError('did', errmsg);
return true;
}
obj.warningText = gettext(`Are you sure you wish to ${state.role_op} all the objects owned by the selected role?`);
return false;
}
}
function saveCallBack (data) {
if (data.errormsg) {
Notify.alert(
gettext('Error'),
gettext(data.errormsg)
);
} else {
Notify.success(gettext(data.info));
}
}
function getUISchema(treeNodeInfo, itemNodeData ) {
return new RoleReassign(
{
roleList: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData, {includeItemKeys: ['_id']}),
databaseList: ()=>getNodeListByName('database', treeNodeInfo, itemNodeData, {cacheLevel: 'database', cacheNode: 'database', includeItemKeys: ['_id']}),
nodeInfo: treeNodeInfo
},
{
old_role_name: itemNodeData.label
}
);
}
export function showRoleReassign() {
let tree = pgBrowser.tree,
item = tree.selected(),
data = item ? tree.itemData(item) : undefined,
treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(item),
itemNodeData = pgBrowser.tree.findNodeByDomElement(item).getData();
pgBrowser.Node.registerUtilityPanel();
let panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md, 480),
j = panel.$container.find('.obj_properties').first();
panel.title(gettext(`Reassign/Drop Owned - ${data.label}`));
panel.focus();
const baseUrl = generateNodeUrl.call( pgAdmin.Browser.Nodes[data._type], treeNodeInfo, 'reassign', data, true);
let schema = getUISchema(treeNodeInfo, itemNodeData),
sqlHelpUrl = '',
msqlurl = generateNodeUrl.call( pgAdmin.Browser.Nodes[data._type], treeNodeInfo, 'reassign', data, true),
extraData = {nodeType: data._type, msqlurl:msqlurl},
helpUrl = url_for('help.static', {
'filename': 'role_reassign_dialog.html',
});
getUtilityView(
schema, treeNodeInfo, 'create', 'dialog', j[0], panel, saveCallBack, extraData, 'Reassign/Drop', baseUrl, sqlHelpUrl, helpUrl);
}

View File

@ -16,6 +16,7 @@ import SchemaView from 'sources/SchemaView';
import 'wcdocker';
import Theme from '../../../static/js/Theme';
import url_for from 'sources/url_for';
import { generateNodeUrl } from './node_ajax';
/* The entry point for rendering React based view in properties, called in node.js */
export function getUtilityView(schema, treeNodeInfo, actionType, formType, container, containerPanel,
@ -33,6 +34,10 @@ export function getUtilityView(schema, treeNodeInfo, actionType, formType, conta
/* button icons */
const saveBtnIcon = extraData.save_btn_icon;
/* Node type & Noen obj*/
let nodeObj = extraData.nodeType? pgAdmin.Browser.Nodes[extraData.nodeType]: undefined;
let itemNodeData = extraData?.itemNodeData ? itemNodeData: undefined;
/* on save button callback, promise required */
const onSaveClick = (isNew, data)=>new Promise((resolve, reject)=>{
return api({
@ -49,6 +54,23 @@ export function getUtilityView(schema, treeNodeInfo, actionType, formType, conta
});
});
/* Called when switched to SQL tab, promise required */
const getSQLValue = (isNew, changedData)=>{
const msqlUrl = extraData?.msqlurl ? extraData.msqlurl: generateNodeUrl.call(nodeObj, treeNodeInfo, 'msql', itemNodeData, !isNew, nodeObj.url_jump_after_node);
return new Promise((resolve, reject)=>{
api({
url: msqlUrl,
method: 'GET',
params: changedData,
}).then((res)=>{
resolve(res.data.data);
}).catch((err)=>{
onError(err);
reject(err);
});
});
};
/* Callback for help button */
const onHelp = (isSqlHelp=false)=>{
if(isSqlHelp) {
@ -100,6 +122,15 @@ export function getUtilityView(schema, treeNodeInfo, actionType, formType, conta
});
let onError = (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);
}
};
let _schema = schema;
/* Fire at will, mount the DOM */
@ -117,7 +148,8 @@ export function getUtilityView(schema, treeNodeInfo, actionType, formType, conta
onHelp={onHelp}
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
confirmOnCloseReset={confirmOnReset}
hasSQL={false}
hasSQL={nodeObj?nodeObj.hasSQL:false && (actionType === 'create' || actionType === 'edit')}
getSQLValue={getSQLValue}
isTabView={isTabView}
disableSqlHelp={sqlHelpUrl == undefined || sqlHelpUrl == ''}
disableDialogHelp={helpUrl == undefined || helpUrl == ''}

View File

@ -27,7 +27,6 @@ const useStyles = makeStyles(() =>
}
}),
);
const classes = useStyles();
// Azure credentials
export function AzureCredentials(props) {
@ -113,6 +112,7 @@ AzureCredentials.propTypes = {
// Azure Instance
export function AzureInstanceDetails(props) {
const [azureInstanceSchema, setAzureInstanceSchema] = React.useState();
const classes = useStyles();
React.useMemo(() => {
const AzureSchema = new AzureClusterSchema({
@ -213,6 +213,7 @@ AzureInstanceDetails.propTypes = {
// Azure Database Details
export function AzureDatabaseDetails(props) {
const [azureDBInstance, setAzureDBInstance] = React.useState();
const classes = useStyles();
React.useMemo(() => {
const azureDBSchema = new AzureDatabaseSchema({