From d9cfbf592eb805b6208d07a34f1f472e32dd2c01 Mon Sep 17 00:00:00 2001 From: Nikhil Mohite Date: Mon, 26 Jul 2021 11:19:19 +0530 Subject: [PATCH] Port View node to react. Fixes #6637 --- .../databases/schemas/views/static/js/view.js | 21 +- .../schemas/views/static/js/view.ui.js | 186 ++++++++++++++++++ web/pgadmin/static/js/SchemaView/FormView.jsx | 9 +- .../static/js/SchemaView/MappedControl.jsx | 7 +- web/pgadmin/static/js/SchemaView/index.jsx | 18 ++ .../static/js/components/FormComponents.jsx | 16 +- .../schema_ui_files/event_trigger.ui.spec.js | 9 + .../schema_ui_files/view.ui.spec.js | 134 +++++++++++++ 8 files changed, 388 insertions(+), 12 deletions(-) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js create mode 100644 web/regression/javascript/schema_ui_files/view.ui.spec.js diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js index 87977fbc7..2f196ef63 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.js @@ -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) { diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js new file mode 100644 index 000000000..77c66f86a --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js @@ -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.' + ) + '

' + gettext('Do you wish to continue?') + + ''; + } else { + obj.warningText = null; + } + return true; + } + + } else { + errmsg = null; + _.each(['definition'], (item) => { + setError(item, errmsg); + }); + } + } +} diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index c561a80d7..0e86385a1 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -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(()=>{ @@ -282,6 +288,7 @@ export default function FormView({ useMemo(()=>, [sqlTabActive]), ]; tabsClassname[sqlTabName] = classes.fullSpace; + fullTabs.push(sqlTabName); } useEffect(()=>{ @@ -314,7 +321,7 @@ export default function FormView({ {Object.keys(tabs).map((tabName, i)=>{ return ( + className={fullTabs.indexOf(tabName) == -1 ? classes.nestedControl : null}> {tabs[tabName]} ); diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index c11159b50..16b73d13a 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -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 ; case 'sql': - return ; + return ; 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 = [ diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 246c36a29..6c3dcc952 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -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) { diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index d4eef3529..fcca88579 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -155,12 +155,16 @@ InputSQL.propTypes = { }; -export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, ...props}) { - return ( - - - - ); +export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props}) { + if(noLabel) { + return ; + } else { + return ( + + + + ); + } } FormInputSQL.propTypes = { hasError: PropTypes.bool, diff --git a/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js b/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js index 9de9cafe3..37d318bb4 100644 --- a/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js @@ -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); + }); }); diff --git a/web/regression/javascript/schema_ui_files/view.ui.spec.js b/web/regression/javascript/schema_ui_files/view.ui.spec.js new file mode 100644 index 000000000..d801b1c0b --- /dev/null +++ b/web/regression/javascript/schema_ui_files/view.ui.spec.js @@ -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({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + 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); + + }); +}); +