diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx
index 23a69c895..d1e1cde82 100644
--- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx
+++ b/web/pgadmin/static/js/SchemaView/DataGridView.jsx
@@ -9,7 +9,7 @@
/* The DataGridView component is based on react-table component */
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons';
@@ -28,6 +28,9 @@ import FormView from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types';
import { evalFunc } from 'sources/utils';
+import { useOnScreen } from '../custom_hooks';
+import { DepListenerContext } from './DepListener';
+import { getSchemaRow } from '../utils';
const useStyles = makeStyles((theme)=>({
grid: {
@@ -57,6 +60,9 @@ const useStyles = makeStyles((theme)=>({
padding: 0,
minWidth: 0,
backgroundColor: 'inherit',
+ '&.Mui-disabled': {
+ border: 0,
+ },
},
gridTableContainer: {
overflow: 'auto',
@@ -138,13 +144,33 @@ DataTableHeader.propTypes = {
headerGroups: PropTypes.array.isRequired,
};
-function DataTableRow({row, totalRows, isResizing}) {
+function DataTableRow({row, totalRows, isResizing, schema, accessPath}) {
const classes = useStyles();
const [key, setKey] = useState(false);
+ const depListener = useContext(DepListenerContext);
/* Memoize the row to avoid unnecessary re-render.
* If table data changes, then react-table re-renders the complete tables
* We can avoid re-render by if row data is not changed
*/
+
+ useEffect(()=>{
+ /* Calculate the fields which depends on the current field
+ deps has info on fields which the current field depends on. */
+ schema.fields.forEach((field)=>{
+ /* Self change is also dep change */
+ if(field.depChange) {
+ depListener.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange);
+ }
+ (evalFunc(null, field.deps) || []).forEach((dep)=>{
+ let source = accessPath.concat(dep);
+ if(_.isArray(dep)) {
+ source = dep;
+ }
+ depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
+ });
+ });
+ }, []);
+
let depsMap = _.values(row.values, Object.keys(row.values).filter((k)=>!k.startsWith('btn')));
depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing]);
return useMemo(()=>
@@ -168,18 +194,7 @@ function DataTableRow({row, totalRows, isResizing}) {
export default function DataGridView({
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) {
const classes = useStyles();
- /* Calculate the fields which depends on the current field
- deps has info on fields which the current field depends on. */
- const dependsOnField = useMemo(()=>{
- let res = {};
- schema.fields.forEach((field)=>{
- (field.deps || []).forEach((dep)=>{
- res[dep] = res[dep] || [];
- res[dep].push(field.id);
- });
- });
- return res;
- }, []);
+
/* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema);
let columns = useMemo(
@@ -195,11 +210,17 @@ export default function DataGridView({
dataType: 'edit',
width: 30,
minWidth: '0',
- Cell: ({row})=>} className={classes.gridRowButton}
- onClick={()=>{
- row.toggleRowExpanded(!row.isExpanded);
- }}
- />
+ Cell: ({row})=>{
+ let canEditRow = true;
+ if(props.canEditRow) {
+ canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});
+ }
+ return } className={classes.gridRowButton}
+ onClick={()=>{
+ row.toggleRowExpanded(!row.isExpanded);
+ }} disabled={!canEditRow}
+ />
+ }
};
colInfo.Cell.displayName = 'Cell',
colInfo.Cell.propTypes = {
@@ -218,17 +239,23 @@ export default function DataGridView({
width: 30,
minWidth: '0',
Cell: ({row}) => {
+ let canDeleteRow = true;
+ if(props.canDeleteRow) {
+ canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
+ }
+
return (
}
onClick={()=>{
confirmDeleteRow(()=>{
+ /* Get the changes on dependent fields as well */
dataDispatch({
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
path: accessPath,
value: row.index,
});
}, ()=>{}, props.customDeleteTitle, props.customDeleteMsg);
- }} className={classes.gridRowButton} />
+ }} className={classes.gridRowButton} disabled={!canDeleteRow} />
);
}
};
@@ -287,25 +314,10 @@ export default function DataGridView({
disabled={!editable}
visible={_visible}
onCellChange={(value)=>{
- /* Get the changes on dependent fields as well.
- * The return value of depChange function is merged and passed to state.
- */
- const depChange = (state)=>{
- let rowdata = _.get(state, accessPath.concat(row.index));
- _field.depChange && _.merge(rowdata, _field.depChange(rowdata, _field.id) || {});
- (dependsOnField[_field.id] || []).forEach((d)=>{
- d = _.find(schemaRef.current.fields, (f)=>f.id==d);
- if(d.depChange) {
- _.merge(rowdata, d.depChange(rowdata, _field.id) || {});
- }
- });
- return state;
- };
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat([row.index, _field.id]),
value: value,
- depChange: depChange,
});
}}
reRenderRow={other.reRenderRow}
@@ -326,12 +338,7 @@ export default function DataGridView({
);
const onAddClick = useCallback(()=>{
- let newRow = {};
- columns.forEach((column)=>{
- if(column.field) {
- newRow[column.field.id] = schemaRef.current.defaults[column.field.id];
- }
- });
+ let newRow = schemaRef.current.getNewData();
dataDispatch({
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
path: accessPath,
@@ -387,12 +394,12 @@ export default function DataGridView({
{rows.map((row, i) => {
prepareRow(row);
return
-
+
{props.canEdit && row.isExpanded &&
+ schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}
+ isDataGridForm={true}/>
}
;
})}
diff --git a/web/pgadmin/static/js/SchemaView/DepListener.js b/web/pgadmin/static/js/SchemaView/DepListener.js
new file mode 100644
index 000000000..13ad9c302
--- /dev/null
+++ b/web/pgadmin/static/js/SchemaView/DepListener.js
@@ -0,0 +1,77 @@
+import _ from 'lodash';
+import React from 'react';
+
+export const DepListenerContext = React.createContext();
+
+export default class DepListener {
+ constructor() {
+ this._depListeners = [];
+ }
+
+ /* Will keep track of the dependent fields and there callbacks */
+ addDepListener(source, dest, callback, defCallback) {
+ this._depListeners = this._depListeners || [];
+ this._depListeners.push({
+ source: source,
+ dest: dest,
+ callback: callback,
+ defCallback: defCallback
+ });
+ }
+
+ _getListenerData(state, listener, actionObj) {
+ /* Get data at same level */
+ let data = state;
+ let dataPath = _.slice(listener.dest, 0, -1);
+ if(dataPath.length > 0) {
+ data = _.get(state, dataPath);
+ }
+ data = _.assign(data, listener.callback && listener.callback(data, listener.source, state, actionObj) || {});
+ return state;
+ }
+
+ _getDefListenerPromise(state, listener, actionObj) {
+ /* Get data at same level */
+ let data = state;
+ let dataPath = _.slice(listener.dest, 0, -1);
+ if(dataPath.length > 0) {
+ data = _.get(state, dataPath);
+ }
+ return (listener.defCallback && listener.defCallback(data, listener.source, state, actionObj));
+ }
+
+ /* Called when any field changed and trigger callbacks */
+ getDepChange(currPath, state, actionObj) {
+ if(actionObj.depChangeResolved) {
+ state = this._getListenerData(state, {callback: actionObj.depChangeResolved}, actionObj);
+ } else {
+ let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|')));
+ if(allListeners) {
+ for(const listener of allListeners) {
+ state = this._getListenerData(state, listener, actionObj);
+ }
+ }
+ }
+ return state;
+ }
+
+ getDeferredDepChange(currPath, state, actionObj) {
+ let deferredList = [];
+ let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|')));
+ if(allListeners) {
+ for(const listener of allListeners) {
+ if(listener.defCallback) {
+ let thePromise = this._getDefListenerPromise(state, listener, actionObj);
+ if(thePromise) {
+ deferredList.push({
+ action: actionObj,
+ promise: thePromise,
+ });
+ }
+ }
+
+ }
+ }
+ return deferredList;
+ }
+}
diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx
index 020e54efe..c9891c6cf 100644
--- a/web/pgadmin/static/js/SchemaView/FormView.jsx
+++ b/web/pgadmin/static/js/SchemaView/FormView.jsx
@@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
import _ from 'lodash';
import PropTypes from 'prop-types';
@@ -21,6 +21,8 @@ import { InputSQL } from '../components/FormComponents';
import gettext from 'sources/gettext';
import { evalFunc } from 'sources/utils';
import CustomPropTypes from '../custom_prop_types';
+import { useOnScreen } from '../custom_hooks';
+import { DepListenerContext } from './DepListener';
const useStyles = makeStyles((theme)=>({
fullSpace: {
@@ -33,6 +35,9 @@ const useStyles = makeStyles((theme)=>({
nestedTabPanel: {
backgroundColor: theme.otherVars.headerBg,
},
+ nestedControl: {
+ height: 'unset',
+ }
}));
/* Optional SQL tab */
@@ -67,28 +72,52 @@ SQLTab.propTypes = {
/* The first component of schema view form */
export default function FormView({
- value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab, getSQLValue, onTabChange, firstEleRef, className}) {
+ value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
+ getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false}) {
let defaultTab = 'General';
let tabs = {};
let tabsClassname = {};
const [tabValue, setTabValue] = useState(0);
const classes = useStyles();
const firstElement = useRef();
+ const formRef = useRef();
+ const onScreenTracker = useRef(false);
+ const depListener = useContext(DepListenerContext);
let groupLabels = {};
schema = schema || {fields: []};
- /* Calculate the fields which depends on the current field
- deps has info on fields which the current field depends on. */
- const dependsOnField = useMemo(()=>{
- let res = {};
- schema.fields.forEach((field)=>{
- (field.deps || []).forEach((dep)=>{
- res[dep] = res[dep] || [];
- res[dep].push(field.id);
+ let isOnScreen = useOnScreen(formRef);
+ if(isOnScreen) {
+ /* Don't do it when the form is alredy visible */
+ if(onScreenTracker.current == false) {
+ /* Re-select the tab. If form is hidden then sometimes it is not selected */
+ setTabValue(tabValue);
+ onScreenTracker.current = true;
+ }
+ } else {
+ onScreenTracker.current = false;
+ }
+
+ useEffect(()=>{
+ /* Calculate the fields which depends on the current field */
+ if(!isDataGridForm) {
+ schema.fields.forEach((field)=>{
+ /* Self change is also dep change */
+ if(field.depChange || field.deferredDepChange) {
+ depListener.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
+ }
+ (evalFunc(null, field.deps) || []).forEach((dep)=>{
+ let source = accessPath.concat(dep);
+ if(_.isArray(dep)) {
+ source = dep;
+ }
+ if(field.depChange) {
+ depListener.addDepListener(source, accessPath.concat(field.id), field.depChange);
+ }
+ });
});
- });
- return res;
+ }
}, []);
/* Prepare the array of components based on the types */
@@ -127,19 +156,36 @@ export default function FormView({
/* Lets choose the path based on type */
if(field.type === 'nested-tab') {
/* Pass on the top schema */
- field.schema.top = schema.top;
+ if(isNested) {
+ field.schema.top = schema.top;
+ } else {
+ field.schema.top = schema;
+ }
+
tabs[group].push(
+ schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} {...field}/>
);
} else if(field.type === 'collection') {
- /* Pass on the top schema */
- field.schema.top = schema.top;
/* If its a collection, let data grid view handle it */
+ let depsMap = [value[field.id]];
+ /* Pass on the top schema */
+ if(isNested) {
+ field.schema.top = schema.top;
+ } else {
+ field.schema.top = schema;
+ }
+
+ /* Eval the params based on state */
+ let {canAdd, canEdit, canDelete, ..._field} = field;
+ canAdd = evalFunc(schema, canAdd, value);
+ canEdit = evalFunc(schema, canAdd, value);
+ canDelete = evalFunc(schema, canAdd, value);
+
tabs[group].push(
- useMemo(()=>, [value[field.id]])
+ useMemo(()=>, depsMap)
);
} else if(field.type === 'group') {
groupLabels[field.id] = field.label;
@@ -171,21 +217,10 @@ export default function FormView({
{...field}
onChange={(value)=>{
/* Get the changes on dependent fields as well */
- const depChange = (state)=>{
- field.depChange && _.merge(state, field.depChange(state) || {});
- (dependsOnField[field.id] || []).forEach((d)=>{
- d = _.find(schema.fields, (f)=>f.id==d);
- if(d.depChange) {
- _.merge(state, d.depChange(state) || {});
- }
- });
- return state;
- };
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat(field.id),
value: value,
- depChange: depChange,
});
}}
hasError={hasError}
@@ -197,7 +232,7 @@ export default function FormView({
_visible,
hasError,
classes.controlRow,
- ...(field.deps || []).map((dep)=>value[dep])
+ ...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
])
);
}
@@ -206,8 +241,8 @@ export default function FormView({
/* Add the SQL tab if required */
let sqlTabActive = false;
+ let sqlTabName = gettext('SQL');
if(hasSQLTab) {
- let sqlTabName = gettext('SQL');
sqlTabActive = (Object.keys(tabs).length === tabValue);
/* Re-render and fetch the SQL tab when it is active */
tabs[sqlTabName] = [
@@ -226,7 +261,7 @@ export default function FormView({
return (
<>
-
+
{Object.keys(tabs).map((tabName, i)=>{
return (
-
+
{tabs[tabName]}
);
diff --git a/web/pgadmin/static/js/SchemaView/base_schema.ui.js b/web/pgadmin/static/js/SchemaView/base_schema.ui.js
index 7f9091dda..b8aa7cbea 100644
--- a/web/pgadmin/static/js/SchemaView/base_schema.ui.js
+++ b/web/pgadmin/static/js/SchemaView/base_schema.ui.js
@@ -7,6 +7,8 @@
//
//////////////////////////////////////////////////////////////
+import _ from "lodash";
+
/* This is the base schema class for SchemaView.
* A UI schema must inherit this to use SchemaView for UI.
*/
@@ -28,8 +30,7 @@ export default class BaseUISchema {
}
get top() {
- /* If no top, I'm the top */
- return this._top || this;
+ return this._top;
}
/* The original data before any changes */
@@ -41,6 +42,16 @@ export default class BaseUISchema {
return this._origData || {};
}
+ /* The session data, can be useful but setting this will not affect UI
+ this._sessData is set by SchemaView directly. set sessData should not be allowed anywhere */
+ get sessData() {
+ return this._sessData || {};
+ }
+
+ set sessData(val) {
+ throw new Error('Property sessData is readonly.', val);
+ }
+
/* Property allows to restrict setting this later */
get defaults() {
return this._defaults || {};
@@ -102,4 +113,17 @@ export default class BaseUISchema {
validate() {
return false;
}
+
+ /* Returns the new data row for the schema based on defaults and input */
+ getNewData(data={}) {
+ let newRow = {};
+ this.fields.forEach((field)=>{
+ if(!_.isUndefined(data[field.id])){
+ newRow[field.id] = data[field.id];
+ } else {
+ newRow[field.id] = this.defaults[field.id];
+ }
+ });
+ return newRow;
+ }
}
diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx
index b66e2f9be..cb7ba49bc 100644
--- a/web/pgadmin/static/js/SchemaView/index.jsx
+++ b/web/pgadmin/static/js/SchemaView/index.jsx
@@ -34,6 +34,7 @@ import { evalFunc } from 'sources/utils';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
import { parseApiError } from '../api_instance';
+import DepListener, {DepListenerContext} from './DepListener';
const useDialogStyles = makeStyles((theme)=>({
root: {
@@ -221,8 +222,36 @@ export const SCHEMA_STATE_ACTIONS = {
ADD_ROW: 'add_row',
DELETE_ROW: 'delete_row',
RERENDER: 'rerender',
+ CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
+ DEFERRED_DEPCHANGE: 'deferred_depchange',
};
+const getDepChange = (currPath, newState, oldState, action)=>{
+ if(action.depChange) {
+ newState = action.depChange(currPath, newState, {
+ type: action.type,
+ path: action.path,
+ value: action.value,
+ oldState: _.cloneDeep(oldState),
+ depChangeResolved: action.depChangeResolved,
+ });
+ }
+ return newState;
+}
+
+const getDeferredDepChange = (currPath, newState, oldState, action)=>{
+ if(action.deferredDepChange) {
+ let deferredPromiseList = action.deferredDepChange(currPath, newState, {
+ type: action.type,
+ path: action.path,
+ value: action.value,
+ depChange: action.depChange,
+ oldState: _.cloneDeep(oldState),
+ });
+ return deferredPromiseList
+ }
+}
+
/* The main function which manipulates the session state based on actions */
/*
The state is managed based on path array of a particular key
@@ -243,6 +272,7 @@ The state starts with path []
const sessDataReducer = (state, action)=>{
let data = _.cloneDeep(state);
let rows, cid;
+ data.__deferred__ = data.__deferred__ || [];
switch(action.type) {
case SCHEMA_STATE_ACTIONS.INIT:
data = action.payload;
@@ -250,9 +280,10 @@ const sessDataReducer = (state, action)=>{
case SCHEMA_STATE_ACTIONS.SET_VALUE:
_.set(data, action.path, action.value);
/* If there is any dep listeners get the changes */
- if(action.depChange) {
- data = action.depChange(data);
- }
+ data = getDepChange(action.path, data, state, action);
+ let deferredList = getDeferredDepChange(action.path, data, state, action);
+ // let deferredInfo = getDeferredDepChange(action.path, data, state, action);
+ data.__deferred__ = deferredList || [];
break;
case SCHEMA_STATE_ACTIONS.ADD_ROW:
/* Create id to identify a row uniquely, usefull when getting diff */
@@ -260,11 +291,21 @@ const sessDataReducer = (state, action)=>{
action.value['cid'] = cid;
rows = (_.get(data, action.path)||[]).concat(action.value);
_.set(data, action.path, rows);
+ /* If there is any dep listeners get the changes */
+ data = getDepChange(action.path, data, state, action);
break;
case SCHEMA_STATE_ACTIONS.DELETE_ROW:
rows = _.get(data, action.path)||[];
rows.splice(action.value, 1);
_.set(data, action.path, rows);
+ /* If there is any dep listeners get the changes */
+ data = getDepChange(action.path, data, state, action);
+ break;
+ case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
+ data.__deferred__ = [];
+ break;
+ case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE:
+ data = getDepChange(action.path, data, state, action);
break;
}
return data;
@@ -317,12 +358,16 @@ function SchemaDialogView({
const [formReady, setFormReady] = useState(false);
const firstEleRef = useRef();
const isNew = schema.isNew(schema.origData);
+
+ const depListenerObj = useRef(new DepListener());
/* The session data */
const [sessData, sessDispatch] = useReducer(sessDataReducer, {});
useEffect(()=>{
/* if sessData changes, validate the schema */
if(!formReady) return;
+ /* Set the _sessData, can be usefull to some deep controls */
+ schema._sessData = sessData;
let isNotValid = validateSchema(schema, sessData, (name, message)=>{
if(message) {
setFormErr({
@@ -341,6 +386,25 @@ function SchemaDialogView({
props.onDataChange && props.onDataChange(dataChanged);
}, [sessData]);
+ useEffect(()=>{
+ if(sessData.__deferred__?.length > 0) {
+ sessDispatch({
+ type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE,
+ });
+
+ // let deferredDepChang = sessData.__deferred__[0];
+ let item = sessData.__deferred__[0];
+ item.promise.then((resFunc)=>{
+ sessDispatch({
+ type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE,
+ path: item.action.path,
+ depChange: item.action.depChange,
+ depChangeResolved: resFunc,
+ });
+ });
+ }
+ }, [sessData.__deferred__?.length]);
+
useEffect(()=>{
/* Docker on load focusses itself, so our focus should execute later */
let focusTimeout = setTimeout(()=>{
@@ -470,37 +534,47 @@ function SchemaDialogView({
}
};
+ const sessDispatchWithListener = (action)=>{
+ sessDispatch({
+ ...action,
+ depChange: (...args)=>depListenerObj.current.getDepChange(...args),
+ deferredDepChange: (...args)=>depListenerObj.current.getDeferredDepChange(...args),
+ });
+ };
+
/* I am Groot */
return (
-
-
-
- setSqlTabActive(sqlActive)}
- firstEleRef={firstEleRef} />
-
-
-
- {useMemo(()=>
- props.onHelp(true, isNew)} icon={}
- disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
- props.onHelp(false, isNew)} icon={} title="Help for this dialog."/>
- , [])}
-
- } className={classes.buttonMargin}>
- {gettext('Close')}
-
- } disabled={!dirty || saving} className={classes.buttonMargin}>
- {gettext('Reset')}
-
- } disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
- {gettext('Save')}
-
+
+
+
+
+ setSqlTabActive(sqlActive)}
+ firstEleRef={firstEleRef} />
+
+
+
+ {useMemo(()=>
+ props.onHelp(true, isNew)} icon={}
+ disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
+ props.onHelp(false, isNew)} icon={} title="Help for this dialog."/>
+ , [])}
+
+ } className={classes.buttonMargin}>
+ {gettext('Close')}
+
+ } disabled={!dirty || saving} className={classes.buttonMargin}>
+ {gettext('Reset')}
+
+ } disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
+ {gettext('Save')}
+
+
-
+
);
}
diff --git a/web/pgadmin/static/js/components/TabPanel.jsx b/web/pgadmin/static/js/components/TabPanel.jsx
index ce46ba707..1e476d6dd 100644
--- a/web/pgadmin/static/js/components/TabPanel.jsx
+++ b/web/pgadmin/static/js/components/TabPanel.jsx
@@ -19,6 +19,9 @@ const useStyles = makeStyles((theme)=>({
padding: theme.spacing(1),
overflow: 'auto',
backgroundColor: theme.palette.grey[400]
+ },
+ content: {
+ height: '100%',
}
}));
@@ -28,7 +31,7 @@ export default function TabPanel({children, classNameRoot, className, value, ind
const active = value === index;
return (
- {children}
+ {children}
);
}
diff --git a/web/pgadmin/static/js/validators.js b/web/pgadmin/static/js/validators.js
index a3343480a..fee3e7358 100644
--- a/web/pgadmin/static/js/validators.js
+++ b/web/pgadmin/static/js/validators.js
@@ -47,14 +47,14 @@ export function integerValidator(label, value) {
/* Validate value to check if it is empty */
export function emptyValidator(label, value) {
- if(isEmptyString(value) || String(value).replace(/^\s+|\s+$/g, '') == '') {
+ if(isEmptyString(value)) {
return sprintf(pgAdmin.Browser.messages.CANNOT_BE_EMPTY, label);
}
return null;
}
-export function isEmptyString(string) {
- return _.isUndefined(string) || _.isNull(string) || String(string).trim() === '';
+export function isEmptyString(value) {
+ return _.isUndefined(value) || _.isNull(value) || String(value).trim() === '' || String(value).replace(/^\s+|\s+$/g, '') == '';
}
/* Validate rows to check for any duplicate rows based on uniqueCols-columns array */