Improved the extendability of the SchemaView and DataGridView. (#7876)

Restructured these modules for ease of maintenance and apply the single
responsibility principle (wherever applicable).

* SchemaView

 - Split the code based on the functionality and responsibility.
 - Introduced a new View 'InlineView' instead of using the
   'nextInline' configuration of the fields to have a better, and
   manageable view.
 - Using the separate class 'SchemaState' for managing the data and
   states of the SchemaView (separated from the 'useSchemaState'
   custom hook).
 - Introduced three new custom hooks 'useFieldValue',
   'useFieldOptions', 'useFieldError' for the individual control to
   use for each Schema Field.
 - Don't pass value as the parameter props, and let the
   'useFieldValue' and other custom hooks to decide, whether to
   rerender the control itself or the whole dialog/view. (single
   responsibility principle)
 - Introduced a new data store with a subscription facility.
 - Moving the field metadata (option) evaluation to a separate place
   for better management, and each option can be defined for a
   particular kind of field (for example - collection, row, cell,
   general, etc).
 - Allow to provide custom control for all kind of Schema field.

* DataGridView

 - Same as SchemaView, split the DataGridView call into smaller,
   manageable chunks. (For example - grid, row, mappedCell, etc).
 - Use context based approach for providing the row and table data
   instead of passing them as parameters to every component
   separately.
 - Have a facility to extend this feature separately in future.
   (for example - selectable cell, column grouping, etc.)
 - Separated the features like deletable, editable, reorder,
   expandable etc. cells using the above feature support.
 - Added ability to provide the CustomHeader, and CustomRow through the
   Schema field, which will extend the ability to customize better.
 - Removed the 'DataGridViewWithHeaderForm' as it has been achieved
   through providing 'CustomHeader', and also introduced
   'DataGridFormHeader' (a custom header) to achieve the same feature
   as 'DataGridViewWithHeaderForm'.
This commit is contained in:
Ashesh Vashi
2024-09-09 14:27:31 +05:30
committed by GitHub
parent e5012ea9c6
commit e9af0c3226
154 changed files with 5429 additions and 3058 deletions

View File

@@ -39,7 +39,9 @@ export class DomainConstSchema extends BaseUISchema {
id: 'convalidated', label: gettext('Validate?'), cell: 'checkbox',
type: 'checkbox',
readonly: function(state) {
let currCon = _.find(obj.top.origData.constraints, (con)=>con.conoid == state.conoid);
let currCon = _.find(
obj.top.origData.constraints, (con) => con.conoid == state.conoid
);
return !obj.isNew(state) && currCon.convalidated;
},
}

View File

@@ -9,7 +9,7 @@
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DataGridViewWithHeaderForm from 'sources/helpers/DataGridViewWithHeaderForm';
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
import { isEmptyString } from '../../../../../../../../static/js/validators';
class TokenHeaderSchema extends BaseUISchema {
@@ -155,8 +155,8 @@ export default class FTSConfigurationSchema extends BaseUISchema {
group: gettext('Tokens'), mode: ['create','edit'],
editable: false, schema: this.tokColumnSchema,
headerSchema: this.tokHeaderSchema,
headerVisible: function() { return true;},
CustomControl: DataGridViewWithHeaderForm,
headerFormVisible: true,
GridHeader: DataGridFormHeader,
uniqueCol : ['token'],
canAdd: true, canEdit: false, canDelete: true,
}

View File

@@ -10,8 +10,8 @@ import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import _ from 'lodash';
import { isEmptyString } from 'sources/validators';
import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView';
import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView';
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax';
import TableSchema from '../../../../static/js/table.ui';
import pgAdmin from 'sources/pgadmin';
@@ -342,10 +342,12 @@ export default class ExclusionConstraintSchema extends BaseUISchema {
group: gettext('Columns'), type: 'collection',
mode: ['create', 'edit', 'properties'],
editable: false, schema: this.exColumnSchema,
headerSchema: this.exHeaderSchema, headerVisible: (state)=>obj.isNew(state),
CustomControl: DataGridViewWithHeaderForm,
headerSchema: this.exHeaderSchema,
headerFormVisible: (state)=>obj.isNew(state),
GridHeader: DataGridFormHeader,
uniqueCol: ['column'],
canAdd: false, canDelete: function(state) {
canAdd: (state)=>obj.isNew(state),
canDelete: function(state) {
// We can't update columns of existing
return obj.isNew(state);
},

View File

@@ -11,11 +11,12 @@ import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import _ from 'lodash';
import { isEmptyString } from 'sources/validators';
import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView';
import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView';
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax';
import TableSchema from '../../../../static/js/table.ui';
export function getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, noColumns=false, initData={}) {
return new ForeignKeySchema({
local_column: noColumns ? [] : ()=>getNodeListByName('column', treeNodeInfo, itemNodeData),
@@ -58,12 +59,20 @@ class ForeignKeyHeaderSchema extends BaseUISchema {
}
addDisabled(state) {
return !(state.local_column && (state.references || this.origData.references) && state.referenced);
return !(
state.local_column && (
state.references || this.origData.references
) && state.referenced
);
}
/* Data to ForeignKeyColumnSchema will added using the header form */
getNewData(data) {
let references_table_name = _.find(this.refTables, (t)=>t.value==data.references || t.value == this.origData.references)?.label;
let references_table_name = _.find(
this.refTables,
(t) => t.value == data.references || t.value == this.origData.references
)?.label;
return {
local_column: data.local_column,
referenced: data.referenced,
@@ -229,7 +238,10 @@ export default class ForeignKeySchema extends BaseUISchema {
if(!obj.isNew(state)) {
let origData = {};
if(obj.inTable && obj.top) {
origData = _.find(obj.top.origData['foreign_key'], (r)=>r.cid == state.cid);
origData = _.find(
obj.top.origData['foreign_key'],
(r) => r.cid == state.cid
);
} else {
origData = obj.origData;
}
@@ -304,14 +316,14 @@ export default class ForeignKeySchema extends BaseUISchema {
group: gettext('Columns'), type: 'collection',
mode: ['create', 'edit', 'properties'],
editable: false, schema: this.fkColumnSchema,
headerSchema: this.fkHeaderSchema, headerVisible: (state)=>obj.isNew(state),
CustomControl: DataGridViewWithHeaderForm,
headerSchema: this.fkHeaderSchema,
headerFormVisible: (state)=>obj.isNew(state),
GridHeader: DataGridFormHeader,
uniqueCol: ['local_column', 'references', 'referenced'],
canAdd: false, canDelete: function(state) {
// We can't update columns of existing foreign key.
return obj.isNew(state);
},
readonly: obj.isReadonly, cell: ()=>({
canAdd: (state)=>obj.isNew(state),
canDelete: (state)=>obj.isNew(state),
readonly: obj.isReadonly,
cell: () => ({
cell: '',
controlProps: {
formatter: {
@@ -358,9 +370,10 @@ export default class ForeignKeySchema extends BaseUISchema {
}
}
if(actionObj.type == SCHEMA_STATE_ACTIONS.ADD_ROW) {
obj.fkHeaderSchema.origData.references = null;
// Set references value.
obj.fkHeaderSchema.origData.references = obj.fkHeaderSchema.sessData.references;
obj.fkHeaderSchema.origData.references = null;
obj.fkHeaderSchema.origData.references =
obj.fkHeaderSchema.sessData.references;
obj.fkHeaderSchema.origData._disable_references = true;
}
return {columns: currColumns};

View File

@@ -7,12 +7,13 @@
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import DataGridViewWithHeaderForm from '../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
import _ from 'lodash';
import { isEmptyString } from 'sources/validators';
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import pgAdmin from 'sources/pgadmin';
import { isEmptyString } from 'sources/validators';
function inSchema(node_info) {
@@ -23,8 +24,8 @@ class IndexColHeaderSchema extends BaseUISchema {
constructor(columns) {
super({
is_exp: true,
colname: undefined,
expression: undefined,
colname: '',
expression: '',
});
this.columns = columns;
@@ -90,10 +91,10 @@ class IndexColumnSchema extends BaseUISchema {
}
isEditable(state) {
let topObj = this._top;
let topObj = this.top;
if(this.inSchemaWithModelCheck(state)) {
return false;
} else if (topObj._sessData && topObj._sessData.amname === 'btree') {
} else if (topObj.sessData && topObj.sessData.amname === 'btree') {
state.is_sort_nulls_applicable = true;
return true;
} else {
@@ -155,9 +156,8 @@ class IndexColumnSchema extends BaseUISchema {
* to access method selected by user if not selected
* send btree related op_class options
*/
let amname = obj._top?._sessData ?
obj._top?._sessData.amname :
obj._top?.origData.amname;
let amname = obj.top?.sessData.amname ||
obj.top?.origData.amname;
if(_.isUndefined(amname))
return options;
@@ -573,10 +573,12 @@ export default class IndexSchema extends BaseUISchema {
group: gettext('Columns'), type: 'collection',
mode: ['create', 'edit', 'properties'],
editable: false, schema: this.indexColumnSchema,
headerSchema: this.indexHeaderSchema, headerVisible: (state)=>indexSchemaObj.isNew(state),
CustomControl: DataGridViewWithHeaderForm,
headerSchema: this.indexHeaderSchema,
headerFormVisible: (state)=>indexSchemaObj.isNew(state),
GridHeader: DataGridFormHeader,
uniqueCol: ['colname'],
canAdd: false, canDelete: function(state) {
canAdd: (state)=>indexSchemaObj.isNew(state),
canDelete: function(state) {
// We can't update columns of existing
return indexSchemaObj.isNew(state);
}, cell: ()=>({

View File

@@ -25,9 +25,11 @@ import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.u
import pgAdmin from 'sources/pgadmin';
export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) {
const spcname = ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{
return (m.label != 'pg_global');
});
const spcname = () => getNodeListByName(
'tablespace', treeNodeInfo, itemNodeData, {}, (m) => {
return (m.label != 'pg_global');
}
);
let tableNode = pgBrowser.Nodes['table'];
@@ -48,9 +50,9 @@ export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) {
},
treeNodeInfo,
{
columns: ()=>getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser),
vacuum_settings: ()=>getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData),
constraints: ()=>new ConstraintsSchema(
columns: () => getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser),
vacuum_settings: () => getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData),
constraints: () => new ConstraintsSchema(
treeNodeInfo,
()=>getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, true, {autoindex: false}),
()=>getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgBrowser, true),
@@ -274,46 +276,47 @@ export class LikeSchema extends BaseUISchema {
id: 'like_default_value', label: gettext('With default values?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_constraints', label: gettext('With constraints?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_indexes', label: gettext('With indexes?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_storage', label: gettext('With storage?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_comments', label: gettext('With comments?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_compression', label: gettext('With compression?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
min_version: 140000, inlineNext: true,
min_version: 140000, inlineGroup: 'like_relation',
},{
id: 'like_generated', label: gettext('With generated?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
min_version: 120000, inlineNext: true,
min_version: 120000, inlineGroup: 'like_relation',
},{
id: 'like_identity', label: gettext('With identity?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineNext: true,
inlineGroup: 'like_relation',
},{
id: 'like_statistics', label: gettext('With statistics?'),
type: 'switch', mode: ['create'], deps: ['like_relation'],
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
inlineGroup: 'like_relation',
}
];
}
@@ -484,6 +487,12 @@ export default class TableSchema extends BaseUISchema {
}
};
}
},{
id: 'columns', type: 'group', label: gettext('Columns'),
},{
id: 'advanced', label: gettext('Advanced'), type: 'group',
},{
id: 'constraints', label: gettext('Constraints'), type: 'group',
},{
id: 'partition', type: 'group', label: gettext('Partitions'),
mode: ['edit', 'create'], min_version: 100000,
@@ -494,6 +503,12 @@ export default class TableSchema extends BaseUISchema {
// Always show in case of create mode
return (obj.isNew(state) || state.is_partitioned);
},
},{
type: 'group', id: 'parameters', label: gettext('Parameters'),
visible: !this.inErd,
},{
id: 'security_group', type: 'group', label: gettext('Security'),
visible: !this.inErd,
},{
id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch',
type: 'switch', mode: ['properties', 'create', 'edit'],
@@ -510,9 +525,12 @@ export default class TableSchema extends BaseUISchema {
mode: ['properties', 'create', 'edit'], disabled: this.inCatalog,
},{
id: 'coll_inherits', label: gettext('Inherited from table(s)'),
type: 'select', group: gettext('Columns'),
type: 'select', group: 'columns',
deps: ['typname', 'is_partitioned'], mode: ['create', 'edit'],
controlProps: { multiple: true, allowClear: false, placeholder: gettext('Select to inherit from...')},
controlProps: {
multiple: true, allowClear: false,
placeholder: gettext('Select to inherit from...')
},
options: this.fieldOptions.coll_inherits, visible: !this.inErd,
optionsLoaded: (res)=>obj.inheritedTableList=res,
disabled: (state)=>{
@@ -611,9 +629,6 @@ export default class TableSchema extends BaseUISchema {
}
});
},
},{
id: 'advanced', label: gettext('Advanced'), type: 'group',
visible: true,
},
{
id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch',
@@ -654,12 +669,9 @@ export default class TableSchema extends BaseUISchema {
},{
// Tab control for columns
id: 'columns', label: gettext('Columns'), type: 'collection',
group: gettext('Columns'),
schema: this.columnsSchema,
mode: ['create', 'edit'],
disabled: this.inCatalog,
deps: ['typname', 'is_partitioned'],
depChange: (state, source, topState, actionObj)=>{
group: 'columns', schema: this.columnsSchema, mode: ['create', 'edit'],
disabled: this.inCatalog, deps: ['typname', 'is_partitioned'],
depChange: (state, source, topState, actionObj) => {
if(source[0] === 'columns') {
/* In ERD, attnum is an imp let for setting the links
Here, attnum is set to max avail value.
@@ -718,7 +730,7 @@ export default class TableSchema extends BaseUISchema {
allowMultipleEmptyRow: false,
},{
// Here we will create tab control for constraints
type: 'nested-tab', group: gettext('Constraints'),
type: 'nested-tab', group: 'constraints',
mode: ['edit', 'create'],
schema: obj.constraintsObj,
},{
@@ -995,17 +1007,12 @@ export default class TableSchema extends BaseUISchema {
'</li></ul>',
].join(''),
min_version: 100000,
},{
type: 'group', id: 'parameters', label: gettext('Parameters'),
visible: !this.inErd,
},{
// Here - we will create tab control for storage parameters
// (auto vacuum).
type: 'nested-tab', group: 'parameters',
mode: ['edit', 'create'], deps: ['is_partitioned'],
schema: this.vacuumSettingsSchema, visible: !this.inErd,
},{
id: 'security_group', type: 'group', label: gettext('Security'), visible: !this.inErd,
},
{
id: 'relacl_str', label: gettext('Privileges'), disabled: this.inCatalog,

View File

@@ -1056,7 +1056,7 @@ class DataTypeSchema extends BaseUISchema {
}
},{
id: 'maxsize',
group: gettext('Definition'),
group: gettext('Data Type'),
label: gettext('Size'),
type: 'int',
deps: ['typtype'],

View File

@@ -7,6 +7,7 @@
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import SecLabelSchema from '../../../../../static/js/sec_label.ui';
@@ -154,7 +155,10 @@ export default class MViewSchema extends BaseUISchema {
if (state.definition) {
obj.warningText = null;
if (obj.origData.oid !== undefined && state.definition !== obj.origData.definition) {
if (
!_.isUndefined(obj.origData.oid) &&
state.definition !== obj.origData.definition
) {
obj.warningText = gettext(
'Updating the definition will drop and re-create the materialized view. It may result in loss of information about its dependent objects.'
) + '<br><br><b>' + gettext('Do you want to continue?') + '</b>';

View File

@@ -7,6 +7,7 @@
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import SecLabelSchema from '../../../../../static/js/sec_label.ui';
@@ -135,9 +136,10 @@ export default class ViewSchema extends BaseUISchema {
}
if (state.definition) {
if (!(obj.nodeInfo.server.server_type == 'pg' &&
if (!(
obj.nodeInfo.server.server_type == 'pg' &&
// No need to check this when creating a view
obj.origData.oid !== undefined
!_.isUndefined(obj.sessData.oid)
) || (
state.definition === obj.origData.definition
)) {
@@ -150,7 +152,7 @@ export default class ViewSchema extends BaseUISchema {
).split('FROM'),
new_def = [];
if (state.definition !== undefined) {
if (!_.isUndefined(state.definition)) {
new_def = state.definition.replace(
/\s/gi, ''
).split('FROM');

View File

@@ -129,7 +129,10 @@ export default class SubscriptionSchema extends BaseUISchema{
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'], min: 1, max: 65535,
depChange: (state)=>{
if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){
if(
obj.origData.port != state.port && !obj.isNew(state) &&
state.connected
) {
obj.informText = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);
@@ -145,7 +148,10 @@ export default class SubscriptionSchema extends BaseUISchema{
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
mode: ['properties', 'edit', 'create'],
depChange: (state)=>{
if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){
if(
obj.origData.username != state.username && !obj.isNew(state) &&
state.connected
) {
obj.informText = gettext(
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
);

View File

@@ -28,7 +28,7 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
super({
grantee: undefined,
grantor: nodeInfo?.server?.user?.name,
privileges: undefined,
privileges: [],
});
this.granteeOptions = granteeOptions;
this.grantorOptions = grantorOptions;
@@ -56,9 +56,12 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
{
id: 'privileges', label: gettext('Privileges'),
type: 'text', group: null,
cell: ()=>({cell: 'privilege', controlProps: {
supportedPrivs: this.supportedPrivs,
}}),
cell: () => ({
cell: 'privilege',
controlProps: {
supportedPrivs: this.supportedPrivs,
}
}),
disabled : function(state) {
return !(
obj.nodeInfo &&

View File

@@ -136,7 +136,7 @@ export default class ServerSchema extends BaseUISchema {
id: 'shared_username', label: gettext('Shared Username'), type: 'text',
controlProps: { maxLength: 64},
mode: ['properties', 'create', 'edit'], deps: ['shared', 'username'],
readonly: (s)=>{
readonly: (s) => {
return !(!this.origData.shared && s.shared);
}, visible: ()=>{
return current_user.is_admin && pgAdmin.server_mode == 'True';

View File

@@ -148,7 +148,7 @@ export default class VariableSchema extends BaseUISchema {
editable: function(state) {
return obj.isNew(state) || !obj.allReadOnly;
},
cell: ()=>({
cell: () => ({
cell: 'select',
options: this.vnameOptions,
optionsLoaded: (options)=>{obj.setVarTypes(options);},

View File

@@ -10,6 +10,7 @@
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
export default class ServerGroupSchema extends BaseUISchema {
constructor() {
super({
@@ -28,7 +29,7 @@ export default class ServerGroupSchema extends BaseUISchema {
id: 'name', label: gettext('Name'), type: 'text', group: null,
mode: ['properties', 'edit', 'create'], noEmpty: true,
disabled: false,
}
},
];
}
}