mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Port Event Triggers node to react. Fixes #6578
This commit is contained in:
parent
d1e823bf39
commit
793dbc6e7f
@ -7,6 +7,9 @@
|
|||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
import EventTriggerSchema from './event_trigger.ui';
|
||||||
|
import { getNodeListByName, getNodeAjaxOptions } from '../../../../../../static/js/node_ajax';
|
||||||
|
|
||||||
define('pgadmin.node.event_trigger', [
|
define('pgadmin.node.event_trigger', [
|
||||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||||
'sources/pgadmin', 'pgadmin.browser',
|
'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
|
// Define the model for event trigger node
|
||||||
model: pgAdmin.Browser.Node.Model.extend({
|
model: pgAdmin.Browser.Node.Model.extend({
|
||||||
idAttribute: 'oid',
|
idAttribute: 'oid',
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -56,6 +56,7 @@ function SQLTab({active, getSQLValue}) {
|
|||||||
options={{
|
options={{
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
}}
|
}}
|
||||||
|
isAsync={true}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import _ from 'lodash';
|
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 { InputSelect, InputText } from '../components/FormComponents';
|
||||||
import Privilege from '../components/Privilege';
|
import Privilege from '../components/Privilege';
|
||||||
import { evalFunc } from 'sources/utils';
|
import { evalFunc } from 'sources/utils';
|
||||||
@ -28,6 +28,10 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
|
|||||||
onChange && onChange(value);
|
onChange && onChange(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onSqlChange = useCallback((e, cm) => {
|
||||||
|
onChange && onChange(cm.getValue());
|
||||||
|
});
|
||||||
|
|
||||||
const onIntChange = useCallback((e) => {
|
const onIntChange = useCallback((e) => {
|
||||||
let value = e;
|
let value = e;
|
||||||
if(e && e.target) {
|
if(e && e.target) {
|
||||||
@ -73,7 +77,7 @@ function MappedFormControlBase({type, value, id, onChange, className, visible, i
|
|||||||
case 'file':
|
case 'file':
|
||||||
return <FormInputFileSelect name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
return <FormInputFileSelect name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||||
case 'sql':
|
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:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -9,26 +9,57 @@
|
|||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
||||||
|
import {useOnScreen} from 'sources/custom_hooks';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
/* React wrapper for CodeMirror */
|
/* React wrapper for CodeMirror */
|
||||||
export default function CodeMirror({name, value, options}) {
|
export default function CodeMirror({name, value, options, events, ...props}) {
|
||||||
const taRef = useRef();
|
const taRef = useRef();
|
||||||
const cmObj = useRef();
|
const cmObj = useRef();
|
||||||
|
const cmWrapper = useRef();
|
||||||
|
const isVisibleTrack = useRef();
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
/* Create the object only once on mount */
|
/* Create the object only once on mount */
|
||||||
cmObj.current = new OrigCodeMirror.fromTextArea(
|
cmObj.current = new OrigCodeMirror.fromTextArea(
|
||||||
taRef.current, options);
|
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(()=>{
|
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 */
|
/* Refresh when value changes */
|
||||||
if(cmObj.current) {
|
if(cmObj.current) {
|
||||||
cmObj.current.setValue(value);
|
cmObj.current.setValue(value);
|
||||||
cmObj.current.refresh();
|
cmObj.current.refresh();
|
||||||
}
|
}
|
||||||
}, [value]);
|
cmObj.current.refresh();
|
||||||
|
} else if(!onScreenVisible) {
|
||||||
|
isVisibleTrack.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
return <textarea ref={taRef} name={name} />;
|
return <textarea ref={taRef} name={name} />;
|
||||||
}
|
}
|
||||||
@ -36,5 +67,8 @@ export default function CodeMirror({name, value, options}) {
|
|||||||
CodeMirror.propTypes = {
|
CodeMirror.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
options: PropTypes.object
|
options: PropTypes.object,
|
||||||
|
change: PropTypes.func,
|
||||||
|
events: PropTypes.object,
|
||||||
|
isAsync: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
@ -127,8 +127,9 @@ FormInput.propTypes = {
|
|||||||
testcid: PropTypes.any,
|
testcid: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InputSQL({value, options}) {
|
export function InputSQL({value, options, onChange, ...props}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value||''}
|
value={value||''}
|
||||||
@ -138,18 +139,26 @@ export function InputSQL({value, options}) {
|
|||||||
...options,
|
...options,
|
||||||
}}
|
}}
|
||||||
className={classes.sql}
|
className={classes.sql}
|
||||||
|
events={{
|
||||||
|
change: (cm)=>{
|
||||||
|
onChange && onChange(cm.getValue(), cm);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
InputSQL.propTypes = {
|
InputSQL.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
options: PropTypes.object,
|
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 (
|
return (
|
||||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
|
||||||
<InputSQL value={value} options={controlProps}/>
|
<InputSQL value={value} options={controlProps} {...props}/>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -162,6 +171,7 @@ FormInputSQL.propTypes = {
|
|||||||
testcid: PropTypes.string,
|
testcid: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
controlProps: PropTypes.object,
|
controlProps: PropTypes.object,
|
||||||
|
change: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useRef, useEffect} from 'react';
|
import {useRef, useEffect, useState} from 'react';
|
||||||
|
|
||||||
/* React hook for setInterval */
|
/* React hook for setInterval */
|
||||||
export function useInterval(callback, delay) {
|
export function useInterval(callback, delay) {
|
||||||
@ -39,3 +39,21 @@ export function useDelayDebounce(callback, args, delay) {
|
|||||||
}, [args]);
|
}, [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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ describe('CodeMirror', ()=>{
|
|||||||
cmInstance = mount(
|
cmInstance = mount(
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={'Init text'}
|
value={'Init text'}
|
||||||
|
isAsync={true}
|
||||||
options={options}
|
options={options}
|
||||||
className="testClass"
|
className="testClass"
|
||||||
/>);
|
/>);
|
||||||
|
@ -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.');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user