Port View node to react. Fixes #6637

This commit is contained in:
Nikhil Mohite 2021-07-26 11:19:19 +05:30 committed by Akshay Joshi
parent 6d18842dd3
commit d9cfbf592e
8 changed files with 388 additions and 12 deletions

View File

@ -7,6 +7,10 @@
//
//////////////////////////////////////////////////////////////
import { getNodeListByName } from '../../../../../../../static/js/node_ajax';
import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.ui';
import ViewSchema from './view.ui';
define('pgadmin.node.view', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform',
@ -90,7 +94,20 @@ define('pgadmin.node.view', [
},
]);
},
getSchema: function(treeNodeInfo, itemNodeData) {
return new ViewSchema(
(privileges)=>getNodePrivilegeRoleSchema('', treeNodeInfo, itemNodeData, privileges),
treeNodeInfo,
{
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
schema: ()=>getNodeListByName('schema', treeNodeInfo, itemNodeData, {cacheLevel: 'database'}),
},
{
owner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name,
schema: treeNodeInfo.schema.label
}
);
},
/**
Define model for the view node and specify the
properties of the model in schema.

View File

@ -0,0 +1,186 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import SecLabelSchema from '../../../../../static/js/sec_label.ui';
import { isEmptyString } from 'sources/validators';
export default class ViewSchema extends BaseUISchema {
constructor(getPrivilegeRoleSchema, nodeInfo, fieldOptions={}, initValues) {
super({
owner: undefined,
schema: undefined,
...initValues
});
this.getPrivilegeRoleSchema = getPrivilegeRoleSchema;
this.nodeInfo = nodeInfo;
this.warningText = null;
this.fieldOptions = {
role: [],
schema: [],
...fieldOptions,
};
}
get idAttribute() {
return 'oid';
}
notInSchema() {
if(this.node_info && 'catalog' in this.node_info) {
return true;
}
return false;
}
get baseFields() {
let obj = this;
return [{
id: 'name', label: gettext('Name'), cell: 'text',
type: 'text', disabled: obj.notInSchema, noEmpty: true,
},{
id: 'oid', label: gettext('OID'), cell: 'text',
type: 'text', mode: ['properties'],
},{
id: 'owner', label: gettext('Owner'), cell: 'text',
node: 'role', disabled: obj.notInSchema,
type: 'select', controlProps: { allowClear: false },
options: obj.fieldOptions.role
},{
id: 'schema', label: gettext('Schema'), cell: 'text',
type: 'select', disabled: obj.notInSchema, mode: ['create', 'edit'],
controlProps: {
allowClear: false,
first_empty: false,
},
options: obj.fieldOptions.schema
},{
id: 'system_view', label: gettext('System view?'), cell: 'text',
type: 'switch', mode: ['properties'],
},{
id: 'acl', label: gettext('Privileges'),
mode: ['properties'], type: 'text', group: gettext('Security'),
},{
id: 'comment', label: gettext('Comment'), cell: 'text',
type: 'multiline', disabled: obj.notInSchema,
},{
id: 'security_barrier', label: gettext('Security barrier?'),
type: 'switch', min_version: '90200', group: gettext('Definition'),
disabled: obj.notInSchema,
},{
id: 'check_option', label: gettext('Check options'),
type: 'select', group: gettext('Definition'),
min_version: '90400', mode:['properties', 'create', 'edit'],
controlProps: {
// Set select2 option width to 100%
allowClear: false,
}, disabled: obj.notInSchema,
options:[{
label: gettext('No'), value: 'no',
},{
label: gettext('Local'), value: 'local',
},{
label: gettext('Cascaded'), value: 'cascaded',
}],
},{
id: 'definition', label: gettext('Code'), cell: 'text',
type: 'sql', mode: ['create', 'edit'], group: gettext('Code'),
noLabel: true,
disabled: obj.notInSchema,
controlProps: {
className: ['sql-code-control'],
},
},
{
id: 'datacl', label: gettext('Privileges'), type: 'collection',
schema: this.getPrivilegeRoleSchema(['a', 'r', 'w', 'd', 'D', 'x', 't']),
uniqueCol : ['grantee'],
editable: false,
group: gettext('Security'), mode: ['edit', 'create'],
canAdd: true, canDelete: true,
},
{
// Add Security Labels Control
id: 'seclabels', label: gettext('Security labels'),
schema: new SecLabelSchema(),
editable: false, type: 'collection',
canEdit: false, group: gettext('Security'), canDelete: true,
mode: ['edit', 'create'], canAdd: true,
control: 'unique-col-collection',
uniqueCol : ['provider'],
}
];
}
validate(state, setError) {
let errmsg = null;
let obj = this;
if (isEmptyString(state.service)) {
/* view definition validation*/
if (isEmptyString(state.definition)) {
errmsg = gettext('Please enter view code.');
setError('definition', errmsg);
return true;
} else {
errmsg = null;
setError('definition', errmsg);
}
if (state.definition) {
if (!(obj.nodeInfo.server.server_type == 'pg' &&
// No need to check this when creating a view
obj.origData.oid !== undefined
) || !(
state.definition !== obj.origData.definition
)) {
obj.warningText = null;
return true;
}
let old_def = obj.origData.definition &&
obj.origData.definition.replace(
/\s/gi, ''
).split('FROM'),
new_def = [];
if (state.definition !== undefined) {
new_def = state.definition.replace(
/\s/gi, ''
).split('FROM');
}
if ((old_def.length != new_def.length) || (
old_def.length > 1 && (
old_def[0] != new_def[0]
)
)) {
obj.warningText = gettext(
'Changing the columns in a view requires dropping and re-creating the view. This may fail if other objects are dependent upon this view, or may cause procedural functions to fail if they are not modified to take account of the changes.'
) + '<br><br><b>' + gettext('Do you wish to continue?') +
'</b>';
} else {
obj.warningText = null;
}
return true;
}
} else {
errmsg = null;
_.each(['definition'], (item) => {
setError(item, errmsg);
});
}
}
}

