Port Event Triggers node to react. Fixes #6578

This commit is contained in:
Nikhil Mohite 2021-07-07 18:17:16 +05:30 committed by Akshay Joshi
parent d1e823bf39
commit 793dbc6e7f
9 changed files with 338 additions and 10 deletions

View File

@ -7,6 +7,9 @@
//
//////////////////////////////////////////////////////////////
import EventTriggerSchema from './event_trigger.ui';
import { getNodeListByName, getNodeAjaxOptions } from '../../../../../../static/js/node_ajax';
define('pgadmin.node.event_trigger', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser',
@ -64,6 +67,19 @@ define('pgadmin.node.event_trigger', [
},
]);
},
getSchema: function(treeNodeInfo, itemNodeData) {
return new EventTriggerSchema(
{
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
function_names: ()=>getNodeAjaxOptions('fopts', this, treeNodeInfo, itemNodeData, {
cacheLevel: 'trigger_function',
}),
},
{
eventowner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name,
}
);
},
// Define the model for event trigger node
model: pgAdmin.Browser.Node.Model.extend({
idAttribute: 'oid',

View File

@ -0,0 +1,133 @@
/////////////////////////////////////////////////////////////
//
// 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 EventTriggerSchema extends BaseUISchema {
constructor(fieldOptions={}, initValues) {
super({
oid: undefined,
name: undefined,
eventowner: undefined,
is_sys_obj: undefined,
comment: undefined,
enabled: 'O',
eventfuncoid: undefined,
eventfunname: undefined,
eventname: 'DDL_COMMAND_START',
when: undefined,
xmin: undefined,
source: undefined,
language: undefined,
...initValues
});
this.fieldOptions = {
role: [],
function_names: [],
...fieldOptions,
};
}
get idAttribute() {
return 'oid';
}
get baseFields() {
//let obj = this;
return [
{
id: 'name', label: gettext('Name'), cell: 'text',
type: 'text', noEmpty: true
},
{
id: 'oid', label: gettext('OID'), cell: 'text',
type: 'text', mode: ['properties'],
},{
id: 'eventowner', label: gettext('Owner'),
type: 'select', mode: ['properties', 'edit','create'], node: 'role',
options: this.fieldOptions.role
},
{
id: 'is_sys_obj', label: gettext('System event trigger?'),
cell:'switch', type: 'switch',
mode: ['properties'],
},
{
id: 'comment', label: gettext('Comment'), type: 'multiline',
},{
id: 'enabled', label: gettext('Trigger enabled?'),
group: gettext('Definition'), mode: ['properties', 'edit','create'],
options: [
{label: gettext('Enable'), value: 'O'},
{label: gettext('Disable'), value: 'D'},
{label: gettext('Replica'), value: 'R'},
{label: gettext('Always'), value: 'A'},
],
type: 'select', controlProps: { allowClear: false, width: '100%' },
},{
id: 'eventfunname', label: gettext('Trigger function'),
type: 'select', group: gettext('Definition'),
options: this.fieldOptions.function_names
},{
id: 'eventname', label: gettext('Event'),
group: gettext('Definition'), cell: 'text',
options: [
{label: gettext('DDL COMMAND START'), value: 'DDL_COMMAND_START'},
{label: gettext('DDL COMMAND END'), value: 'DDL_COMMAND_END'},
{label: gettext('SQL DROP'), value: 'SQL_DROP'},
],
type: 'select', controlProps: { allowClear: false, width: '100%' },
},
{
id: 'when', label: gettext('When TAG in'), cell: 'string',
type: 'sql', group: gettext('Definition'),
controlProps: {className:['custom_height_css_class']},
},
{
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,
}
];
}
validate(state, setError) {
let errmsg = null;
if (isEmptyString(state.service)) {
/* Event function name validation*/
if (isEmptyString(state.eventfunname)) {
errmsg = gettext('Event trigger function cannot be empty.');
setError('eventfunname', errmsg);
return true;
} else {
errmsg = null;
setError('eventfunname', errmsg);
}
} else {
errmsg = null;
_.each(['eventfunname'], (item) => {
setError(item, errmsg);
});
}
}
}

View File

@ -56,6 +56,7 @@ function SQLTab({active, getSQLValue}) {
options={{
readOnly: true,
}}
isAsync={true}
/>;
}

View File

@ -10,7 +10,7 @@
import React, { useCallback } from 'react';
import _ from 'lodash';
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, InputSQL, FormInputColor, FormInputFileSelect, FormInputToggle, InputSwitch } from '../components/FormComponents';
import { FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor, FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL } from '../components/FormComponents';
import { InputSelect, InputText } from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
@ -28,6 +28,10 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
onChange && onChange(value);
});
const onSqlChange = useCallback((e, cm) => {
onChange && onChange(cm.getValue());
});
const onIntChange = useCallback((e) => {
let value = e;
if(e && e.target) {
@ -73,7 +77,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 <InputSQL name={name} value={value} onChange={onTextChange} className={className} {...props}/>;
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} {...props}/>;
default:
return <></>;
}

View File

@ -9,26 +9,57 @@
import React, { useEffect, useRef } from 'react';
import {default as OrigCodeMirror} from 'bundled_codemirror';
import {useOnScreen} from 'sources/custom_hooks';
import PropTypes from 'prop-types';
/* React wrapper for CodeMirror */
export default function CodeMirror({name, value, options}) {
export default function CodeMirror({name, value, options, events, ...props}) {
const taRef = useRef();
const cmObj = useRef();
const cmWrapper = useRef();
const isVisibleTrack = useRef();
useEffect(()=>{
/* Create the object only once on mount */
cmObj.current = new OrigCodeMirror.fromTextArea(
taRef.current, options);
if(cmObj.current) {
try {
cmWrapper.current = cmObj.current.getWrapperElement();
} catch(e) {
cmWrapper.current = null;
}
}
Object.keys(events||{}).forEach((eventName)=>{
cmObj.current.on(eventName, events[eventName]);
});
}, []);
useEffect(()=>{
/* Refresh when value changes async */
if(props.isAsync) {
if(cmObj.current) {
cmObj.current.setValue(value);
cmObj.current.refresh();
}
}
}, [value]);
const onScreenVisible = useOnScreen(cmWrapper);
if(!isVisibleTrack.current && onScreenVisible) {
isVisibleTrack.current = true;
/* Refresh when value changes */
if(cmObj.current) {
cmObj.current.setValue(value);
cmObj.current.refresh();
}
}, [value]);
cmObj.current.refresh();
} else if(!onScreenVisible) {
isVisibleTrack.current = false;
}
return <textarea ref={taRef} name={name} />;
}
@ -36,5 +67,8 @@ export default function CodeMirror({name, value, options}) {
CodeMirror.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.object
options: PropTypes.object,
change: PropTypes.func,
events: PropTypes.object,
isAsync: PropTypes.bool
};

