- 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,15 +89,16 @@ 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)=>{
reject(err);
});
}
otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data);
resolve(transform(data));
}).catch((err)=>{
reject(err);
});
} else {
// To fetch only options from cache, we do not need time from 'at'
// attribute but only options.

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') {
let change = cleanCid(_.get(sessData, currPath));
attrChanged(currPath, change);
/* 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,27 +361,28 @@ 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) {
if (_.isObject(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.
So to decide whether row is new or not set, the cid starts with
nn (not new) for existing rows. Newly added will start with 'c' (created)
*/
el['cid'] = _.uniqueId('nn');
prepareData(el);
}
});
}
if (_.isObject(val)) {
prepareData(val);
}
});
return origData;
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.
So to decide whether row is new or not set, the cid starts with
nn (not new) for existing rows. Newly added will start with 'c' (created)
*/
el['cid'] = _.uniqueId('nn');
prepareData(el);
}
});
}
return val;
}
/* If its the dialog */
@ -560,38 +602,55 @@ 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 (
<DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.root}>
<Box className={classes.form}>
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
onClose={onErrClose} />
</Box>
<Box className={classes.footer}>
{useMemo(()=><Box>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
</Box>, [])}
<Box marginLeft="auto">
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
{gettext('Close')}
</DefaultButton>
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
{gettext('Reset')}
</DefaultButton>
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
{gettext('Save')}
</PrimaryButton>
<StateUtilsContext.Provider value={stateUtils}>
<DepListenerContext.Provider value={depListenerObj.current}>
<Box className={classes.root}>
<Box className={classes.form}>
<Loader message={loaderText}/>
<FormView value={sessData} viewHelperProps={viewHelperProps} formErr={formErr}
schema={schema} accessPath={[]} dataDispatch={sessDispatchWithListener}
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} />
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={formErr.message}
onClose={onErrClose} />
</Box>
<Box className={classes.footer}>
{useMemo(()=><Box>
<PgIconButton data-test="sql-help" onClick={()=>props.onHelp(true, isNew)} icon={<InfoIcon />}
disabled={props.disableSqlHelp} className={classes.buttonMargin} title="SQL help for this object type."/>
<PgIconButton data-test="dialog-help" onClick={()=>props.onHelp(false, isNew)} icon={<HelpIcon />} title="Help for this dialog."/>
</Box>, [])}
<Box marginLeft="auto">
<DefaultButton data-test="Close" onClick={props.onClose} startIcon={<CloseIcon />} className={classes.buttonMargin}>
{gettext('Close')}
</DefaultButton>
<DefaultButton data-test="Reset" onClick={onResetClick} startIcon={<SettingsBackupRestoreIcon />} disabled={!dirty || saving} className={classes.buttonMargin}>
{gettext('Reset')}
</DefaultButton>
<PrimaryButton data-test="Save" onClick={onSaveClick} startIcon={<SaveIcon />} disabled={!dirty || saving || Boolean(formErr.name) || !formReady}>
{gettext('Save')}
</PrimaryButton>
</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');
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.');