View File

@ -167,6 +167,8 @@ export default function FormView({
}
}, []);
let fullTabs = [];
/* Prepare the array of components based on the types */
schema.fields.forEach((field)=>{
let {visible, disabled, readonly, canAdd, canEdit, canDelete, modeSupported} =
@ -232,6 +234,10 @@ export default function FormView({
* lets pass the new changes to dependent and get the new values
* from there as well.
*/
if(field.noLabel) {
tabsClassname[group] = classes.fullSpace;
fullTabs.push(group);
}
tabs[group].push(
useMemo(()=><MappedFormControl
inputRef={(ele)=>{
@ -282,6 +288,7 @@ export default function FormView({
useMemo(()=><SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />, [sqlTabActive]),
];
tabsClassname[sqlTabName] = classes.fullSpace;
fullTabs.push(sqlTabName);
}
useEffect(()=>{
@ -314,7 +321,7 @@ export default function FormView({
{Object.keys(tabs).map((tabName, i)=>{
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}
className={tabName != sqlTabName ? classes.nestedControl : null}>
className={fullTabs.indexOf(tabName) == -1 ? classes.nestedControl : null}>
{tabs[tabName]}
</TabPanel>
);

View File

@ -18,7 +18,7 @@ import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
/* Control mapping for form view */
function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, ...props}) {
function MappedFormControlBase({type, value, id, onChange, className, visible, inputRef, noLabel, ...props}) {
const name = id;
const onTextChange = useCallback((e) => {
let value = e;
@ -90,7 +90,7 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
case 'file':
return <FormInputFileSelect name={name} value={value} onChange={onTextChange} className={className} {...props} />;
case 'sql':
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} {...props}/>;
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} {...props} />;
default:
return <></>;
}
@ -108,6 +108,7 @@ MappedFormControlBase.propTypes = {
]),
visible: PropTypes.bool,
inputRef: CustomPropTypes.ref,
noLabel: PropTypes.bool
};
/* Control mapping for grid cell view */
@ -202,7 +203,7 @@ const ALLOWED_PROPS_FIELD_COMMON = [
];
const ALLOWED_PROPS_FIELD_FORM = [
'type', 'onChange', 'state',
'type', 'onChange', 'state', 'noLabel'
];
const ALLOWED_PROPS_FIELD_CELL = [

View File

@ -485,7 +485,25 @@ function SchemaDialogView({
} else {
changeData[schema.idAttribute] = schema.origData[schema.idAttribute];
}
if (schema.warningText) {
pgAlertify().confirm(
gettext('Warning'),
schema.warningText,
()=> {
save(changeData);
},
() => {
setSaving(false);
setLoaderText('');
return true;
}
);
} else {
save(changeData);
}
};
const save = (changeData) => {
props.onSave(isNew, changeData)
.then(()=>{
if(schema.informText) {

View File

@ -155,12 +155,16 @@ InputSQL.propTypes = {
};
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, ...props}) {
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props}) {
if(noLabel) {
return <InputSQL value={value} options={controlProps} {...props}/>;
} else {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} options={controlProps} {...props}/>
</FormInput>
);
}
}
FormInputSQL.propTypes = {
hasError: PropTypes.bool,

View File

@ -106,6 +106,15 @@ describe('EventTriggerSchema', ()=>{
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('eventfunname', 'Event trigger function cannot be empty.');
state.eventfunname = 'Test';
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('eventfunname', null);
state.service = 'Test';
state.eventfunname = 'Test';
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('eventfunname', null);
});
});

View File

@ -0,0 +1,134 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import pgAdmin from 'sources/pgadmin';
import {messages} from '../fake_messages';
import SchemaView from '../../../pgadmin/static/js/SchemaView';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import ViewSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js';
class MockSchema extends BaseUISchema {
get baseFields() {
return [];
}
}
describe('ViewSchema', ()=>{
let mount;
let schemaObj = new ViewSchema(
()=>new MockSchema(),
{server: {server_type: 'pg'}},
{
role: ()=>[],
schema: ()=>[],
},
{
owner: 'postgres',
schema: 'public'
}
);
let getInitData = ()=>Promise.resolve({});
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(()=>{
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(()=>{
jasmineEnzyme();
/* messages used by validators */
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
});
it('create', ()=>{
mount(<SchemaView
formType='dialog'
schema={schemaObj}
viewHelperProps={{
mode: 'create',
}}
onSave={()=>{}}
onClose={()=>{}}
onHelp={()=>{}}
onEdit={()=>{}}
onDataChange={()=>{}}
confirmOnCloseReset={false}
hasSQL={false}
disableSqlHelp={false}
/>);
});
it('edit', ()=>{
mount(<SchemaView
formType='dialog'
schema={schemaObj}
getInitData={getInitData}
viewHelperProps={{
mode: 'edit',
}}
onSave={()=>{}}
onClose={()=>{}}
onHelp={()=>{}}
onEdit={()=>{}}
onDataChange={()=>{}}
confirmOnCloseReset={false}
hasSQL={false}
disableSqlHelp={false}
/>);
});
it('properties', ()=>{
mount(<SchemaView
formType='tab'
schema={schemaObj}
getInitData={getInitData}
viewHelperProps={{
mode: 'properties',
}}
onHelp={()=>{}}
onEdit={()=>{}}
/>);
});
it('validate', ()=>{
let state = {};
let setError = jasmine.createSpy('setError');
state.definition = null;
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('definition', 'Please enter view code.');
state.definition = 'SELECT 1;';
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('definition', null);
state.definition = 'SELECT 1';
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('definition', null);
state.service = 'Test';
state.definition = 'SELECT 1';
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('definition', null);
});
});