- Add VaccumSettings schema. - Allow collection to have fixed rows. - Changes in data change comparison and add state utils context. - Fixed jasmine test cases.
parent
eb48765a5a
commit
377fe80046
|
@ -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'],
|
||||
}];
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.');
|
||||
|
||||
|
|
Loading…
Reference in New Issue