From 169d8fa4805f769d2aec299abec264a601e120d3 Mon Sep 17 00:00:00 2001 From: Nikhil Mohite Date: Tue, 10 Aug 2021 11:37:51 +0530 Subject: [PATCH] Port Trigger Functions node to react. Fixes #6665 --- .../databases/schemas/functions/__init__.py | 3 +- .../functions/static/js/trigger_function.js | 50 ++- .../static/js/trigger_function.ui.js | 291 ++++++++++++++++++ .../trigger_function.ui.spec.js | 126 ++++++++ 4 files changed, 443 insertions(+), 27 deletions(-) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.ui.js create mode 100644 web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py index 60fd80464..82c9bed51 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/__init__.py @@ -1480,7 +1480,8 @@ class FunctionView(PGChildNodeView, DataTypeReader, SchemaDiffObjectCompare): parallel_dict = {'u': 'UNSAFE', 's': 'SAFE', 'r': 'RESTRICTED'} # Get Schema Name from its OID. - self._get_schema_name_from_oid(data) + if self.node_type != 'trigger_function': + self._get_schema_name_from_oid(data) if fnid is not None: # Edit Mode diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js index 94d9e9a7b..83ed9c6e3 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.js @@ -6,6 +6,10 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// +import TriggerFunctionSchema from './trigger_function.ui'; +import { getNodeListByName, getNodeAjaxOptions } from '../../../../../../../static/js/node_ajax'; +import { getNodeVariableSchema } from '../../../../../static/js/variable.ui'; +import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.ui'; /* Create and Register Function Collection and Node. */ define('pgadmin.node.trigger_function', [ @@ -81,6 +85,26 @@ define('pgadmin.node.trigger_function', [ }, ]); }, + getSchema: function(treeNodeInfo, itemNodeData) { + return new TriggerFunctionSchema( + (privileges)=>getNodePrivilegeRoleSchema('', treeNodeInfo, itemNodeData, privileges), + ()=>getNodeVariableSchema(this, treeNodeInfo, itemNodeData, false, false), + { + role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData), + schema: ()=>getNodeListByName('schema', treeNodeInfo, itemNodeData, {cacheLevel: 'database'}), + language: ()=>getNodeAjaxOptions('get_languages', this, treeNodeInfo, itemNodeData, {noCache: true}, (res) => { + return _.reject(res, function(o) { + return o.label == 'sql' || o.label == 'edbspl'; + }); + }), + nodeInfo: treeNodeInfo + }, + { + funcowner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name, + pronamespace: treeNodeInfo.schema ? treeNodeInfo.schema.label : '' + } + ); + }, model: pgBrowser.Node.Model.extend({ idAttribute: 'oid', initialize: function(attrs, args) { @@ -99,34 +123,8 @@ define('pgadmin.node.trigger_function', [ defaults: { name: undefined, oid: undefined, - xmin: undefined, funcowner: undefined, - pronamespace: undefined, description: undefined, - pronargs: undefined, /* Argument Count */ - proargs: undefined, /* Arguments */ - proargtypenames: undefined, /* Argument Signature */ - prorettypename: 'trigger', /* Return Type */ - lanname: 'plpgsql', /* Language Name in which function is being written */ - provolatile: undefined, /* Volatility */ - proretset: undefined, /* Return Set */ - proisstrict: undefined, - prosecdef: undefined, /* Security of definer */ - proiswindow: undefined, /* Window Function ? */ - procost: undefined, /* Estimated execution Cost */ - prorows: undefined, /* Estimated number of rows */ - proleakproof: undefined, - args: [], - prosrc: undefined, - prosrc_c: undefined, - probin: '$libdir/', - options: [], - variables: [], - proacl: undefined, - seclabels: [], - acl: [], - sysfunc: undefined, - sysproc: undefined, }, schema: [{ id: 'name', label: gettext('Name'), cell: 'string', diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.ui.js new file mode 100644 index 000000000..0257ce8b0 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.ui.js @@ -0,0 +1,291 @@ +///////////////////////////////////////////////////////////// +// +// 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 SecLabelSchema from '../../../../../static/js/sec_label.ui'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { isEmptyString } from 'sources/validators'; + + +export default class TriggerFunctionSchema extends BaseUISchema { + constructor(getPrivilegeRoleSchema, getVariableSchema, fieldOptions={}, initValues) { + super({ + name: null, + oid: null, + xmin: null, + funcowner: null, + pronamespace: null, + description: null, + pronargs: null, /* Argument Count */ + proargs: null, /* Arguments */ + proargtypenames: null, /* Argument Signature */ + prorettypename: 'trigger', /* Return Type */ + lanname: 'plpgsql', /* Language Name in which function is being written */ + provolatile: null, /* Volatility */ + proretset: null, /* Return Set */ + proisstrict: null, + prosecdef: null, /* Security of definer */ + proiswindow: null, /* Window Function ? */ + procost: null, /* Estimated execution Cost */ + prorows: null, /* Estimated number of rows */ + proleakproof: null, + args: [], + prosrc: null, + prosrc_c: null, + probin: '$libdir/', + options: [], + variables: [], + proacl: null, + seclabels: [], + acl: [], + sysfunc: null, + sysproc: null, + ...initValues + }); + + this.getPrivilegeRoleSchema = getPrivilegeRoleSchema; + this.getVariableSchema = getVariableSchema; + this.fieldOptions = { + role: [], + schema: [], + language: [], + nodeInfo: null, + ...fieldOptions, + }; + + } + + get idAttribute() { + return 'oid'; + } + + isReadonly(state) { + switch(state.name){ + case 'proargs': + case 'proargtypenames': + case 'prorettypename': + case 'proretset': + case 'proiswindow': + return !this.isNew(); + default: + return false; + } + } + + isVisible(state) { + if (state.name == 'sysproc') { return false; } + return true; + } + + isDisabled() { + if('catalog' in this.fieldOptions.nodeInfo) { + return true; + } + return false; + } + + + get baseFields() { + let obj = this; + return [ + { + id: 'name', label: gettext('Name'), cell: 'text', + type: 'text', mode: ['properties', 'create', 'edit'], + disabled: obj.isDisabled, readonly: obj.isReadonly, + noEmpty: true + },{ + id: 'oid', label: gettext('OID'), cell: 'text', + type: 'text' , mode: ['properties'], + },{ + id: 'funcowner', label: gettext('Owner'), cell: 'text', + type:'select', disabled: obj.isDisabled, readonly: obj.isReadonly, + options: obj.fieldOptions.role, + controlProps: { allowClear: false } + },{ + id: 'pronamespace', label: gettext('Schema'), cell: 'string', + type: 'select', cache_level: 'database', + disabled: obj.isDisabled, readonly: obj.isReadonly, + mode: ['create', 'edit'], + options: obj.fieldOptions.schema, + controlProps: { allowClear: false } + },{ + id: 'sysfunc', label: gettext('System trigger function?'), + cell:'boolean', type: 'switch', + mode: ['properties'], visible: obj.isVisible + },{ + id: 'sysproc', label: gettext('System procedure?'), + cell:'boolean', type: 'switch', + mode: ['properties'], visible: obj.isVisible + },{ + id: 'description', label: gettext('Comment'), cell: 'string', + type: 'multiline', disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'pronargs', label: gettext('Argument count'), cell: 'text', + type: 'text', group: gettext('Definition'), mode: ['properties'], + },{ + id: 'proargs', label: gettext('Arguments'), cell: 'string', + type: 'text', group: gettext('Definition'), mode: ['properties', 'edit'], + ddisabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'proargtypenames', label: gettext('Signature arguments'), cell: + 'text', type: 'text', group: gettext('Definition'), mode: ['properties'], + disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'prorettypename', label: gettext('Return type'), cell: 'text', + type: 'select', group: gettext('Definition'), + disabled: obj.isDisabled, readonly: obj.isReadonly, + controlProps: { + width: '100%', + allowClear: false, + }, + mode: ['create'], visible: obj.isVisible, + options: [ + {label: gettext('trigger'), value: 'trigger'}, + {label: gettext('event_trigger'), value: 'event_trigger'}, + ], + },{ + id: 'prorettypename', label: gettext('Return type'), cell: 'text', + type: 'text', group: gettext('Definition'), + mode: ['properties', 'edit'], disabled: obj.isDisabled, readonly: obj.isReadonly, + visible: obj.isVisible + }, { + id: 'lanname', label: gettext('Language'), cell: 'text', + type: 'select', group: gettext('Definition'), + disabled: obj.isDisabled, readonly: obj.isReadonly, + options: obj.fieldOptions.language, + controlProps: { + allowClear: false, + filter: (options) => { + return (options||[]).filter(option => { + return option.label != ''; + }); + } + }, + },{ + id: 'prosrc', label: gettext('Code'), cell: 'text', + type: 'sql', isFullTab: true, + mode: ['properties', 'create', 'edit'], + group: gettext('Code'), deps: ['lanname'], + visible: (state) => { + if (state.lanname == 'c') { + return false; + } + return true; + }, + disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'probin', label: gettext('Object file'), cell: 'string', + type: 'text', group: gettext('Definition'), deps: ['lanname'], + visible: (state) => { + if (state.lanname == 'c') { return true; } + return false; + }, + disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'prosrc_c', label: gettext('Link symbol'), cell: 'string', + type: 'text', group: gettext('Definition'), deps: ['lanname'], + visible: (state) => { + if (state.lanname == 'c') { return true; } + return false; + }, + disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'provolatile', label: gettext('Volatility'), cell: 'text', + type: 'select', group: gettext('Options'), + options:[ + {'label': 'VOLATILE', 'value': 'v'}, + {'label': 'STABLE', 'value': 's'}, + {'label': 'IMMUTABLE', 'value': 'i'}, + ], disabled: obj.isDisabled, readonly: obj.isReadonly, + controlProps: { allowClear: false }, + },{ + id: 'proretset', label: gettext('Returns a set?'), type: 'switch', + group: gettext('Options'), disabled: obj.isDisabled, readonly: obj.isReadonly, + visible: obj.isVisible + },{ + id: 'proisstrict', label: gettext('Strict?'), type: 'switch', + disabled: obj.isDisabled, readonly: obj.isReadonly, group: gettext('Options'), + },{ + id: 'prosecdef', label: gettext('Security of definer?'), + group: gettext('Options'), cell:'boolean', type: 'switch', + disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'proiswindow', label: gettext('Window?'), + group: gettext('Options'), cell:'boolean', type: 'switch', + disabled: obj.isDisabled, readonly: obj.isReadonly, visible: obj.isVisible + },{ + id: 'procost', label: gettext('Estimated cost'), type: 'text', + group: gettext('Options'), disabled: obj.isDisabled, readonly: obj.isReadonly, + },{ + id: 'prorows', label: gettext('Estimated rows'), type: 'text', + group: gettext('Options'), + disabled: (state) => { + let isDisabled = true; + if(state.proretset == true) { + isDisabled = false; + } + return isDisabled; + }, + readonly: obj.isReadonly, + deps: ['proretset'], visible: obj.isVisible + },{ + id: 'proleakproof', label: gettext('Leak proof?'), + group: gettext('Options'), cell:'boolean', type: 'switch', min_version: 90200, + disabled: obj.isDisabled, readonly: obj.isReadonly, + }, { + id: 'proacl', label: gettext('Privileges'), mode: ['properties'], + group: gettext('Security'), type: 'text', + }, + { + id: 'variables', label: '', type: 'collection', + group: gettext('Parameters'), control: 'variable-collection', + mode: ['edit', 'create'], canEdit: false, + canDelete: true, disabled: obj.isDisabled, readonly: obj.isReadonly, + schema: this.getVariableSchema(), + editable: false, + }, + { + id: 'acl', label: gettext('Privileges'), type: 'collection', + schema: this.getPrivilegeRoleSchema(['X']), + uniqueCol : ['grantee'], + editable: false, + group: gettext('Security'), mode: ['edit', 'create'], + canAdd: true, canDelete: true, + }, + { + id: 'seclabels', label: gettext('Security labels'), type: 'collection', + schema: new SecLabelSchema(), + editable: false, group: gettext('Security'), + mode: ['edit', 'create'], + canAdd: true, canEdit: false, canDelete: true, + uniqueCol : ['provider'], + min_version: 90200, + disabled: obj.isDisabled, readonly: obj.isReadonly, + } + ]; + } + + validate(state, setError) { + let errmsg = null; + + if (isEmptyString(state.service)) { + + /* code validation*/ + if (isEmptyString(state.prosrc)) { + errmsg = gettext('Code cannot be empty.'); + setError('prosrc', errmsg); + return true; + } else { + errmsg = null; + setError('prosrc', errmsg); + } + + } + } +} diff --git a/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js b/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js new file mode 100644 index 000000000..a552d5d22 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js @@ -0,0 +1,126 @@ +///////////////////////////////////////////////////////////// +// +// 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 TriggerFunctionSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/functions/static/js/trigger_function.ui'; + +class MockSchema extends BaseUISchema { + get baseFields() { + return []; + } +} + +describe('TriggerFunctionSchema', ()=>{ + let mount; + let schemaObj = new TriggerFunctionSchema( + ()=>new MockSchema(), + ()=>new MockSchema(), + { + role: [], + schema: [], + language: [], + nodeInfo: {} + }, + { + funcowner: 'postgres', + pronamespace: '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.prosrc = null; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('prosrc', 'Code cannot be empty.'); + + state.prosrc = 'SELECT 1'; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('prosrc', null); + }); + +}); +