From 377fe8004670166d15db9f3365c986dcca18f407 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Thu, 29 Jul 2021 13:18:04 +0530 Subject: [PATCH] - Add VaccumSettings schema. - Allow collection to have fixed rows. - Changes in data change comparison and add state utils context. - Fixed jasmine test cases. --- .../servers/static/js/vacuum.ui.js | 179 ++++++++++++++++++ web/pgadmin/browser/static/js/node_ajax.js | 17 +- .../static/js/SchemaView/DataGridView.jsx | 32 +++- web/pgadmin/static/js/SchemaView/FormView.jsx | 5 + .../static/js/SchemaView/MappedControl.jsx | 6 +- web/pgadmin/static/js/SchemaView/index.jsx | 179 ++++++++++++------ .../javascript/SchemaView/SchemaView.spec.js | 2 - .../schema_ui_files/language.ui.spec.js | 1 + 8 files changed, 342 insertions(+), 79 deletions(-) create mode 100644 web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js diff --git a/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js new file mode 100644 index 000000000..93f27b994 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js @@ -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'], + }]; + } +} diff --git a/web/pgadmin/browser/static/js/node_ajax.js b/web/pgadmin/browser/static/js/node_ajax.js index 7594a0813..7b5b9fe0a 100644 --- a/web/pgadmin/browser/static/js/node_ajax.js +++ b/web/pgadmin/browser/static/js/node_ajax.js @@ -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. diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx index 74a45ea8d..b26df43d8 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, 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 ( - + {(props.label || props.canAdd) && {props.label} {props.canAdd && } className={classes.gridControlsButton} />} - + }
@@ -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, diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index a0ecc392a..2b6e174b6 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -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( ; default: - return <>; + return {value}; } } @@ -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 ; default: - return <>; + return {value}; } } diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 6c3dcc952..3402d43e5 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -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 ( - - - - - - - - - {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')} - + + + + + + + + + + {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/regression/javascript/SchemaView/SchemaView.spec.js b/web/regression/javascript/SchemaView/SchemaView.spec.js index 176b7ebba..a30d84867 100644 --- a/web/regression/javascript/SchemaView/SchemaView.spec.js +++ b/web/regression/javascript/SchemaView/SchemaView.spec.js @@ -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); diff --git a/web/regression/javascript/schema_ui_files/language.ui.spec.js b/web/regression/javascript/schema_ui_files/language.ui.spec.js index 163c07ede..bc7989129 100644 --- a/web/regression/javascript/schema_ui_files/language.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/language.ui.spec.js @@ -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.');