mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-25 18:20:20 -06:00
Port View node to react. Fixes #6637
This commit is contained in:
parent
6d18842dd3
commit
d9cfbf592e
@ -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,11 +94,24 @@ 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.
|
||||
*/
|
||||
*/
|
||||
model: pgBrowser.Node.Model.extend({
|
||||
idAttribute: 'oid',
|
||||
initialize: function(attrs, args) {
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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 = [
|
||||
|
@ -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) {
|
||||
|
@ -155,12 +155,16 @@ InputSQL.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, ...props}) {
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
|
||||
<InputSQL value={value} options={controlProps} {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
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,
|
||||
|
@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
134
web/regression/javascript/schema_ui_files/view.ui.spec.js
Normal file
134
web/regression/javascript/schema_ui_files/view.ui.spec.js
Normal 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);
|
||||
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user