mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-25 18:20:20 -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', [
|
||||
'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',
|
||||
|
@ -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={{
|
||||
readOnly: true,
|
||||
}}
|
||||
isAsync={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -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 <></>;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ describe('CodeMirror', ()=>{
|
||||
cmInstance = mount(
|
||||
<CodeMirror
|
||||
value={'Init text'}
|
||||
isAsync={true}
|
||||
options={options}
|
||||
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