- Add VaccumSettings schema. - Allow collection to have fixed rows. - Changes in data change comparison and add state utils context. - Fixed jasmine test cases.

pull/58/head
Aditya Toshniwal 2021-07-29 13:18:04 +05:30 committed by Akshay Joshi
parent eb48765a5a
commit 377fe80046
8 changed files with 342 additions and 79 deletions

View File

@ -0,0 +1,179 @@
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import { getNodeAjaxOptions } from '../../../../static/js/node_ajax';
export function getNodeVacuumSettingsSchema(nodeObj, treeNodeInfo, itemNodeData) {
let tableVacuumRows = ()=>getNodeAjaxOptions('get_table_vacuum', nodeObj, treeNodeInfo, itemNodeData, {noCache: true});
let toastTableVacuumRows = ()=>getNodeAjaxOptions('get_toast_table_vacuum', nodeObj, treeNodeInfo, itemNodeData, {noCache: true});
return new VacuumSettingsSchema(tableVacuumRows, toastTableVacuumRows, treeNodeInfo);
}
export class VacuumTableSchema extends BaseUISchema {
constructor(valueDep) {
super();
this.valueDep = valueDep;
}
get baseFields() {
let obj = this;
return [
{
id: 'label', name: 'label', label: gettext('Label'),
},
{
id: 'value', name: 'value', label: gettext('Value'),
type: 'text', deps: [[this.valueDep]],
editable: function() {
return obj.top.sessData[this.valueDep];
},
cell: (state)=>{
switch(state.column_type) {
case 'integer':
return {cell: 'int'};
case 'number':
return {cell: 'numeric', controlProps: {decimals: 5}};
case 'string':
return {cell: 'text'};
default:
return {cell: ''};
}
}
},
{
id: 'setting', name: 'setting', label: gettext('Default'),
},
];
}
}
export default class VacuumSettingsSchema extends BaseUISchema {
constructor(tableVars, toastTableVars, nodeInfo) {
super({
vacuum_table: [],
vacuum_toast: [],
});
this.tableVars = tableVars;
this.toastTableVars = toastTableVars;
this.nodeInfo = nodeInfo;
this.vacuumTableObj = new VacuumTableSchema('autovacuum_custom');
this.vacuumToastTableObj = new VacuumTableSchema('toast_autovacuum');
}
inSchemaCheck() {
if(this.nodeInfo && 'catalog' in this.nodeInfo)
{
return true;
}
return false;
}
get baseFields() {
var obj = this;
return [{
id: 'autovacuum_custom', label: gettext('Custom auto-vacuum?'),
group: gettext('Table'), mode: ['edit', 'create'],
type: 'switch', disabled: function(state) {
if(state.is_partitioned) {
return true;
}
// If table is partitioned table then disabled it.
if(state.top && state.is_partitioned) {
// We also need to unset rest of all
state.autovacuum_custom = false;
return true;
}
if(obj.inSchemaCheck)
{
return false;
}
return true;
},
depChange(state) {
if(state.is_partitioned) {
return {autovacuum_custom: false};
}
}
},
{
id: 'autovacuum_enabled', label: gettext('Autovacuum Enabled?'),
group: gettext('Table'), mode: ['edit', 'create'], type: 'toggle',
options: [
{'label': gettext('Not set'), 'value': 'x'},
{'label': gettext('Yes'), 'value': 't'},
{'label': gettext('No'), 'value': 'f'},
],
deps: ['autovacuum_custom'],
disabled: function(state) {
if(obj.inSchemaCheck && state.autovacuum_custom) {
return false;
}
return true;
},
depChange: function(state) {
if(obj.inSchemaCheck && state.autovacuum_custom) {
return;
}
return {autovacuum_enabled: 'x'};
},
},
{
id: 'vacuum_table', label: '', editable: false, type: 'collection',
canEdit: false, canAdd: false, canDelete: false, group: gettext('Table'),
fixedRows: this.tableVars,
schema: this.vacuumTableObj,
mode: ['edit', 'create'],
},
{
id: 'toast_autovacuum', label: gettext('Custom auto-vacuum?'),
group: gettext('TOAST table'), mode: ['edit', 'create'],
type: 'switch',
disabled: function(state) {
// We need to check additional condition to toggle enable/disable
// for table auto-vacuum
if(obj.inSchemaCheck && (obj.isNew() || (state.toast_autovacuum_enabled || state.hastoasttable))) {
return false;
}
return true;
}
},
{
id: 'toast_autovacuum_enabled', label: gettext('Autovacuum Enabled?'),
group: gettext('TOAST table'), mode: ['edit', 'create'],
type: 'toggle',
options: [
{'label': gettext('Not set'), 'value': 'x'},
{'label': gettext('Yes'), 'value': 't'},
{'label': gettext('No'), 'value': 'f'},
],
deps:['toast_autovacuum'],
disabled: function(state) {
if(obj.inSchemaCheck && state.toast_autovacuum) {
return false;
}
return true;
},
depChange: function(state) {
if(obj.inSchemaCheck && state.toast_autovacuum) {
return;
}
if(obj.isNew() || state.hastoasttable) {
return {toast_autovacuum_enabled: 'x'};
}
},
},
{
id: 'vacuum_toast', label: '',
type: 'collection',
fixedRows: this.toastTableVars,
editable: function(state) {
return state.isNew();
},
canEdit: false, canAdd: false, canDelete: false, group: gettext('TOAST table'),
schema: this.vacuumToastTableObj,
mode: ['properties', 'edit', 'create'], deps: ['toast_autovacuum'],
}];
}
}

