- 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)) {
api.get(fullUrl, {
params: otherParams.urlParams,
})
.then((res)=>{
}).then((res)=>{
data = res.data;
if(res.data.data) {
data = res.data.data;
}
otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data);
resolve(transform(data));
})
.catch((err)=>{
}).catch((err)=>{
reject(err);
});
} else {

View File

@ -9,7 +9,7 @@
/* 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 { makeStyles } from '@material-ui/core/styles';
import { PgIconButton } from '../components/Buttons';
@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
import _ from 'lodash';
import gettext from 'sources/gettext';
import { SCHEMA_STATE_ACTIONS } from '.';
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
import FormView from './FormView';
import { confirmDeleteRow } from '../helpers/legacyConnector';
import CustomPropTypes from 'sources/custom_prop_types';
@ -78,7 +78,6 @@ const useStyles = makeStyles((theme)=>({
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
position: 'relative',
textAlign: 'center'
},
tableCellHeader: {
fontWeight: theme.typography.fontWeightBold,
@ -87,6 +86,7 @@ const useStyles = makeStyles((theme)=>({
},
btnCell: {
padding: theme.spacing(0.5, 0),
textAlign: 'center',
},
resizer: {
display: 'inline-block',
@ -202,8 +202,10 @@ function DataTableRow({row, totalRows, isResizing, schema, schemaRef, accessPath
}
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 stateUtils = useContext(StateUtilsContext);
/* Using ref so that schema variable is not frozen in columns closure */
const schemaRef = useRef(schema);
@ -386,6 +388,23 @@ export default function DataGridView({
...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);
if(!props.visible) {
@ -395,12 +414,12 @@ export default function DataGridView({
return (
<Box className={containerClassName}>
<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.gridControls}>
{props.canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={onAddClick} icon={<AddIcon />} className={classes.gridControlsButton} />}
</Box>
</Box>
</Box>}
<div {...getTableProps()} className={classes.table}>
<DataTableHeader headerGroups={headerGroups} />
<div {...getTableBodyProps()}>
@ -432,6 +451,7 @@ DataGridView.propTypes = {
accessPath: PropTypes.array.isRequired,
dataDispatch: PropTypes.func.isRequired,
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
fixedRows: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
columns: PropTypes.array,
canEdit: PropTypes.bool,
canAdd: PropTypes.bool,

View File

@ -217,6 +217,11 @@ export default function FormView({
depsMap.push(canAdd, canEdit, canDelete, visible);
if(!_.isUndefined(field.fixedRows)) {
canAdd = false;
canDelete = false;
}
tabs[group].push(
<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}

View File

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

View File

@ -65,11 +65,13 @@ const useDialogStyles = makeStyles((theme)=>({
},
}));
export const StateUtilsContext = React.createContext();
function getForQueryParams(data) {
let retData = {...data};
Object.keys(retData).forEach((key)=>{
let value = retData[key];
if(_.isArray(value) || _.isObject(value)) {
if(_.isObject(value)) {
retData[key] = JSON.stringify(value);
}
});
@ -79,6 +81,38 @@ function getForQueryParams(data) {
/* Compare the sessData with schema.origData
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) {
let changedData = {};
let isEdit = mode === 'edit';
@ -87,15 +121,8 @@ function getChangedData(topSchema, mode, sessData, stringify=false) {
const attrChanged = (currPath, change, force=false)=>{
let origVal = _.get(topSchema.origData, 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 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) {
if(isValueEqual(origVal, sessVal) && !force) {
return;
} else {
change = change || _.get(sessData, currPath);
@ -146,8 +173,22 @@ function getChangedData(topSchema, mode, sessData, stringify=false) {
}
} else if(!isEdit) {
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));
attrChanged(currPath, change);
}
} else {
attrChanged(currPath);
}
@ -320,11 +361,16 @@ function cleanCid(coll) {
return coll.map((o)=>_.pickBy(o, (v, k)=>k!='cid'));
}
function prepareData(origData) {
_.forIn(origData, function (val) {
if (_.isArray(val)) {
val.forEach(function(el) {
function prepareData(val) {
if(_.isPlainObject(val)) {
_.forIn(val, function (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
This helps in easily getting what has changed */
/* Nested collection rows may or may not have idAttribute.
@ -336,11 +382,7 @@ function prepareData(origData) {
}
});
}
if (_.isObject(val)) {
prepareData(val);
}
});
return origData;
return val;
}
/* 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 */
return (
<StateUtilsContext.Provider value={stateUtils}>
<DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.root}>
<Box className={classes.form}>
@ -592,6 +650,7 @@ function SchemaDialogView({
</Box>
</Box>
</DepListenerContext.Provider>
</StateUtilsContext.Provider>
);
}

View File

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

View File

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