View File

@ -127,8 +127,9 @@ FormInput.propTypes = {
testcid: PropTypes.any,
};
export function InputSQL({value, options}) {
export function InputSQL({value, options, onChange, ...props}) {
const classes = useStyles();
return (
<CodeMirror
value={value||''}
@ -138,18 +139,26 @@ export function InputSQL({value, options}) {
...options,
}}
className={classes.sql}
events={{
change: (cm)=>{
onChange && onChange(cm.getValue(), cm);
},
}}
{...props}
/>
);
}
InputSQL.propTypes = {
value: PropTypes.string,
options: PropTypes.object,
onChange: PropTypes.func
};
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps}) {
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}/>
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} options={controlProps} {...props}/>
</FormInput>
);
}
@ -162,6 +171,7 @@ FormInputSQL.propTypes = {
testcid: PropTypes.string,
value: PropTypes.string,
controlProps: PropTypes.object,
change: PropTypes.func,
};

View File

@ -1,4 +1,4 @@
import {useRef, useEffect} from 'react';
import {useRef, useEffect, useState} from 'react';
/* React hook for setInterval */
export function useInterval(callback, delay) {
@ -39,3 +39,21 @@ export function useDelayDebounce(callback, args, delay) {
}, [args]);
}
export function useOnScreen(ref) {
const [isIntersecting, setIntersecting] = useState(false);
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
}
);
useEffect(() => {
if (ref.current) {
observer.observe(ref.current);
}
// Remove the observer as soon as the component is unmounted
return () => { observer.disconnect(); };
}, []);
return isIntersecting;
}

View File

@ -26,6 +26,7 @@ describe('CodeMirror', ()=>{
cmInstance = mount(
<CodeMirror
value={'Init text'}
isAsync={true}
options={options}
className="testClass"
/>);

View File

@ -0,0 +1,111 @@
/////////////////////////////////////////////////////////////
//
// 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 EventTriggerSchema from '../../../pgadmin/browser/server_groups/servers/databases/event_triggers/static/js/event_trigger.ui';
describe('EventTriggerSchema', ()=>{
let mount;
let schemaObj = new EventTriggerSchema(
{
role: ()=>[],
function_names: ()=>[],
},
{
eventowner: 'postgres'
}
);
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: 'create',
}}
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.eventfunname = null;
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('eventfunname', 'Event trigger function cannot be empty.');
});
});