View File

@ -89,13 +89,14 @@ export function getNodeAjaxOptions(url, nodeObj, treeNodeInfo, itemNodeData, par
if (_.isUndefined(data) || _.isNull(data)) { if (_.isUndefined(data) || _.isNull(data)) {
api.get(fullUrl, { api.get(fullUrl, {
params: otherParams.urlParams, params: otherParams.urlParams,
}) }).then((res)=>{
.then((res)=>{ data = res.data;
if(res.data.data) {
data = res.data.data; data = res.data.data;
}
otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data); otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data);
resolve(transform(data)); resolve(transform(data));
}) }).catch((err)=>{
.catch((err)=>{
reject(err); reject(err);
}); });
} else { } else {

View File

@ -9,7 +9,7 @@
/* The DataGridView component is based on react-table component */ /* The DataGridView component is based on react-table component */
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box } from '@material-ui/core'; import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons'; import { PgIconButton } from '../components/Buttons';
@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
import _ from 'lodash'; import _ from 'lodash';
import gettext from 'sources/gettext'; import gettext from 'sources/gettext';
import { SCHEMA_STATE_ACTIONS } from '.'; import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
import FormView from './FormView'; import FormView from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector'; import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types'; import CustomPropTypes from 'sources/custom_prop_types';
@ -78,7 +78,6 @@ const useStyles = makeStyles((theme)=>({
...theme.mixins.panelBorder.bottom, ...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right, ...theme.mixins.panelBorder.right,
position: 'relative', position: 'relative',
textAlign: 'center'
}, },
tableCellHeader: { tableCellHeader: {
fontWeight: theme.typography.fontWeightBold, fontWeight: theme.typography.fontWeightBold,
@ -87,6 +86,7 @@ const useStyles = makeStyles((theme)=>({
}, },
btnCell: { btnCell: {
padding: theme.spacing(0.5, 0), padding: theme.spacing(0.5, 0),
textAlign: 'center',
}, },
resizer: { resizer: {
display: 'inline-block', display: 'inline-block',
@ -202,8 +202,10 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
} }
export default function DataGridView({ export default function DataGridView({
value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName, ...props}) { value, viewHelperProps, formErr, schema, accessPath, dataDispatch, containerClassName,
fixedRows, ...props}) {
const classes = useStyles(); const classes = useStyles();
const stateUtils = useContext(StateUtilsContext);
/* Using ref so that schema variable is not frozen in columns closure */ /* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema); const schemaRef = useRef(schema);
@ -386,6 +388,23 @@ export default function DataGridView({
...tablePlugins, ...tablePlugins,
); );
useEffect(()=>{
let rowsPromise = fixedRows, umounted=false;
if(typeof rowsPromise === 'function') {
rowsPromise = rowsPromise();
}
if(rowsPromise) {
Promise.resolve(rowsPromise)
.then((res)=>{
/* If component unmounted, dont update state */
if(!umounted) {
stateUtils.initOrigData(accessPath, res);
}
});
}
return ()=>umounted=true;
}, []);
const isResizing = _.flatMap(headerGroups, headerGroup => headerGroup.headers.map(col=>col.isResizing)).includes(true); const isResizing = _.flatMap(headerGroups, headerGroup => headerGroup.headers.map(col=>col.isResizing)).includes(true);
if(!props.visible) { if(!props.visible) {
@ -395,12 +414,12 @@ export default function DataGridView({
return ( return (
<Box className={containerClassName}> <Box className={containerClassName}>
<Box className={classes.grid}> <Box className={classes.grid}>
<Box className={classes.gridHeader}> {(props.label || props.canAdd) && <Box className={classes.gridHeader}>
<Box className={classes.gridHeaderText}>{props.label}</Box> <Box className={classes.gridHeaderText}>{props.label}</Box>
<Box className={classes.gridControls}> <Box className={classes.gridControls}>
{props.canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />} {props.canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
</Box> </Box>
</Box> </Box>}
<div {...getTableProps()} className={classes.table}> <div {...getTableProps()} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} /> <DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()}> <div {...getTableBodyProps()}>
@ -432,6 +451,7 @@ DataGridView.propTypes = {
accessPath: PropTypes.array.isRequired, accessPath: PropTypes.array.isRequired,
dataDispatch: PropTypes.func.isRequired, dataDispatch: PropTypes.func.isRequired,
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
fixedRows: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
columns: PropTypes.array, columns: PropTypes.array,
canEdit: PropTypes.bool, canEdit: PropTypes.bool,
canAdd: PropTypes.bool, canAdd: PropTypes.bool,

View File

@ -217,6 +217,11 @@ export default function FormView({
depsMap.push(canAdd, canEdit, canDelete, visible); depsMap.push(canAdd, canEdit, canDelete, visible);
if(!_.isUndefined(field.fixedRows)) {
canAdd = false;
canDelete = false;
}
tabs[group].push( tabs[group].push(
<DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr} <DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow} schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}

View File

@ -92,7 +92,7 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
case 'sql': case 'sql':
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />; return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />;
default: default:
return <></>; return <span>{value}</span>;
} }
} }
@ -120,7 +120,7 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
value = e.target.value; value = e.target.value;
} }
onCellChange(value); onCellChange && onCellChange(value);
}, []); }, []);
const onIntChange = useCallback((e) => { const onIntChange = useCallback((e) => {
@ -179,7 +179,7 @@ function MappedCellControlBase({cell, value, id, optionsLoaded, onCellChange, vi
case 'privilege': case 'privilege':
return <Privilege name={name} value={value} onChange={onTextChange} {...props}/>; return <Privilege name={name} value={value} onChange={onTextChange} {...props}/>;
default: default:
return <></>; return <span>{value}</span>;
} }
} }

View File

@ -65,11 +65,13 @@ const useDialogStyles = makeStyles((theme)=>({
}, },
})); }));
export const StateUtilsContext = React.createContext();
function getForQueryParams(data) { function getForQueryParams(data) {
let retData = {...data}; let retData = {...data};
Object.keys(retData).forEach((key)=>{ Object.keys(retData).forEach((key)=>{
let value = retData[key]; let value = retData[key];
if(_.isArray(value) || _.isObject(value)) { if(_.isObject(value)) {
retData[key] = JSON.stringify(value); retData[key] = JSON.stringify(value);
} }
}); });
@ -79,6 +81,38 @@ function getForQueryParams(data) {
/* Compare the sessData with schema.origData /* Compare the sessData with schema.origData
schema.origData is set to incoming or default data schema.origData is set to incoming or default data
*/ */
function isValueEqual(val1, val2) {
let attrDefined = !_.isUndefined(val1) && !_.isUndefined(val2) && !_.isNull(val1) && !_.isNull(val2);
/* If the orig value was null and new one is empty string, then its a "no change" */
/* If the orig value and new value are of different datatype but of same value(numeric) "no change" */
/* If the orig value is undefined or null and new value is boolean false "no change" */
if ((_.isEqual(val1, val2)
|| ((val1 === null || _.isUndefined(val1)) && !val2)
|| (attrDefined ? _.isEqual(val1.toString(), val2.toString()) : false
))) {
return true;
} else {
return false;
}
}
function objectComparator(obj1, obj2) {
for(const key of _.union(Object.keys(obj1), Object.keys(obj2))) {
let equal = isValueEqual(obj1[key], obj2[key]);
if(equal) {
continue;
} else {
return false;
}
}
return true;
}
const diffArrayOptions = {
compareFunction: objectComparator,
};
function getChangedData(topSchema, mode, sessData, stringify=false) { function getChangedData(topSchema, mode, sessData, stringify=false) {
let changedData = {}; let changedData = {};
let isEdit = mode === 'edit'; let isEdit = mode === 'edit';
@ -87,15 +121,8 @@ function getChangedData(topSchema, mode, sessData, stringify=false) {
const attrChanged = (currPath, change, force=false)=>{ const attrChanged = (currPath, change, force=false)=>{
let origVal = _.get(topSchema.origData, currPath); let origVal = _.get(topSchema.origData, currPath);
let sessVal = _.get(sessData, currPath); let sessVal = _.get(sessData, currPath);
let attrDefined = !_.isUndefined(origVal) && !_.isUndefined(sessVal) && !_.isNull(origVal) && !_.isNull(sessVal);
/* If the orig value was null and new one is empty string, then its a "no change" */ if(isValueEqual(origVal, sessVal) && !force) {
/* If the orig value and new value are of different datatype but of same value(numeric) "no change" */
/* If the orig value is undefined or null and new value is boolean false "no change" */
if ((_.isEqual(origVal, sessVal)
|| ((origVal === null || _.isUndefined(origVal)) && !sessVal)
|| (attrDefined ? _.isEqual(origVal.toString(), sessVal.toString()) : false))
&& !force) {
return; return;
} else { } else {
change = change || _.get(sessData, currPath); change = change || _.get(sessData, currPath);
@ -146,8 +173,22 @@ function getChangedData(topSchema, mode, sessData, stringify=false) {
} }
} else if(!isEdit) { } else if(!isEdit) {
if(field.type === 'collection') { if(field.type === 'collection') {
/* For fixed rows, check the updated changes */
if(!_.isUndefined(field.fixedRows)) {
const changeDiff = diffArray(
_.get(topSchema.origData, currPath) || [],
_.get(sessData, currPath) || [],
'cid',
diffArrayOptions
);
if(changeDiff.updated.length > 0) {
let change = cleanCid(_.get(sessData, currPath));
attrChanged(currPath, change, true);
}
} else {
let change = cleanCid(_.get(sessData, currPath)); let change = cleanCid(_.get(sessData, currPath));
attrChanged(currPath, change); attrChanged(currPath, change);
}
} else { } else {
attrChanged(currPath); attrChanged(currPath);
} }
@ -320,11 +361,16 @@ function cleanCid(coll) {
return coll.map((o)=>_.pickBy(o, (v, k)=>k!='cid')); return coll.map((o)=>_.pickBy(o, (v, k)=>k!='cid'));
} }
function prepareData(origData) { function prepareData(val) {
_.forIn(origData, function (val) { if(_.isPlainObject(val)) {
if (_.isArray(val)) { _.forIn(val, function (el) {
val.forEach(function(el) {
if (_.isObject(el)) { if (_.isObject(el)) {
prepareData(el);
}
});
} else if(_.isArray(val)) {
val.forEach(function(el) {
if (_.isPlainObject(el)) {
/* The each row in collection need to have an id to identify them uniquely /* The each row in collection need to have an id to identify them uniquely
This helps in easily getting what has changed */ This helps in easily getting what has changed */
/* Nested collection rows may or may not have idAttribute. /* Nested collection rows may or may not have idAttribute.
@ -336,11 +382,7 @@ function prepareData(origData) {
} }
}); });
} }
if (_.isObject(val)) { return val;
prepareData(val);
}
});
return origData;
} }
/* If its the dialog */ /* If its the dialog */
@ -560,8 +602,24 @@ function SchemaDialogView({
}); });
}; };
const stateUtils = useMemo(()=>({
dataDispatch: sessDispatchWithListener,
initOrigData: (path, value)=>{
if(path) {
let data = prepareData(value);
_.set(schema.origData, path, data);
sessDispatchWithListener({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: path,
value: data,
});
}
}
}), []);
/* I am Groot */ /* I am Groot */
return ( return (
<StateUtilsContext.Provider value={stateUtils}>
<DepListenerContext.Provider value={depListenerObj.current}> <DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.root}> <Box className={classes.root}>
<Box className={classes.form}> <Box className={classes.form}>
@ -592,6 +650,7 @@ function SchemaDialogView({
</Box> </Box>
</Box> </Box>
</DepListenerContext.Provider> </DepListenerContext.Provider>
</StateUtilsContext.Provider>
); );
} }

View File

@ -204,8 +204,6 @@ describe('SchemaView', ()=>{
ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click'); ctrl.find('ForwardRef(Tab)[label="SQL"]').find('button').simulate('click');
setTimeout(()=>{ setTimeout(()=>{
ctrl.update(); ctrl.update();
/* Dont show error message */
expect(ctrl.find('FormFooterMessage').prop('message')).toBe('');
expect(ctrl.find('CodeMirror').prop('value')).toBe('-- No updates.'); expect(ctrl.find('CodeMirror').prop('value')).toBe('-- No updates.');
done(); done();
}, 0); }, 0);

View File

@ -115,6 +115,7 @@ describe('LanguageSchema', ()=>{
let setError = jasmine.createSpy('setError'); let setError = jasmine.createSpy('setError');
state.lanproc = ''; state.lanproc = '';
state.isTemplate = true;
schemaObj.validate(state, setError); schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('lanproc', 'Handler function cannot be empty.'); expect(setError).toHaveBeenCalledWith('lanproc', 'Handler function cannot be empty.');