mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 15:09:02 +03:00
console: add scheduled triggers support (#4732)
This commit is contained in:
parent
ae75c6c06e
commit
aaab6d3eb6
@ -82,6 +82,7 @@
|
||||
"jsx-a11y/no-autofocus": 0,
|
||||
"max-len": 0,
|
||||
"no-continue": 0,
|
||||
"no-new": 0,
|
||||
"eqeqeq": 0,
|
||||
"no-nested-ternary": 0
|
||||
},
|
||||
@ -160,8 +161,12 @@
|
||||
"no-unused-expressions": "off",
|
||||
"no-console": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"no-plusplus": "off",
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"no-new": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"jsx-a11y/interactive-supports-focus": "off",
|
||||
"no-restricted-properties": "off",
|
||||
"react/no-danger": "off",
|
||||
|
@ -21,10 +21,12 @@ import {
|
||||
} from '../../validators/validators';
|
||||
import { setPromptValue } from '../../../helpers/common';
|
||||
|
||||
const EVENT_TRIGGER_INDEX_ROUTE = '/events/data';
|
||||
|
||||
const testName = 'ctr'; // create trigger
|
||||
|
||||
export const visitEventsManagePage = () => {
|
||||
cy.visit('/events/manage');
|
||||
cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/manage`);
|
||||
};
|
||||
|
||||
export const passPTCreateTable = () => {
|
||||
@ -69,11 +71,11 @@ export const passPTCreateTable = () => {
|
||||
|
||||
export const checkCreateTriggerRoute = () => {
|
||||
// Click on the create trigger button
|
||||
cy.visit('/events/manage');
|
||||
cy.visit(EVENT_TRIGGER_INDEX_ROUTE);
|
||||
cy.wait(15000);
|
||||
cy.get(getElementFromAlias('data-create-trigger')).click();
|
||||
cy.get(getElementFromAlias('data-sidebar-add')).click();
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`);
|
||||
};
|
||||
|
||||
export const failCTWithoutData = () => {
|
||||
@ -82,7 +84,7 @@ export const failCTWithoutData = () => {
|
||||
// Click on create
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
// Check if the route didn't change
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`);
|
||||
// Validate
|
||||
validateCT(getTriggerName(0, testName), ResultType.FAILURE);
|
||||
};
|
||||
@ -108,9 +110,15 @@ export const passCT = () => {
|
||||
cy.get(getElementFromAlias('advanced-settings')).click();
|
||||
|
||||
// retry configuration
|
||||
cy.get(getElementFromAlias('no-of-retries')).type(getNoOfRetries());
|
||||
cy.get(getElementFromAlias('interval-seconds')).type(getIntervalSeconds());
|
||||
cy.get(getElementFromAlias('timeout-seconds')).type(getTimeoutSeconds());
|
||||
cy.get(getElementFromAlias('no-of-retries'))
|
||||
.clear()
|
||||
.type(getNoOfRetries());
|
||||
cy.get(getElementFromAlias('interval-seconds'))
|
||||
.clear()
|
||||
.type(getIntervalSeconds());
|
||||
cy.get(getElementFromAlias('timeout-seconds'))
|
||||
.clear()
|
||||
.type(getTimeoutSeconds());
|
||||
|
||||
// Click on create
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
@ -118,7 +126,10 @@ export const passCT = () => {
|
||||
// Check if the trigger got created and navigated to processed events page
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`${baseUrl}/events/manage/triggers/${getTriggerName(0, testName)}/processed`
|
||||
`${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(
|
||||
0,
|
||||
testName
|
||||
)}/modify`
|
||||
);
|
||||
cy.get(getElementFromAlias(getTriggerName(0, testName)));
|
||||
// Validate
|
||||
@ -127,7 +138,7 @@ export const passCT = () => {
|
||||
|
||||
export const failCTDuplicateTrigger = () => {
|
||||
// Visit create trigger page
|
||||
cy.visit('/events/manage/triggers/add');
|
||||
cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/add`);
|
||||
// trigger and table name
|
||||
cy.get(getElementFromAlias('trigger-name'))
|
||||
.clear()
|
||||
@ -148,7 +159,7 @@ export const failCTDuplicateTrigger = () => {
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
cy.wait(5000);
|
||||
// should be on the same URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`);
|
||||
};
|
||||
|
||||
export const insertTableRow = () => {
|
||||
@ -163,13 +174,17 @@ export const insertTableRow = () => {
|
||||
// now it should invoke the trigger to webhook
|
||||
cy.wait(10000);
|
||||
// check if processed events has a row and it is a successful response
|
||||
cy.visit(`/events/manage/triggers/${getTriggerName(0, testName)}/processed`);
|
||||
cy.get(getElementFromAlias('trigger-processed-events')).contains('1');
|
||||
cy.visit(
|
||||
`${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(0, testName)}/processed`
|
||||
);
|
||||
cy.get('.rt-tr-group').should('have.length', 1);
|
||||
};
|
||||
|
||||
export const deleteCTTestTrigger = () => {
|
||||
// Go to the settings section of the trigger
|
||||
cy.visit(`/events/manage/triggers/${getTriggerName(0, testName)}/processed`);
|
||||
cy.visit(
|
||||
`${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(0, testName)}/processed`
|
||||
);
|
||||
// click on settings tab
|
||||
cy.get(getElementFromAlias('trigger-modify')).click();
|
||||
setPromptValue(getTriggerName(0, testName));
|
||||
@ -181,7 +196,7 @@ export const deleteCTTestTrigger = () => {
|
||||
.should('be.called');
|
||||
cy.wait(7000);
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers`);
|
||||
cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/manage`);
|
||||
// Validate
|
||||
validateCTrigger(getTriggerName(0, testName), ResultType.FAILURE);
|
||||
};
|
||||
|
78
console/package-lock.json
generated
78
console/package-lock.json
generated
@ -3259,6 +3259,40 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-notification-system": {
|
||||
"version": "0.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-notification-system/-/react-notification-system-0.2.39.tgz",
|
||||
"integrity": "sha512-yfptO86dbfW4qaw34CIedfakdKWV3sPM0xb4T5EQZOza80WLvOUUKjdmZTeGyEKk/7n2za8RJa6aZpz/A+2Qbw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-notification-system-redux": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-notification-system-redux/-/react-notification-system-redux-1.1.6.tgz",
|
||||
"integrity": "sha512-1Bi7ddqw0Taud2qYAvgN8STBat0/YxXEaRK+t9GlZBywKaO9ZaQ6uBHZPynVFYkmqQtURQPPYt7cNgdVzGlrNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"@types/react-notification-system": "*",
|
||||
"redux": "^3.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"redux": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
|
||||
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.2.1",
|
||||
"lodash-es": "^4.2.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"symbol-observable": "^1.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/react-redux": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.7.tgz",
|
||||
@ -3870,9 +3904,9 @@
|
||||
}
|
||||
},
|
||||
"ace-builds": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.8.tgz",
|
||||
"integrity": "sha512-8ZVAxwyCGAxQX8mOp9imSXH0hoSPkGfy8igJy+WO/7axL30saRhKgg1XPACSmxxPA7nfHVwM+ShWXT+vKsNuFg=="
|
||||
"version": "1.4.11",
|
||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.11.tgz",
|
||||
"integrity": "sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.1.1",
|
||||
@ -6611,6 +6645,12 @@
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||
@ -12843,10 +12883,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
|
||||
"dev": true
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
|
||||
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
@ -13688,7 +13727,7 @@
|
||||
},
|
||||
"onetime": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||
"dev": true
|
||||
},
|
||||
@ -15232,6 +15271,24 @@
|
||||
"prop-types": "^15.5.8"
|
||||
}
|
||||
},
|
||||
"react-datetime": {
|
||||
"version": "2.16.3",
|
||||
"resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-2.16.3.tgz",
|
||||
"integrity": "sha512-amWfb5iGEiyqjLmqCLlPpu2oN415jK8wX1qoTq7qn6EYiU7qQgbNHglww014PT4O/3G5eo/3kbJu/M/IxxTyGw==",
|
||||
"requires": {
|
||||
"create-react-class": "^15.5.2",
|
||||
"object-assign": "^3.0.0",
|
||||
"prop-types": "^15.5.7",
|
||||
"react-onclickoutside": "^6.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-assign": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
|
||||
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
||||
@ -15358,6 +15415,11 @@
|
||||
"react-notification-system": "^0.2.x"
|
||||
}
|
||||
},
|
||||
"react-onclickoutside": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
|
||||
"integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A=="
|
||||
},
|
||||
"react-overlays": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz",
|
||||
|
@ -50,6 +50,7 @@
|
||||
"dependencies": {
|
||||
"@graphql-codegen/core": "1.13.5",
|
||||
"@graphql-codegen/typescript": "1.13.5",
|
||||
"ace-builds": "^1.4.11",
|
||||
"apollo-link": "1.2.14",
|
||||
"apollo-link-ws": "1.0.20",
|
||||
"brace": "0.11.1",
|
||||
@ -62,6 +63,7 @@
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"less": "3.11.1",
|
||||
"moment": "^2.26.0",
|
||||
"piping": "0.3.2",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.13.1",
|
||||
@ -69,6 +71,7 @@
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-bootstrap": "0.32.4",
|
||||
"react-copy-to-clipboard": "5.0.2",
|
||||
"react-datetime": "^2.16.3",
|
||||
"react-dom": "16.13.1",
|
||||
"react-helmet": "5.2.1",
|
||||
"react-icons": "3.9.0",
|
||||
@ -120,6 +123,7 @@
|
||||
"@types/react-dom": "16.9.5",
|
||||
"@types/react-helmet": "5.0.15",
|
||||
"@types/react-hot-loader": "4.1.1",
|
||||
"@types/react-notification-system-redux": "1.1.6",
|
||||
"@types/react-redux": "7.1.7",
|
||||
"@types/react-router": "^3.0.8",
|
||||
"@types/react-router-redux": "4.0.44",
|
||||
|
@ -4,7 +4,7 @@ const baseUrl = globals.dataApiUrl;
|
||||
const hasuractlApiHost = globals.apiHost;
|
||||
const hasuractlApiPort = globals.apiPort;
|
||||
|
||||
const hasuractlUrl = hasuractlApiHost + ':' + hasuractlApiPort;
|
||||
const hasuractlUrl = `${hasuractlApiHost}:${hasuractlApiPort}`;
|
||||
|
||||
const Endpoints = {
|
||||
getSchema: `${baseUrl}/v1/query`,
|
@ -5,7 +5,6 @@ import { isEmpty } from './components/Common/utils/jsUtils';
|
||||
|
||||
// TODO: move this section to a more appropriate location
|
||||
/* set helper tools into window */
|
||||
|
||||
import sqlFormatter from './helpers/sql-formatter.min';
|
||||
import hljs from './helpers/highlight.min';
|
||||
|
||||
@ -47,7 +46,6 @@ const globals = {
|
||||
telemetryNotificationShown: '',
|
||||
isProduction,
|
||||
};
|
||||
|
||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
||||
if (isProduction) {
|
||||
const consolePath = window.__env.consolePath;
|
||||
|
BIN
console/src/action-diagram.png
Normal file
BIN
console/src/action-diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -1,5 +1,4 @@
|
||||
import defaultState from './State';
|
||||
import Notifications from 'react-notification-system-redux';
|
||||
import { loadConsoleOpts } from '../../telemetry/Actions';
|
||||
import { fetchServerConfig } from '../Main/Actions';
|
||||
|
||||
@ -24,29 +23,6 @@ const CONNECTION_FAILED = 'App/CONNECTION_FAILED';
|
||||
* onRemove: function, null, same as onAdd
|
||||
* uid: integer/string, null, unique identifier to the notification, same uid will not be shown again
|
||||
*/
|
||||
const showNotification = ({
|
||||
level = 'info',
|
||||
position = 'tr',
|
||||
...options
|
||||
} = {}) => {
|
||||
return dispatch => {
|
||||
if (level === 'success') {
|
||||
dispatch(Notifications.removeAll());
|
||||
}
|
||||
|
||||
dispatch(
|
||||
Notifications.show(
|
||||
{
|
||||
position,
|
||||
autoDismiss: ['error', 'warning'].includes(level) ? 0 : 5,
|
||||
dismissible: ['error', 'warning'].includes(level) ? 'button' : 'both',
|
||||
...options,
|
||||
},
|
||||
level
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const requireAsyncGlobals = ({ dispatch }) => {
|
||||
return (nextState, finalState, callback) => {
|
||||
@ -119,5 +95,4 @@ export {
|
||||
FAILED_REQUEST,
|
||||
ERROR_REQUEST,
|
||||
CONNECTION_FAILED,
|
||||
showNotification,
|
||||
};
|
||||
|
@ -5,7 +5,6 @@ import ProgressBar from 'react-progress-bar-plus';
|
||||
import Notifications from 'react-notification-system-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import ErrorBoundary from '../Error/ErrorBoundary';
|
||||
import { telemetryNotificationShown } from '../../telemetry/Actions';
|
||||
import { showTelemetryNotification } from '../../telemetry/Notifications';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import globals from 'Globals';
|
||||
|
||||
const stateKey = 'CONSOLE_LOCAL_INFO:' + globals.dataApiUrl;
|
||||
|
||||
const CONSOLE_ADMIN_SECRET = 'CONSOLE_ADMIN_SECRET';
|
||||
|
||||
const loadAppState = () => JSON.parse(window.localStorage.getItem(stateKey));
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { ACE_EDITOR_THEME, ACE_EDITOR_FONT_SIZE } from './utils';
|
||||
import AceEditor, { IAceEditorProps } from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-noconflict/ext-language_tools';
|
||||
import 'ace-builds/src-noconflict/ext-error_marker';
|
||||
import 'ace-builds/src-noconflict/ext-beautify';
|
||||
import { ACE_EDITOR_THEME, ACE_EDITOR_FONT_SIZE } from './utils';
|
||||
|
||||
const Editor = ({ mode, ...props }) => {
|
||||
const Editor: React.FC<IAceEditorProps> = ({ mode, ...props }) => {
|
||||
return (
|
||||
<AceEditor
|
||||
mode={mode}
|
||||
theme={ACE_EDITOR_THEME}
|
||||
fontSize={ACE_EDITOR_FONT_SIZE}
|
||||
showPrintMargine
|
||||
showGutter
|
||||
tabSize={2}
|
||||
setOptions={{
|
@ -1,3 +1,5 @@
|
||||
// eslint-disable-file import/no-extraneous-dependencies
|
||||
|
||||
import 'ace-builds/src-noconflict/theme-eclipse';
|
||||
import 'ace-builds/src-noconflict/mode-graphqlschema';
|
||||
import 'ace-builds/src-noconflict/mode-sql';
|
||||
@ -6,7 +8,7 @@ import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
export const ACE_EDITOR_THEME = 'eclipse';
|
||||
export const ACE_EDITOR_FONT_SIZE = 14;
|
||||
|
||||
export const getLanguageModeFromExtension = extension => {
|
||||
export const getLanguageModeFromExtension = (extension: string) => {
|
||||
switch (extension) {
|
||||
case 'ts':
|
||||
return 'typescript';
|
@ -10,8 +10,8 @@ import styles from './CollapsibleToggle.scss';
|
||||
*/
|
||||
|
||||
interface CollapsibleToggleProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
title: React.ReactNode;
|
||||
isOpen?: boolean;
|
||||
toggleHandler?: () => void;
|
||||
testId: string;
|
||||
useDefaultTitleStyle?: boolean;
|
||||
@ -40,7 +40,7 @@ class CollapsibleToggle extends React.Component<
|
||||
const { isOpen, toggleHandler } = nextProps;
|
||||
|
||||
if (toggleHandler) {
|
||||
this.setState({ isOpen, toggleHandler });
|
||||
this.setState({ isOpen: !!isOpen, toggleHandler });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -478,6 +478,10 @@ input {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.addPadding20Px {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.width_auto {
|
||||
width: auto;
|
||||
}
|
||||
|
@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import InputGroup from 'react-bootstrap/lib/InputGroup';
|
||||
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
||||
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
||||
|
||||
class DropButton extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
dropdownOptions,
|
||||
value,
|
||||
required,
|
||||
onInputChange,
|
||||
onButtonChange,
|
||||
dataKey,
|
||||
dataIndex,
|
||||
bsClass,
|
||||
disabled,
|
||||
inputVal,
|
||||
inputPlaceHolder,
|
||||
id,
|
||||
testId,
|
||||
} = this.props;
|
||||
return (
|
||||
<InputGroup className={bsClass}>
|
||||
<DropdownButton
|
||||
title={value || title}
|
||||
componentClass={InputGroup.Button}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
data-test={testId + '-' + 'dropdown-button'}
|
||||
>
|
||||
{dropdownOptions.map((d, i) => (
|
||||
<MenuItem
|
||||
data-index-id={dataIndex}
|
||||
value={d.value}
|
||||
onClick={onButtonChange}
|
||||
eventKey={i + 1}
|
||||
key={i}
|
||||
data-test={testId + '-' + 'dropdown-item' + '-' + (i + 1)}
|
||||
>
|
||||
{d.display_text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
<input
|
||||
type="text"
|
||||
data-key={dataKey}
|
||||
data-index-id={dataIndex}
|
||||
className={'form-control'}
|
||||
required={required}
|
||||
onChange={onInputChange}
|
||||
disabled={disabled}
|
||||
value={inputVal || ''}
|
||||
placeholder={inputPlaceHolder}
|
||||
data-test={testId + '-' + 'input'}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DropButton.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
dropdownOptions: PropTypes.array.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
dataKey: PropTypes.string.isRequired,
|
||||
dataIndex: PropTypes.string.isRequired,
|
||||
inputVal: PropTypes.string.isRequired,
|
||||
inputPlaceHolder: PropTypes.string,
|
||||
required: PropTypes.bool.isRequired,
|
||||
onButtonChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
bsClass: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
testId: PropTypes.string,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default DropButton;
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import InputGroup from 'react-bootstrap/lib/InputGroup';
|
||||
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
||||
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
||||
|
||||
type DropDownButtonProps = {
|
||||
title: string;
|
||||
dropdownOptions: {
|
||||
display_text: string;
|
||||
value: string;
|
||||
}[];
|
||||
dataKey: string;
|
||||
dataIndex?: string;
|
||||
onButtonChange: (e: React.MouseEvent) => void;
|
||||
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value?: string;
|
||||
inputVal: string;
|
||||
required: boolean;
|
||||
id: string;
|
||||
testId: string;
|
||||
disabled?: boolean;
|
||||
bsClass: string;
|
||||
inputPlaceHolder: string;
|
||||
};
|
||||
|
||||
const DDButton: React.FC<DropDownButtonProps> = props => {
|
||||
const {
|
||||
title,
|
||||
dropdownOptions,
|
||||
value,
|
||||
required,
|
||||
onInputChange,
|
||||
onButtonChange,
|
||||
dataKey,
|
||||
dataIndex,
|
||||
bsClass,
|
||||
disabled,
|
||||
inputVal,
|
||||
inputPlaceHolder,
|
||||
id,
|
||||
testId,
|
||||
} = props;
|
||||
return (
|
||||
<InputGroup className={bsClass}>
|
||||
<DropdownButton
|
||||
title={value || title}
|
||||
componentClass={InputGroup.Button}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
data-test={`${testId}-dropdown-button`}
|
||||
>
|
||||
{dropdownOptions.map((d, i) => (
|
||||
<MenuItem
|
||||
data-index-id={dataIndex}
|
||||
value={d.value}
|
||||
onClick={onButtonChange}
|
||||
eventKey={i + 1}
|
||||
key={i}
|
||||
data-test={`${testId}-dropdown-item-${i + 1}`}
|
||||
>
|
||||
{d.display_text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
<input
|
||||
type="text"
|
||||
data-key={dataKey}
|
||||
data-index-id={dataIndex}
|
||||
className="form-control"
|
||||
required={required}
|
||||
onChange={onInputChange}
|
||||
disabled={disabled}
|
||||
value={inputVal || ''}
|
||||
placeholder={inputPlaceHolder}
|
||||
data-test={`${testId}-input`}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default DDButton;
|
95
console/src/components/Common/FilterQuery/FilterQuery.tsx
Normal file
95
console/src/components/Common/FilterQuery/FilterQuery.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
import { BaseTable, generateTableDef } from '../utils/pgUtils';
|
||||
import Where from './Where';
|
||||
import Sorts from './Sorts';
|
||||
import { useFilterQuery } from './state';
|
||||
import { Filter, FilterRenderProp } from './types';
|
||||
import { Dispatch } from '../../../types';
|
||||
import ReloadEnumValuesButton from '../../Services/Data/Common/Components/ReloadEnumValuesButton';
|
||||
import Button from '../Button/Button';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import styles from './FilterQuery.scss';
|
||||
|
||||
type Props = {
|
||||
table: BaseTable;
|
||||
relationships: Nullable<string[]>; // TODO better
|
||||
render: FilterRenderProp;
|
||||
dispatch: Dispatch;
|
||||
presets: {
|
||||
filters: Filter[];
|
||||
sorts: OrderBy[];
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Where clause and sorts builder
|
||||
* Accepts a render prop to render the results of filter/sort query
|
||||
*/
|
||||
|
||||
const FilterQuery: React.FC<Props> = props => {
|
||||
const { table, dispatch, presets, render, relationships } = props;
|
||||
|
||||
const { rows, count, runQuery, state, setState } = useFilterQuery(
|
||||
generateTableDef(table.table_name, table.table_schema),
|
||||
dispatch,
|
||||
presets,
|
||||
relationships
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.add_mar_top}>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
runQuery();
|
||||
}}
|
||||
className={styles.add_mar_bottom}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${styles.padd_left_remove}`}
|
||||
>
|
||||
<span className={styles.subheading_text}>Filter</span>
|
||||
<Where
|
||||
filters={state.filters}
|
||||
setFilters={setState.filters}
|
||||
table={table}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${styles.padd_left_remove}`}
|
||||
>
|
||||
<b className={styles.subheading_text}>Sort</b>
|
||||
<Sorts
|
||||
sorts={state.sorts}
|
||||
setSorts={setState.sorts}
|
||||
table={table}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.padd_right} ${styles.clear_fix}`}>
|
||||
<Button
|
||||
type="submit"
|
||||
color="yellow"
|
||||
size="sm"
|
||||
data-test="run-query"
|
||||
className={styles.add_mar_right}
|
||||
>
|
||||
Run query
|
||||
</Button>
|
||||
<ReloadEnumValuesButton
|
||||
dispatch={dispatch}
|
||||
isEnum={table.is_enum}
|
||||
tooltipStyle={styles.add_mar_left_mid}
|
||||
/>
|
||||
{/* <div className={styles.count + ' alert alert-info'}><i>Total <b>{tableName}</b> rows in the database for current query: {count} </i></div> */}
|
||||
</div>
|
||||
</form>
|
||||
{/* TODO: Handle loading state */}
|
||||
{render(rows, count, state, setState, runQuery)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
96
console/src/components/Common/FilterQuery/Sorts.tsx
Normal file
96
console/src/components/Common/FilterQuery/Sorts.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
|
||||
import { BaseTable } from '../utils/pgUtils';
|
||||
import styles from './FilterQuery.scss';
|
||||
|
||||
type Props = {
|
||||
sorts: OrderBy[];
|
||||
setSorts: (o: OrderBy[]) => void;
|
||||
table: BaseTable;
|
||||
};
|
||||
|
||||
const Sorts: React.FC<Props> = props => {
|
||||
const { sorts, setSorts, table } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{sorts.map((sort, i) => {
|
||||
const removeSort = () => {
|
||||
setSorts([...sorts.slice(0, i), ...sorts.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const setColumn = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const col = e.target.value;
|
||||
setSorts([
|
||||
...sorts.slice(0, i),
|
||||
{ ...sorts[i], column: col },
|
||||
...sorts.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setType = (e: React.BaseSyntheticEvent) => {
|
||||
const type = e.target.value;
|
||||
setSorts([
|
||||
...sorts.slice(0, i),
|
||||
{ ...sorts[i], type },
|
||||
...sorts.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i} // eslint-disable-line react/no-array-index-key
|
||||
className={`${styles.inputRow} row`}
|
||||
>
|
||||
<div className="col-xs-4">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setColumn}
|
||||
value={sort.column}
|
||||
data-test={`filter-column-${i}`}
|
||||
>
|
||||
{sort.column === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{table.columns.map(c => (
|
||||
<option key={c.column_name} value={c.column_name}>
|
||||
{c.column_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-3">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setType}
|
||||
value={sort.type}
|
||||
data-test={`filter-op-${i}`}
|
||||
>
|
||||
<option key="asc" value="asc">
|
||||
asc
|
||||
</option>
|
||||
<option key="desc" value="desc">
|
||||
desc
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">
|
||||
{sorts.length === i + 1 ? null : (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={removeSort}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorts;
|
121
console/src/components/Common/FilterQuery/Where.tsx
Normal file
121
console/src/components/Common/FilterQuery/Where.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { ValueFilter, Operator } from './types';
|
||||
import { allOperators } from './utils';
|
||||
|
||||
import { BaseTable } from '../utils/pgUtils';
|
||||
import { isNotDefined } from '../utils/jsUtils';
|
||||
import styles from './FilterQuery.scss';
|
||||
|
||||
type Props = {
|
||||
filters: ValueFilter[];
|
||||
setFilters: (f: ValueFilter[]) => void;
|
||||
table: BaseTable;
|
||||
};
|
||||
|
||||
const Where: React.FC<Props> = props => {
|
||||
const { filters, setFilters, table } = props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{filters.map((filter, i) => {
|
||||
const removeFilter = () => {
|
||||
setFilters([...filters.slice(0, i), ...filters.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const setKey = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const col = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], key: col },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setOperator = (e: React.BaseSyntheticEvent) => {
|
||||
// TODO synthetic event with enums
|
||||
const op: Operator = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], operator: op, value: '' },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
const setValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setFilters([
|
||||
...filters.slice(0, i),
|
||||
{ ...filters[i], value },
|
||||
...filters.slice(i + 1),
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i} // eslint-disable-line react/no-array-index-key
|
||||
className={`${styles.inputRow} row`}
|
||||
>
|
||||
<div className="col-xs-4">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setKey}
|
||||
value={filter.key}
|
||||
data-test={`filter-column-${i}`}
|
||||
>
|
||||
{filter.key === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{table.columns.map(c => (
|
||||
<option key={c.column_name} value={c.column_name}>
|
||||
{c.column_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-3">
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={setOperator}
|
||||
value={filter.operator || ''}
|
||||
data-test={`filter-op-${i}`}
|
||||
>
|
||||
{isNotDefined(filter.operator) ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{allOperators.map(o => (
|
||||
<option key={o.operator} value={o.operator}>
|
||||
{`[${o.alias}] ${o.name}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="-- value --"
|
||||
value={filter.value}
|
||||
onChange={setValue}
|
||||
data-test={`filter-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">
|
||||
{filters.length === i + 1 ? null : (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={removeFilter}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Where;
|
178
console/src/components/Common/FilterQuery/state.ts
Normal file
178
console/src/components/Common/FilterQuery/state.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
TableDefinition,
|
||||
getSelectQuery,
|
||||
OrderBy,
|
||||
makeOrderBy,
|
||||
} from '../utils/v1QueryUtils';
|
||||
import requestAction from '../../../utils/requestAction';
|
||||
import { Dispatch } from '../../../types';
|
||||
import endpoints from '../../../Endpoints';
|
||||
import {
|
||||
makeFilterState,
|
||||
SetFilterState,
|
||||
ValueFilter,
|
||||
makeValueFilter,
|
||||
Filter,
|
||||
RunQuery,
|
||||
} from './types';
|
||||
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
import { isNotDefined } from '../utils/jsUtils';
|
||||
import { parseFilter } from './utils';
|
||||
|
||||
const defaultFilter = makeValueFilter('', null, '');
|
||||
const defaultSort = makeOrderBy('', 'asc');
|
||||
|
||||
const defaultState = makeFilterState([defaultFilter], [defaultSort], 10, 0);
|
||||
|
||||
export const useFilterQuery = (
|
||||
table: TableDefinition,
|
||||
dispatch: Dispatch,
|
||||
presets: {
|
||||
filters: Filter[];
|
||||
sorts: OrderBy[];
|
||||
},
|
||||
relationships: Nullable<string[]>
|
||||
) => {
|
||||
const [state, setState] = React.useState(defaultState);
|
||||
const [rows, setRows] = React.useState<any[]>([]);
|
||||
const [count, setCount] = React.useState<number>();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(false);
|
||||
|
||||
const runQuery: RunQuery = (runQueryOpts = {}) => {
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
|
||||
const { offset, limit, sorts: newSorts } = runQueryOpts;
|
||||
|
||||
const where = {
|
||||
$and: [...state.filters, ...presets.filters]
|
||||
.filter(f => !!f.key && !!f.value)
|
||||
.map(f => parseFilter(f)),
|
||||
};
|
||||
|
||||
const orderBy = newSorts || [
|
||||
...state.sorts.filter(f => !!f.column),
|
||||
...presets.sorts,
|
||||
];
|
||||
|
||||
const query = getSelectQuery(
|
||||
'select',
|
||||
table,
|
||||
['*', ...(relationships || []).map(r => ({ name: r, columns: ['*'] }))],
|
||||
where,
|
||||
isNotDefined(offset) ? state.offset : offset,
|
||||
isNotDefined(limit) ? state.limit : limit,
|
||||
orderBy
|
||||
);
|
||||
const countQuery = getSelectQuery(
|
||||
'count',
|
||||
table,
|
||||
['*', ...(relationships || []).map(r => ({ name: r, columns: ['*'] }))],
|
||||
where,
|
||||
undefined,
|
||||
undefined,
|
||||
orderBy
|
||||
);
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(query),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
requestAction(endpoints.query, options, undefined, undefined, true, true)
|
||||
).then(
|
||||
(data: any[]) => {
|
||||
setRows(data);
|
||||
setLoading(false);
|
||||
if (offset !== undefined) {
|
||||
setState(s => ({ ...s, offset }));
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
setState(s => ({ ...s, limit }));
|
||||
}
|
||||
if (newSorts) {
|
||||
setState(s => ({
|
||||
...s,
|
||||
sorts: newSorts,
|
||||
}));
|
||||
}
|
||||
dispatch(
|
||||
requestAction(
|
||||
endpoints.query,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(countQuery),
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
)
|
||||
).then((countData: { count: number }) => {
|
||||
setCount(countData.count);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
runQuery();
|
||||
}, []);
|
||||
|
||||
const setter: SetFilterState = {
|
||||
sorts: (sorts: OrderBy[]) => {
|
||||
const newSorts = [...sorts];
|
||||
if (!sorts.length || sorts[sorts.length - 1].column) {
|
||||
newSorts.push(defaultSort);
|
||||
}
|
||||
setState(s => ({
|
||||
...s,
|
||||
sorts: newSorts,
|
||||
}));
|
||||
},
|
||||
filters: (filters: ValueFilter[]) => {
|
||||
const newFilters = [...filters];
|
||||
if (
|
||||
!filters.length ||
|
||||
filters[filters.length - 1].value ||
|
||||
filters[filters.length - 1].key
|
||||
) {
|
||||
newFilters.push(defaultFilter);
|
||||
}
|
||||
setState(s => ({
|
||||
...s,
|
||||
filters: newFilters,
|
||||
}));
|
||||
},
|
||||
offset: (o: number) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
offset: o,
|
||||
}));
|
||||
},
|
||||
limit: (l: number) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
limit: l,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
rows,
|
||||
loading,
|
||||
error,
|
||||
runQuery,
|
||||
state,
|
||||
count,
|
||||
setState: setter,
|
||||
};
|
||||
};
|
142
console/src/components/Common/FilterQuery/types.ts
Normal file
142
console/src/components/Common/FilterQuery/types.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { OrderBy } from '../utils/v1QueryUtils';
|
||||
import { Nullable } from '../utils/tsUtils';
|
||||
|
||||
// Types for sorts
|
||||
export type SetSorts = (s: OrderBy[]) => void;
|
||||
|
||||
// All supported postgres operators
|
||||
export type Operator =
|
||||
| '$eq'
|
||||
| '$ne'
|
||||
| '$in'
|
||||
| '$nin'
|
||||
| '$gt'
|
||||
| '$lt'
|
||||
| '$gte'
|
||||
| '$lte'
|
||||
| '$like'
|
||||
| '$nlike'
|
||||
| '$ilike'
|
||||
| '$nilike'
|
||||
| '$similar'
|
||||
| '$nsimilar';
|
||||
|
||||
// Operator with names and aliases
|
||||
export type OperatorDef = {
|
||||
alias: string;
|
||||
operator: Operator;
|
||||
name: string;
|
||||
default?: string;
|
||||
};
|
||||
|
||||
/*
|
||||
* Value filter. Eg: { name: { $eq: "jondoe" } }
|
||||
*/
|
||||
|
||||
export type ValueFilter = {
|
||||
kind: 'value';
|
||||
key: string;
|
||||
operator: Nullable<Operator>;
|
||||
value: string;
|
||||
};
|
||||
/*
|
||||
* Constructor for value filter
|
||||
*/
|
||||
export const makeValueFilter = (
|
||||
key: string,
|
||||
operator: Nullable<Operator>,
|
||||
value: string
|
||||
): ValueFilter => ({ kind: 'value', key, operator, value });
|
||||
|
||||
/*
|
||||
* Relationship filter filter. Eg: { user { name: { $eq: "jondoe" } } }
|
||||
*/
|
||||
|
||||
export type RelationshipFilter = {
|
||||
kind: 'relationship';
|
||||
key: string;
|
||||
value: Filter;
|
||||
};
|
||||
/*
|
||||
* Constructor for relationship filter
|
||||
*/
|
||||
export const makeRelationshipFilter = (
|
||||
key: string,
|
||||
value: Filter
|
||||
): RelationshipFilter => ({ kind: 'relationship', key, value });
|
||||
|
||||
/*
|
||||
* Filter with logical gates
|
||||
* Eg: { $and: [ { title: { $eq: "My Title" } }, { author: { name: { $eq: "jon" }}} ]}
|
||||
*/
|
||||
|
||||
type LogicGate = '$or' | '$and' | '$not';
|
||||
export type OperatorFilter = {
|
||||
kind: 'operator';
|
||||
key: LogicGate;
|
||||
value: Filter[];
|
||||
};
|
||||
|
||||
/*
|
||||
* Constructor for operation filter
|
||||
*/
|
||||
export const makeOperationFilter = (
|
||||
key: LogicGate,
|
||||
value: Filter[]
|
||||
): OperatorFilter => ({ kind: 'operator', key, value });
|
||||
|
||||
/*
|
||||
* Filter for building the where clause
|
||||
* Filter could be a value filter, relationship filter, or a combination of filters
|
||||
*/
|
||||
export type Filter = ValueFilter | RelationshipFilter | OperatorFilter;
|
||||
|
||||
/*
|
||||
* Setter function for Filters
|
||||
*/
|
||||
export type SetValueFilters = (s: ValueFilter[]) => void;
|
||||
|
||||
/*
|
||||
* Local state for the filter query component
|
||||
*/
|
||||
export type FilterState = {
|
||||
filters: ValueFilter[];
|
||||
sorts: OrderBy[];
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
/*
|
||||
* Constructor for FilterState
|
||||
*/
|
||||
export const makeFilterState = (
|
||||
filters: ValueFilter[],
|
||||
sorts: OrderBy[],
|
||||
limit: number,
|
||||
offset: number
|
||||
): FilterState => ({ filters, sorts, limit, offset });
|
||||
|
||||
/*
|
||||
* Local state setter for the filter query component
|
||||
*/
|
||||
export type SetFilterState = {
|
||||
sorts: SetSorts;
|
||||
filters: SetValueFilters;
|
||||
offset: (o: number) => void;
|
||||
limit: (l: number) => void;
|
||||
};
|
||||
|
||||
export type RunQueryOptions = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
sorts?: OrderBy[];
|
||||
};
|
||||
|
||||
export type RunQuery = (options?: RunQueryOptions) => void;
|
||||
|
||||
export type FilterRenderProp = (
|
||||
rows: any[],
|
||||
count: number | undefined,
|
||||
state: FilterState,
|
||||
setState: SetFilterState,
|
||||
runQuery: RunQuery
|
||||
) => React.ReactNode;
|
66
console/src/components/Common/FilterQuery/utils.ts
Normal file
66
console/src/components/Common/FilterQuery/utils.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Operator, OperatorDef, Filter } from './types';
|
||||
|
||||
export const allOperators: OperatorDef[] = [
|
||||
{ name: 'equals', operator: '$eq', alias: '_eq' },
|
||||
{ name: 'not equals', operator: '$ne', alias: '_neq' },
|
||||
{ name: 'in', operator: '$in', alias: '_in', default: '[]' },
|
||||
{ name: 'not in', operator: '$nin', alias: '_nin', default: '[]' },
|
||||
{ name: '>', operator: '$gt', alias: '_gt' },
|
||||
{ name: '<', operator: '$lt', alias: '_lt' },
|
||||
{ name: '>=', operator: '$gte', alias: '_gte' },
|
||||
{ name: '<=', operator: '$lte', alias: '_lte' },
|
||||
{ name: 'like', operator: '$like', alias: '_like', default: '%%' },
|
||||
{
|
||||
name: 'not like',
|
||||
operator: '$nlike',
|
||||
alias: '_nlike',
|
||||
default: '%%',
|
||||
},
|
||||
{
|
||||
name: 'like (case-insensitive)',
|
||||
operator: '$ilike',
|
||||
alias: '_ilike',
|
||||
default: '%%',
|
||||
},
|
||||
{
|
||||
name: 'not like (case-insensitive)',
|
||||
operator: '$nilike',
|
||||
alias: '_nilike',
|
||||
default: '%%',
|
||||
},
|
||||
{ name: 'similar', operator: '$similar', alias: '_similar' },
|
||||
{ name: 'not similar', operator: '$nsimilar', alias: '_nsimilar' },
|
||||
];
|
||||
|
||||
export const getOperatorDefaultValue = (op: Operator) => {
|
||||
const operator = allOperators.find(o => o.operator === op);
|
||||
return operator ? operator.default : '';
|
||||
};
|
||||
|
||||
export const parseFilter = (f: Filter): any => {
|
||||
switch (f.kind) {
|
||||
case 'value':
|
||||
return f.operator
|
||||
? {
|
||||
[f.key]: {
|
||||
[f.operator]: f.value,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
break;
|
||||
|
||||
case 'relationship':
|
||||
return {
|
||||
[f.key]: parseFilter(f.value),
|
||||
};
|
||||
break;
|
||||
case 'operator':
|
||||
return {
|
||||
[f.key]: f.value.map(opFilter => parseFilter(opFilter)),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return parseFilter(f);
|
||||
break;
|
||||
}
|
||||
};
|
@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Headers.scss';
|
||||
import DropdownButton from '../DropdownButton/DropdownButton';
|
||||
import { addPlaceholderHeader } from './utils';
|
||||
|
||||
const Headers = ({ headers, setHeaders }) => {
|
||||
return headers.map(({ name, value, type }, i) => {
|
||||
const setHeaderType = e => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].type = e.target.getAttribute('value');
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderKey = e => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].name = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderValue = e => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].value = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const removeHeader = () => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
setHeaders([...newHeaders.slice(0, i), ...newHeaders.slice(i + 1)]);
|
||||
};
|
||||
|
||||
const getHeaderNameInput = () => {
|
||||
return (
|
||||
<input
|
||||
value={name}
|
||||
onChange={setHeaderKey}
|
||||
placeholder="key"
|
||||
className={`form-control ${styles.add_mar_right} ${styles.headerInputWidth}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getHeaderValueInput = () => {
|
||||
return (
|
||||
<div className={styles.headerInputWidth}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'Value', value: 'static' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={type === 'env' ? 'From env var' : 'Value'}
|
||||
dataKey={type === 'env' ? 'env' : 'static'}
|
||||
onButtonChange={setHeaderType}
|
||||
onInputChange={setHeaderValue}
|
||||
required
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={value}
|
||||
id={`header-value-${i}`}
|
||||
inputPlaceHolder={type === 'env' ? 'HEADER_FROM_ENV' : 'value'}
|
||||
testId={`header-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getRemoveButton = () => {
|
||||
if (i === headers.length - 1) return null;
|
||||
return (
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||
onClick={removeHeader}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
|
||||
key={i}
|
||||
>
|
||||
{getHeaderNameInput()}
|
||||
{getHeaderValueInput()}
|
||||
{getRemoveButton()}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Headers;
|
95
console/src/components/Common/Headers/Headers.tsx
Normal file
95
console/src/components/Common/Headers/Headers.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import styles from './Headers.scss';
|
||||
import DropdownButton from '../DropdownButton/DropdownButton';
|
||||
import { addPlaceholderHeader } from './utils';
|
||||
|
||||
export type Header = {
|
||||
type: 'static' | 'env';
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const defaultHeader: Header = {
|
||||
name: '',
|
||||
type: 'static',
|
||||
value: '',
|
||||
};
|
||||
|
||||
interface HeadersListProps extends React.ComponentProps<'div'> {
|
||||
headers: Header[];
|
||||
setHeaders: (h: Header[]) => void;
|
||||
}
|
||||
|
||||
const Headers: React.FC<HeadersListProps> = ({ headers, setHeaders }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{headers.map(({ name, value, type }, i) => {
|
||||
const setHeaderType = (e: React.BaseSyntheticEvent) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].type = e.target.getAttribute('value');
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderKey = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].name = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const setHeaderValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
newHeaders[i].value = e.target.value;
|
||||
addPlaceholderHeader(newHeaders);
|
||||
setHeaders(newHeaders);
|
||||
};
|
||||
|
||||
const removeHeader = () => {
|
||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||
setHeaders([...newHeaders.slice(0, i), ...newHeaders.slice(i + 1)]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
|
||||
key={i.toString()}
|
||||
>
|
||||
<input
|
||||
value={name}
|
||||
onChange={setHeaderKey}
|
||||
placeholder="key"
|
||||
className={`form-control ${styles.add_mar_right} ${styles.headerInputWidth}`}
|
||||
/>
|
||||
<div className={styles.headerInputWidth}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'Value', value: 'static' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={type === 'env' ? 'From env var' : 'Value'}
|
||||
dataKey={type === 'env' ? 'env' : 'static'}
|
||||
onButtonChange={setHeaderType}
|
||||
onInputChange={setHeaderValue}
|
||||
required={false}
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={value}
|
||||
id={`header-value-${i}`}
|
||||
inputPlaceHolder={type === 'env' ? 'HEADER_FROM_ENV' : 'value'}
|
||||
testId={`header-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
{i < headers.length - 1 ? (
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||
onClick={removeHeader}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Headers;
|
@ -1,13 +1,11 @@
|
||||
const emptyHeader = {
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'static',
|
||||
};
|
||||
import { Header as HeaderClient, defaultHeader } from './Headers';
|
||||
import { Header as HeaderServer } from '../utils/v1QueryUtils';
|
||||
|
||||
export const transformHeaders = (headers = []) => {
|
||||
export const transformHeaders = (headers_?: HeaderClient[]) => {
|
||||
const headers = headers_ || [];
|
||||
return headers
|
||||
.map(h => {
|
||||
const transformedHeader = {
|
||||
const transformedHeader: HeaderServer = {
|
||||
name: h.name,
|
||||
};
|
||||
if (h.type === 'static') {
|
||||
@ -20,24 +18,24 @@ export const transformHeaders = (headers = []) => {
|
||||
.filter(h => !!h.name && (!!h.value || !!h.value_from_env));
|
||||
};
|
||||
|
||||
export const addPlaceholderHeader = newHeaders => {
|
||||
export const addPlaceholderHeader = (newHeaders: HeaderClient[]) => {
|
||||
if (newHeaders.length) {
|
||||
const lastHeader = newHeaders[newHeaders.length - 1];
|
||||
if (lastHeader.name && lastHeader.value) {
|
||||
newHeaders.push(emptyHeader);
|
||||
newHeaders.push(defaultHeader);
|
||||
}
|
||||
} else {
|
||||
newHeaders.push(emptyHeader);
|
||||
newHeaders.push(defaultHeader);
|
||||
}
|
||||
return newHeaders;
|
||||
};
|
||||
|
||||
export const parseServerHeaders = (headers = []) => {
|
||||
export const parseServerHeaders = (headers: HeaderServer[] = []) => {
|
||||
return addPlaceholderHeader(
|
||||
headers.map(h => {
|
||||
const parsedHeader = {
|
||||
const parsedHeader: HeaderClient = {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
value: h.value || '',
|
||||
type: 'static',
|
||||
};
|
||||
if (h.value_from_env) {
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from '../Common.scss';
|
||||
|
||||
const Check = ({ className }) => {
|
||||
const Check = ({ className, title = '' }) => {
|
||||
return (
|
||||
<i
|
||||
className={`fa fa-check ${styles.iconCheck} ${className}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
13
console/src/components/Common/Icons/Clock.js
Normal file
13
console/src/components/Common/Icons/Clock.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
const Clock = ({ className, title = '' }) => {
|
||||
return (
|
||||
<i
|
||||
className={`fa fa-clock-o ${className || ''}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clock;
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from '../Common.scss';
|
||||
|
||||
const Cross = ({ className }) => {
|
||||
const Cross = ({ className, title = '' }) => {
|
||||
return (
|
||||
<i
|
||||
className={`fa fa-times ${styles.iconCross} ${className}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
13
console/src/components/Common/Icons/Invalid.js
Normal file
13
console/src/components/Common/Icons/Invalid.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
const Invalid = ({ className, title = '' }) => {
|
||||
return (
|
||||
<i
|
||||
className={`fa fa-exclamation ${className || ''}`}
|
||||
aria-hidden="true"
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Invalid;
|
7
console/src/components/Common/Icons/Reload.js
Normal file
7
console/src/components/Common/Icons/Reload.js
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const Reload = ({ className }) => {
|
||||
return <i className={`fa fa-repeat ${className || ''}`} aria-hidden="true" />;
|
||||
};
|
||||
|
||||
export default Reload;
|
@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
class BreadCrumb extends React.Component {
|
||||
render() {
|
||||
const { breadCrumbs } = this.props;
|
||||
const styles = require('../../TableCommon/Table.scss');
|
||||
|
||||
let bC = null;
|
||||
|
||||
if (breadCrumbs && breadCrumbs.length > 0) {
|
||||
bC = breadCrumbs.map((b, i) => {
|
||||
let bCElem;
|
||||
|
||||
const Sp = () => {
|
||||
const space = ' ';
|
||||
return space;
|
||||
};
|
||||
|
||||
const addArrow = () => [
|
||||
<Sp key={'breadcrumb-space-before' + i} />,
|
||||
<i key={'l' + i} className="fa fa-angle-right" aria-hidden="true" />,
|
||||
<Sp key={'breadcrumb-space-after' + i} />,
|
||||
];
|
||||
|
||||
const isLastElem = i === breadCrumbs.length - 1;
|
||||
|
||||
if (!isLastElem) {
|
||||
bCElem = [
|
||||
<Link key={'l' + i} to={`${b.url}`}>
|
||||
{b.title}
|
||||
</Link>,
|
||||
addArrow(),
|
||||
];
|
||||
} else {
|
||||
bCElem = [b.title];
|
||||
}
|
||||
|
||||
return bCElem;
|
||||
});
|
||||
}
|
||||
|
||||
return <div className={styles.dataBreadCrumb}>You are here: {bC}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
BreadCrumb.propTypes = {
|
||||
breadCrumbs: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default BreadCrumb;
|
@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from '../../TableCommon/Table.scss';
|
||||
|
||||
export type BreadCrumb = {
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
breadCrumbs: BreadCrumb[];
|
||||
};
|
||||
|
||||
const BreadCrumb: React.FC<Props> = ({ breadCrumbs }) => {
|
||||
let bC = null;
|
||||
|
||||
if (breadCrumbs && breadCrumbs.length > 0) {
|
||||
bC = breadCrumbs.map((b: BreadCrumb, i: number) => {
|
||||
let bCElem;
|
||||
|
||||
const addArrow = () => (
|
||||
<React.Fragment>
|
||||
|
||||
<i
|
||||
key={`${b.title}-arrow`}
|
||||
className="fa fa-angle-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const isLastElem = i === breadCrumbs.length - 1;
|
||||
|
||||
if (!isLastElem) {
|
||||
bCElem = [
|
||||
<Link key={`bc-title-${b.title}`} to={`${b.url}`}>
|
||||
{b.title}
|
||||
</Link>,
|
||||
addArrow(),
|
||||
];
|
||||
} else {
|
||||
bCElem = [b.title];
|
||||
}
|
||||
|
||||
return bCElem;
|
||||
});
|
||||
}
|
||||
|
||||
return <div className={styles.dataBreadCrumb}>You are here: {bC}</div>;
|
||||
};
|
||||
|
||||
export default BreadCrumb;
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import BreadCrumb from '../BreadCrumb/BreadCrumb';
|
||||
import Tabs from '../ReusableTabs/ReusableTabs';
|
||||
|
||||
class CommonTabLayout extends React.Component {
|
||||
render() {
|
||||
const styles = require('./CommonTabLayout.scss');
|
||||
const {
|
||||
breadCrumbs,
|
||||
heading,
|
||||
appPrefix,
|
||||
currentTab,
|
||||
tabsInfo,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.subHeader}>
|
||||
<BreadCrumb breadCrumbs={breadCrumbs} />
|
||||
<h2 className={styles.heading_text + ' ' + styles.set_line_height}>
|
||||
{heading || ''}
|
||||
</h2>
|
||||
<Tabs
|
||||
appPrefix={appPrefix}
|
||||
tabName={currentTab}
|
||||
tabsInfo={tabsInfo}
|
||||
baseUrl={baseUrl}
|
||||
showLoader={showLoader}
|
||||
testPrefix={testPrefix}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommonTabLayout;
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import BreadCrumb, {
|
||||
BreadCrumb as BreadCrumbType,
|
||||
} from '../BreadCrumb/BreadCrumb';
|
||||
import Tabs, { Tabs as TabsType } from '../ReusableTabs/ReusableTabs';
|
||||
import styles from './CommonTabLayout.scss';
|
||||
|
||||
type Props = {
|
||||
breadCrumbs: BreadCrumbType[];
|
||||
heading: React.ReactNode;
|
||||
appPrefix: string;
|
||||
currentTab: string;
|
||||
tabsInfo: TabsType;
|
||||
baseUrl: string;
|
||||
showLoader: boolean;
|
||||
testPrefix: string;
|
||||
};
|
||||
|
||||
const CommonTabLayout: React.FC<Props> = props => {
|
||||
const {
|
||||
breadCrumbs,
|
||||
heading,
|
||||
appPrefix,
|
||||
currentTab,
|
||||
tabsInfo,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.subHeader}>
|
||||
<BreadCrumb breadCrumbs={breadCrumbs} />
|
||||
<h2 className={`${styles.heading_text} ${styles.set_line_height}`}>
|
||||
{heading || ''}
|
||||
</h2>
|
||||
<Tabs
|
||||
appPrefix={appPrefix}
|
||||
tabName={currentTab}
|
||||
tabsInfo={tabsInfo}
|
||||
baseUrl={baseUrl}
|
||||
showLoader={showLoader}
|
||||
testPrefix={testPrefix}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommonTabLayout;
|
@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
class LeftContainer extends React.Component {
|
||||
render() {
|
||||
const styles = require('../../TableCommon/Table.scss');
|
||||
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.pageSidebar + ' col-xs-12 ' + styles.padd_remove}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LeftContainer;
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from '../../TableCommon/Table.scss';
|
||||
|
||||
const LeftContainer: React.FC = ({ children }) => {
|
||||
return (
|
||||
<div className={`${styles.pageSidebar} col-xs-12 ${styles.padd_remove}`}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftContainer;
|
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './LeftSubSidebar.scss';
|
||||
|
||||
interface LeftSidebarItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LeftSidebarSectionProps extends React.ComponentProps<'div'> {
|
||||
items: LeftSidebarItem[];
|
||||
currentItem?: LeftSidebarItem;
|
||||
getServiceEntityLink: (s: string) => string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
const getLeftSidebarSection = ({
|
||||
items = [],
|
||||
currentItem,
|
||||
service,
|
||||
getServiceEntityLink,
|
||||
}: LeftSidebarSectionProps) => {
|
||||
// TODO needs refactor to accomodate other services
|
||||
|
||||
const [searchText, setSearchText] = React.useState('');
|
||||
|
||||
const getSearchInput = () => {
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchText(e.target.value);
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
onChange={handleSearch}
|
||||
className="form-control"
|
||||
placeholder={`search ${service}`}
|
||||
data-test={`search-${service}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO test search
|
||||
let itemList: LeftSidebarItem[] = [];
|
||||
if (searchText) {
|
||||
const secondaryResults: LeftSidebarItem[] = [];
|
||||
items.forEach(a => {
|
||||
if (a.name.startsWith(searchText)) {
|
||||
itemList.push(a);
|
||||
} else if (a.name.includes(searchText)) {
|
||||
secondaryResults.push(a);
|
||||
}
|
||||
});
|
||||
itemList = [...itemList, ...secondaryResults];
|
||||
} else {
|
||||
itemList = [...items];
|
||||
}
|
||||
|
||||
const getChildList = () => {
|
||||
let childList;
|
||||
if (itemList.length === 0) {
|
||||
childList = (
|
||||
<li className={styles.noChildren} data-test="sidebar-no-services">
|
||||
<i>No {service} available</i>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
childList = itemList.map(a => {
|
||||
let activeTableClass = '';
|
||||
if (currentItem && currentItem.name === a.name) {
|
||||
activeTableClass = styles.activeLink;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={activeTableClass}
|
||||
key={a.name}
|
||||
data-test={`action-sidebar-links-${a.name}`}
|
||||
>
|
||||
<Link to={getServiceEntityLink(a.name)} data-test={a.name}>
|
||||
<i
|
||||
className={`${styles.tableIcon} fa fa-wrench`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{a.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return childList;
|
||||
};
|
||||
|
||||
return {
|
||||
getChildList,
|
||||
getSearchInput,
|
||||
count: itemList.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default getLeftSidebarSection;
|
@ -1,86 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import Button from '../../Button/Button';
|
||||
|
||||
class LeftSubSidebar extends React.Component {
|
||||
render() {
|
||||
const styles = require('./LeftSubSidebar.scss');
|
||||
|
||||
const {
|
||||
showAddBtn,
|
||||
searchInput,
|
||||
heading,
|
||||
addLink,
|
||||
addLabel,
|
||||
addTestString,
|
||||
children,
|
||||
childListTestString,
|
||||
} = this.props;
|
||||
|
||||
const getAddButton = () => {
|
||||
let addButton = null;
|
||||
|
||||
if (showAddBtn) {
|
||||
addButton = (
|
||||
<div
|
||||
className={
|
||||
'col-xs-4 text-center ' +
|
||||
styles.padd_left_remove +
|
||||
' ' +
|
||||
styles.sidebarCreateTable
|
||||
}
|
||||
>
|
||||
<Link className={styles.padd_remove_full} to={addLink}>
|
||||
<Button size="xs" color="white" data-test={addTestString}>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return addButton;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.subSidebarList}>
|
||||
<div className={styles.display_flex + ' ' + styles.padd_top_medium}>
|
||||
<div
|
||||
className={
|
||||
styles.sidebarSearch +
|
||||
' form-group col-xs-12 ' +
|
||||
styles.padd_remove
|
||||
}
|
||||
>
|
||||
<i className="fa fa-search" aria-hidden="true" />
|
||||
{searchInput}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.sidebarHeadingWrapper}>
|
||||
<div
|
||||
className={
|
||||
'col-xs-8 ' +
|
||||
styles.sidebarHeading +
|
||||
' ' +
|
||||
styles.padd_left_remove
|
||||
}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
{getAddButton()}
|
||||
</div>
|
||||
<ul
|
||||
className={styles.subSidebarListUL}
|
||||
data-test={childListTestString}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LeftSubSidebar;
|
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import Button from '../../Button/Button';
|
||||
import styles from './LeftSubSidebar.scss';
|
||||
|
||||
interface Props extends React.ComponentProps<'div'> {
|
||||
showAddBtn: boolean;
|
||||
searchInput: React.ReactNode;
|
||||
heading: string;
|
||||
addLink: string;
|
||||
addLabel: string;
|
||||
addTestString: string;
|
||||
childListTestString: string;
|
||||
}
|
||||
|
||||
const LeftSubSidebar: React.FC<Props> = props => {
|
||||
const {
|
||||
showAddBtn,
|
||||
searchInput,
|
||||
heading,
|
||||
addLink,
|
||||
addLabel,
|
||||
addTestString,
|
||||
children,
|
||||
childListTestString,
|
||||
} = props;
|
||||
|
||||
const getAddButton = () => {
|
||||
let addButton = null;
|
||||
|
||||
if (showAddBtn) {
|
||||
addButton = (
|
||||
<div
|
||||
className={`col-xs-4 text-center ${styles.padd_left_remove} ${styles.sidebarCreateTable}`}
|
||||
>
|
||||
<Link className={styles.padd_remove_full} to={addLink}>
|
||||
<Button size="xs" color="white" data-test={addTestString}>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return addButton;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.subSidebarList}>
|
||||
<div className={`${styles.display_flex} ${styles.padd_top_medium}`}>
|
||||
<div
|
||||
className={`${styles.sidebarSearch} form-group col-xs-12 ${styles.padd_remove}`}
|
||||
>
|
||||
<i className="fa fa-search" aria-hidden="true" />
|
||||
{searchInput}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.sidebarHeadingWrapper}>
|
||||
<div
|
||||
className={`col-xs-8 ${styles.sidebarHeading} ${styles.padd_left_remove}`}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
{getAddButton()}
|
||||
</div>
|
||||
<ul className={styles.subSidebarListUL} data-test={childListTestString}>
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSubSidebar;
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
class PageContainer extends React.Component {
|
||||
render() {
|
||||
const styles = require('../../Common.scss');
|
||||
|
||||
const { helmet, leftContainer, children } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet title={helmet} />
|
||||
<div className={styles.wd20 + ' ' + styles.align_left}>
|
||||
{leftContainer}
|
||||
</div>
|
||||
<div className={styles.wd80}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PageContainer;
|
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import styles from '../../Common.scss';
|
||||
|
||||
interface PageContainerProps extends React.ComponentProps<'div'> {
|
||||
helmet: string;
|
||||
leftContainer: React.ReactNode;
|
||||
}
|
||||
|
||||
const PageContainer: React.FC<PageContainerProps> = ({
|
||||
helmet,
|
||||
leftContainer,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Helmet title={helmet} />
|
||||
<div className={`${styles.wd20} ${styles.align_left}`}>
|
||||
{leftContainer}
|
||||
</div>
|
||||
<div className={styles.wd80}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageContainer;
|
@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
const Tabs = ({
|
||||
appPrefix,
|
||||
tabsInfo,
|
||||
tabName,
|
||||
count,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
}) => {
|
||||
let showCount = '';
|
||||
if (!(count === null || count === undefined)) {
|
||||
showCount = '(' + count + ')';
|
||||
}
|
||||
const styles = require('./ReusableTabs.scss');
|
||||
const dataLoader = () => {
|
||||
return (
|
||||
<span className={styles.loader_ml}>
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
return [
|
||||
<div className={styles.common_nav} key={'reusable-tabs-1'}>
|
||||
<ul className="nav nav-pills">
|
||||
{Object.keys(tabsInfo).map((t, i) => (
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === t ? styles.active : ''}
|
||||
key={i}
|
||||
>
|
||||
<Link
|
||||
to={`${baseUrl}/${t}`}
|
||||
data-test={`${
|
||||
testPrefix ? testPrefix + '-' : ''
|
||||
}${appPrefix.slice(1)}-${t}`}
|
||||
>
|
||||
{tabsInfo[t].display_text} {tabName === t ? showCount : null}
|
||||
{tabName === t && showLoader ? dataLoader() : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>,
|
||||
<div className="clearfix" key={'reusable-tabs-2'} />,
|
||||
];
|
||||
};
|
||||
|
||||
export default Tabs;
|
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './ReusableTabs.scss';
|
||||
|
||||
export type Tabs = Record<string, { display_text: string }>;
|
||||
|
||||
type Props = {
|
||||
appPrefix: string;
|
||||
tabsInfo: Tabs;
|
||||
tabName: string;
|
||||
count?: number;
|
||||
baseUrl: string;
|
||||
showLoader: boolean;
|
||||
testPrefix: string;
|
||||
};
|
||||
|
||||
const Tabs: React.FC<Props> = ({
|
||||
appPrefix,
|
||||
tabsInfo,
|
||||
tabName,
|
||||
count,
|
||||
baseUrl,
|
||||
showLoader,
|
||||
testPrefix,
|
||||
}) => {
|
||||
let showCount = '';
|
||||
if (!(count === null || count === undefined)) {
|
||||
showCount = `(${count})`;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={styles.common_nav} key="reusable-tabs-1">
|
||||
<ul className="nav nav-pills">
|
||||
{Object.keys(tabsInfo).map((t: string) => (
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === t ? styles.active : ''}
|
||||
key={t}
|
||||
>
|
||||
<Link
|
||||
to={`${baseUrl}/${t}`}
|
||||
data-test={`${
|
||||
testPrefix ? `${testPrefix}-` : ''
|
||||
}${appPrefix.slice(1)}-${t}`}
|
||||
>
|
||||
{tabsInfo[t].display_text} {tabName === t ? showCount : null}
|
||||
{tabName === t && showLoader ? (
|
||||
<span className={styles.loader_ml}>
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="clearfix" key="reusable-tabs-2" />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const RightContainer = ({ children }) => {
|
||||
const styles = require('./RightContainer.scss');
|
||||
|
||||
return (
|
||||
<div className={styles.container + ' container-fluid'}>
|
||||
<div className="row">
|
||||
<div
|
||||
className={
|
||||
styles.main + ' ' + styles.padd_left_remove + ' ' + styles.padd_top
|
||||
}
|
||||
>
|
||||
<div className={styles.rightBar + ' '}>
|
||||
{children && React.cloneElement(children)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
schema: state.tables.allSchemas,
|
||||
};
|
||||
};
|
||||
|
||||
const rightContainerConnector = connect =>
|
||||
connect(mapStateToProps)(RightContainer);
|
||||
|
||||
export default rightContainerConnector;
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Connect } from 'react-redux';
|
||||
import styles from './RightContainer.scss';
|
||||
|
||||
const RightContainer: React.FC = ({ children }) => {
|
||||
return (
|
||||
<div className={`${styles.container} container-fluid`}>
|
||||
<div className="row">
|
||||
<div
|
||||
className={`${styles.main} ${styles.padd_left_remove} ${styles.padd_top}`}
|
||||
>
|
||||
<div className={`${styles.rightBar} `}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightContainerConnector = (connect: Connect) => connect()(RightContainer);
|
||||
|
||||
export default rightContainerConnector;
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Spinner = ({ className = '' }) => {
|
||||
const styles = require('./Spinner.scss');
|
||||
|
||||
return (
|
||||
<div className={styles.sk_circle + ' ' + className}>
|
||||
<div className={styles.sk_circle1 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle2 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle3 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle4 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle5 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle6 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle7 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle8 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle9 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle10 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle11 + ' ' + styles.sk_child} />
|
||||
<div className={styles.sk_circle12 + ' ' + styles.sk_child} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
27
console/src/components/Common/Spinner/Spinner.tsx
Normal file
27
console/src/components/Common/Spinner/Spinner.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import styles from './Spinner.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Spinner: React.FC<Props> = ({ className = '' }) => {
|
||||
return (
|
||||
<div className={`${styles.sk_circle} ${className}`}>
|
||||
<div className={`${styles.sk_circle1} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle2} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle3} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle4} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle5} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle6} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle7} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle8} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle9} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle10} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle11} ${styles.sk_child}`} />
|
||||
<div className={`${styles.sk_circle12} ${styles.sk_child}`} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
@ -150,3 +150,22 @@
|
||||
.ReactTable .rt-thead [role='columnheader'] {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Event Trigger */
|
||||
|
||||
.ReactTable .rt-table .rt-thead .rt-tr .rt-th:first-child,
|
||||
.ReactTable .rt-table .rt-tbody .rt-tr-group .rt-tr.-odd .rt-td:first-child,
|
||||
.ReactTable .rt-table .rt-tbody .rt-tr-group .rt-tr.-even .rt-td:first-child {
|
||||
min-width: 75px !important;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody .rt-th,
|
||||
.ReactTable .rt-tbody .rt-td {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ReactTable .rt-thead .rt-resizable-header-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -3,11 +3,17 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import styles from './Tooltip.scss';
|
||||
|
||||
const tooltipGen = message => {
|
||||
const tooltipGen = (message: string) => {
|
||||
return <Tooltip id={message}>{message}</Tooltip>;
|
||||
};
|
||||
|
||||
const ToolTip = ({ message, placement = 'right' }) => (
|
||||
export interface TooltipProps extends React.ComponentProps<'i'> {
|
||||
message: string;
|
||||
placement?: 'right' | 'left' | 'top' | 'bottom';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ToolTip: React.FC<TooltipProps> = ({ message, placement = 'right' }) => (
|
||||
<OverlayTrigger placement={placement} overlay={tooltipGen(message)}>
|
||||
<i
|
||||
className={`fa fa-question-circle + ${styles.tooltipIcon}`}
|
@ -1,190 +1,193 @@
|
||||
// TODO: make functions from this file available without imports
|
||||
import moment from 'moment';
|
||||
|
||||
// TODO: make functions from this file available without imports
|
||||
/* TYPE utils */
|
||||
|
||||
export const isNotDefined = value => {
|
||||
export const isNotDefined = (value: unknown) => {
|
||||
return value === null || value === undefined;
|
||||
};
|
||||
|
||||
export const exists = value => {
|
||||
/*
|
||||
* Deprecated: Use "isNull" instead
|
||||
*/
|
||||
export const exists = (value: unknown) => {
|
||||
return value !== null && value !== undefined;
|
||||
};
|
||||
|
||||
export const isArray = value => {
|
||||
export const isArray = (value: unknown) => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
export const isObject = value => {
|
||||
export const isObject = (value: unknown) => {
|
||||
return typeof value === 'object' && value !== null;
|
||||
};
|
||||
|
||||
export const isString = value => {
|
||||
export const isString = (value: unknown) => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isNumber = value => {
|
||||
export const isNumber = (value: unknown) => {
|
||||
return typeof value === 'number';
|
||||
};
|
||||
|
||||
export const isFloat = n => {
|
||||
return typeof value === 'number' && n % 1 !== 0;
|
||||
export const isFloat = (value: unknown) => {
|
||||
return typeof value === 'number' && value % 1 !== 0;
|
||||
};
|
||||
|
||||
export const isBoolean = value => {
|
||||
export const isBoolean = (value: unknown) => {
|
||||
return typeof value === 'boolean';
|
||||
};
|
||||
|
||||
export const isPromise = value => {
|
||||
export const isPromise = (value: any) => {
|
||||
if (!value) return false;
|
||||
return value.constructor.name === 'Promise';
|
||||
};
|
||||
|
||||
export const isValidTemplateLiteral = literal_ => {
|
||||
export const isValidURL = (value: string) => {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isValidTemplateLiteral = (literal_: string) => {
|
||||
const literal = literal_.trim();
|
||||
if (!literal) return false;
|
||||
const templateStartIndex = literal.indexOf('{{');
|
||||
const templateEndEdex = literal.indexOf('}}');
|
||||
return (
|
||||
templateStartIndex !== '-1' && templateEndEdex > templateStartIndex + 2
|
||||
);
|
||||
return templateStartIndex !== -1 && templateEndEdex > templateStartIndex + 2;
|
||||
};
|
||||
|
||||
export const isJsonString = str => {
|
||||
export const isValidDate = (date: Date) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
date.toISOString();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isEmpty = value => {
|
||||
let _isEmpty = false;
|
||||
export const isEmpty = (value: any) => {
|
||||
let empty = false;
|
||||
|
||||
if (!exists(value)) {
|
||||
_isEmpty = true;
|
||||
empty = true;
|
||||
} else if (isArray(value)) {
|
||||
_isEmpty = value.length === 0;
|
||||
empty = value.length === 0;
|
||||
} else if (isObject(value)) {
|
||||
_isEmpty = JSON.stringify(value) === JSON.stringify({});
|
||||
empty = JSON.stringify(value) === JSON.stringify({});
|
||||
} else if (isString(value)) {
|
||||
_isEmpty = value === '';
|
||||
empty = value === '';
|
||||
}
|
||||
|
||||
return _isEmpty;
|
||||
return empty;
|
||||
};
|
||||
|
||||
export const isEqual = (value1, value2) => {
|
||||
let _isEqual = false;
|
||||
export const isEqual = (value1: any, value2: any) => {
|
||||
let equal = false;
|
||||
|
||||
if (typeof value1 === typeof value2) {
|
||||
if (isArray(value1)) {
|
||||
_isEqual = JSON.stringify(value1) === JSON.stringify(value2);
|
||||
equal = JSON.stringify(value1) === JSON.stringify(value2);
|
||||
} else if (isObject(value2)) {
|
||||
const value1Keys = Object.keys(value1);
|
||||
const value2Keys = Object.keys(value2);
|
||||
|
||||
if (value1Keys.length === value2Keys.length) {
|
||||
_isEqual = true;
|
||||
equal = true;
|
||||
|
||||
for (let i = 0; i < value1Keys.length; i++) {
|
||||
const key = value1Keys[i];
|
||||
if (!isEqual(value1[key], value2[key])) {
|
||||
_isEqual = false;
|
||||
equal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_isEqual = value1 === value2;
|
||||
equal = value1 === value2;
|
||||
}
|
||||
}
|
||||
|
||||
return _isEqual;
|
||||
return equal;
|
||||
};
|
||||
|
||||
export function isJsonString(str: string) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/* ARRAY utils */
|
||||
|
||||
export const getLastArrayElement = array => {
|
||||
if (!array) return null;
|
||||
if (!array.length) return null;
|
||||
return array[array.length - 1];
|
||||
};
|
||||
|
||||
export const getFirstArrayElement = array => {
|
||||
if (!array) return null;
|
||||
return array[0];
|
||||
};
|
||||
|
||||
export const deleteArrayElementAtIndex = (array, index) => {
|
||||
export const deleteArrayElementAtIndex = (array: unknown[], index: number) => {
|
||||
return array.splice(index, 1);
|
||||
};
|
||||
|
||||
export const arrayDiff = (arr1, arr2) => {
|
||||
export const arrayDiff = (arr1: unknown[], arr2: unknown[]) => {
|
||||
return arr1.filter(v => !arr2.includes(v));
|
||||
};
|
||||
|
||||
/* JSON utils */
|
||||
|
||||
export const getAllJsonPaths = (json, leafKeys = [], prefix = '') => {
|
||||
const _paths = [];
|
||||
export function getAllJsonPaths(json: any, leafKeys: any[], prefix = '') {
|
||||
const paths = [];
|
||||
|
||||
const addPrefix = subPath => {
|
||||
const addPrefix = (subPath: string) => {
|
||||
return prefix + (prefix && subPath ? '.' : '') + subPath;
|
||||
};
|
||||
|
||||
const handleSubJson = (subJson, newPrefix) => {
|
||||
const handleSubJson = (subJson: any, newPrefix: string) => {
|
||||
const subPaths = getAllJsonPaths(subJson, leafKeys, newPrefix);
|
||||
|
||||
subPaths.forEach(subPath => {
|
||||
_paths.push(subPath);
|
||||
paths.push(subPath);
|
||||
});
|
||||
|
||||
if (!subPaths.length) {
|
||||
_paths.push(newPrefix);
|
||||
paths.push(newPrefix);
|
||||
}
|
||||
};
|
||||
|
||||
if (isArray(json)) {
|
||||
json.forEach((subJson, i) => {
|
||||
json.forEach((subJson: any, i: number) => {
|
||||
handleSubJson(subJson, addPrefix(i.toString()));
|
||||
});
|
||||
} else if (isObject(json)) {
|
||||
Object.keys(json).forEach(key => {
|
||||
if (leafKeys.includes(key)) {
|
||||
_paths.push({ [addPrefix(key)]: json[key] });
|
||||
paths.push({ [addPrefix(key)]: json[key] });
|
||||
} else {
|
||||
handleSubJson(json[key], addPrefix(key));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_paths.push(addPrefix(json));
|
||||
paths.push(addPrefix(json));
|
||||
}
|
||||
|
||||
return _paths;
|
||||
};
|
||||
return paths;
|
||||
}
|
||||
|
||||
/* TRANSFORM utils*/
|
||||
/* TRANSFORM utils */
|
||||
|
||||
export const capitalize = s => {
|
||||
export const capitalize = (s: string) => {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
// return number with commas for readability
|
||||
export const getReadableNumber = number => {
|
||||
if (!isNumber(number)) return number;
|
||||
|
||||
export const getReadableNumber = (number: number) => {
|
||||
return number.toLocaleString();
|
||||
};
|
||||
|
||||
/* URL utils */
|
||||
|
||||
export const getUrlSearchParamValue = param => {
|
||||
export const getUrlSearchParamValue = (param: string) => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
return urlSearchParams.get(param);
|
||||
};
|
||||
|
||||
/* ALERT utils */
|
||||
|
||||
// use browser confirm and prompt to get user confirmation for actions
|
||||
@ -205,14 +208,14 @@ export const getConfirmation = (
|
||||
}
|
||||
|
||||
if (!hardConfirmation) {
|
||||
isConfirmed = confirm(modalContent);
|
||||
isConfirmed = window.confirm(modalContent);
|
||||
} else {
|
||||
modalContent += '\n\n';
|
||||
modalContent += `Type "${confirmationText}" to confirm:`;
|
||||
|
||||
// retry prompt until user cancels or confirmation text matches
|
||||
// prompt returns null on cancel or a string otherwise
|
||||
let promptResponse = '';
|
||||
let promptResponse: string | null = '';
|
||||
while (!isConfirmed && promptResponse !== null) {
|
||||
promptResponse = prompt(modalContent);
|
||||
|
||||
@ -226,14 +229,14 @@ export const getConfirmation = (
|
||||
/* FILE utils */
|
||||
|
||||
export const uploadFile = (
|
||||
fileHandler,
|
||||
fileFormat = null,
|
||||
invalidFileHandler = null,
|
||||
errorCallback = null
|
||||
fileHandler: (s: string | ArrayBufferLike | null) => void,
|
||||
fileFormat: string | null,
|
||||
invalidFileHandler: any,
|
||||
errorCallback?: (title: string, subTitle: string, details?: any) => void
|
||||
) => {
|
||||
const fileInputElement = document.createElement('div');
|
||||
fileInputElement.innerHTML = '<input style="display:none" type="file">';
|
||||
const fileInput = fileInputElement.firstChild;
|
||||
const fileInput: any = fileInputElement.firstChild;
|
||||
document.body.appendChild(fileInputElement);
|
||||
|
||||
const onFileUpload = () => {
|
||||
@ -242,20 +245,18 @@ export const uploadFile = (
|
||||
|
||||
let isValidFile = true;
|
||||
if (fileFormat) {
|
||||
const expectedFileSuffix = '.' + fileFormat;
|
||||
const expectedFileSuffix = `.${fileFormat}`;
|
||||
|
||||
if (!fileName.endsWith(expectedFileSuffix)) {
|
||||
isValidFile = false;
|
||||
|
||||
if (invalidFileHandler) {
|
||||
invalidFileHandler(fileName);
|
||||
} else {
|
||||
if (errorCallback) {
|
||||
errorCallback(
|
||||
'Invalid file format',
|
||||
`Expected a ${expectedFileSuffix} file`
|
||||
);
|
||||
}
|
||||
} else if (errorCallback) {
|
||||
errorCallback(
|
||||
'Invalid file format',
|
||||
`Expected a ${expectedFileSuffix} file`
|
||||
);
|
||||
}
|
||||
|
||||
fileInputElement.remove();
|
||||
@ -283,7 +284,7 @@ export const uploadFile = (
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
export const downloadFile = (fileName, dataString) => {
|
||||
export const downloadFile = (fileName: string, dataString: string) => {
|
||||
const downloadLinkElem = document.createElement('a');
|
||||
downloadLinkElem.setAttribute('href', dataString);
|
||||
downloadLinkElem.setAttribute('download', fileName);
|
||||
@ -295,7 +296,7 @@ export const downloadFile = (fileName, dataString) => {
|
||||
downloadLinkElem.remove();
|
||||
};
|
||||
|
||||
export const downloadObjectAsJsonFile = (fileName, object) => {
|
||||
export const downloadObjectAsJsonFile = (fileName: string, object: any) => {
|
||||
const contentType = 'application/json;charset=utf-8;';
|
||||
|
||||
const jsonSuffix = '.json';
|
||||
@ -303,17 +304,16 @@ export const downloadObjectAsJsonFile = (fileName, object) => {
|
||||
? fileName
|
||||
: fileName + jsonSuffix;
|
||||
|
||||
const dataString =
|
||||
'data:' +
|
||||
contentType +
|
||||
',' +
|
||||
encodeURIComponent(JSON.stringify(object, null, 2));
|
||||
const dataString = `data:${contentType},${encodeURIComponent(
|
||||
JSON.stringify(object, null, 2)
|
||||
)}`;
|
||||
|
||||
downloadFile(fileNameWithSuffix, dataString);
|
||||
};
|
||||
|
||||
export const getFileExtensionFromFilename = filename => {
|
||||
return filename.match(/\.[0-9a-z]+$/i)[0];
|
||||
export const getFileExtensionFromFilename = (filename: string) => {
|
||||
const matches = filename.match(/\.[0-9a-z]+$/i);
|
||||
return matches ? matches[0] : null;
|
||||
};
|
||||
|
||||
// return time in format YYYY_MM_DD_hh_mm_ss_s
|
||||
@ -336,3 +336,7 @@ export const getCurrTimeForFileName = () => {
|
||||
|
||||
return [year, month, day, hours, minutes, seconds, milliSeconds].join('_');
|
||||
};
|
||||
|
||||
export const convertDateTimeToLocale = (dateTime: string) => {
|
||||
return moment(dateTime, moment.ISO_8601).format('ddd, MMM Do HH:mm:ss Z');
|
||||
};
|
@ -1,425 +0,0 @@
|
||||
import React from 'react';
|
||||
import { isEqual, isString } from './jsUtils';
|
||||
|
||||
/*** Table/View utils ***/
|
||||
|
||||
export const getTableName = table => {
|
||||
return table.table_name;
|
||||
};
|
||||
|
||||
export const getTableSchema = table => {
|
||||
return table.table_schema;
|
||||
};
|
||||
|
||||
export const getTableType = table => {
|
||||
return table.table_type;
|
||||
};
|
||||
|
||||
// TODO: figure out better pattern for overloading fns
|
||||
// tableName and tableNameWithSchema are either/or arguments
|
||||
export const generateTableDef = (
|
||||
tableName,
|
||||
tableSchema = 'public',
|
||||
tableNameWithSchema = null
|
||||
) => {
|
||||
if (tableNameWithSchema) {
|
||||
tableSchema = tableNameWithSchema.split('.')[0];
|
||||
tableName = tableNameWithSchema.split('.')[1];
|
||||
}
|
||||
|
||||
return {
|
||||
schema: tableSchema,
|
||||
name: tableName,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTableDef = table => {
|
||||
return generateTableDef(getTableName(table), getTableSchema(table));
|
||||
};
|
||||
|
||||
export const getQualifiedTableDef = tableDef => {
|
||||
return isString(tableDef) ? generateTableDef(tableDef) : tableDef;
|
||||
};
|
||||
|
||||
export const getTableNameWithSchema = (tableDef, wrapDoubleQuotes = false) => {
|
||||
let _fullTableName;
|
||||
|
||||
if (wrapDoubleQuotes) {
|
||||
_fullTableName =
|
||||
'"' + tableDef.schema + '"' + '.' + '"' + tableDef.name + '"';
|
||||
} else {
|
||||
_fullTableName = tableDef.schema + '.' + tableDef.name;
|
||||
}
|
||||
|
||||
return _fullTableName;
|
||||
};
|
||||
|
||||
export const checkIfTable = table => {
|
||||
return table.table_type === 'TABLE';
|
||||
};
|
||||
|
||||
export const displayTableName = table => {
|
||||
const tableName = getTableName(table);
|
||||
const isTable = checkIfTable(table);
|
||||
|
||||
return isTable ? <span>{tableName}</span> : <i>{tableName}</i>;
|
||||
};
|
||||
|
||||
export const findTable = (allTables, tableDef) => {
|
||||
return allTables.find(t => isEqual(getTableDef(t), tableDef));
|
||||
};
|
||||
|
||||
export const getTrackedTables = tables => {
|
||||
return tables.filter(t => t.is_table_tracked);
|
||||
};
|
||||
|
||||
export const getUntrackedTables = tables => {
|
||||
return tables.filter(t => !t.is_table_tracked);
|
||||
};
|
||||
|
||||
export const getOnlyTables = tablesOrViews => {
|
||||
return tablesOrViews.filter(t => checkIfTable(t));
|
||||
};
|
||||
|
||||
export const getOnlyViews = tablesOrViews => {
|
||||
return tablesOrViews.filter(t => !checkIfTable(t));
|
||||
};
|
||||
|
||||
export const QUERY_TYPES = ['insert', 'select', 'update', 'delete'];
|
||||
|
||||
export const getTableSupportedQueries = table => {
|
||||
let supportedQueryTypes;
|
||||
|
||||
if (checkIfTable(table)) {
|
||||
supportedQueryTypes = QUERY_TYPES;
|
||||
} else {
|
||||
// is View
|
||||
supportedQueryTypes = [];
|
||||
|
||||
// Add insert/update permission if it is insertable/updatable as returned by pg
|
||||
if (table.view_info) {
|
||||
if (
|
||||
table.view_info.is_insertable_into === 'YES' ||
|
||||
table.view_info.is_trigger_insertable_into === 'YES'
|
||||
) {
|
||||
supportedQueryTypes.push('insert');
|
||||
}
|
||||
|
||||
supportedQueryTypes.push('select'); // to maintain order
|
||||
|
||||
if (table.view_info.is_updatable === 'YES') {
|
||||
supportedQueryTypes.push('update');
|
||||
supportedQueryTypes.push('delete');
|
||||
} else {
|
||||
if (table.view_info.is_trigger_updatable === 'YES') {
|
||||
supportedQueryTypes.push('update');
|
||||
}
|
||||
|
||||
if (table.view_info.is_trigger_deletable === 'YES') {
|
||||
supportedQueryTypes.push('delete');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
supportedQueryTypes.push('select');
|
||||
}
|
||||
}
|
||||
|
||||
return supportedQueryTypes;
|
||||
};
|
||||
|
||||
/*** Table/View column utils ***/
|
||||
|
||||
export const getTableColumns = table => {
|
||||
return table.columns;
|
||||
};
|
||||
|
||||
export const getColumnName = column => {
|
||||
return column.column_name;
|
||||
};
|
||||
|
||||
export const getTableColumnNames = table => {
|
||||
return getTableColumns(table).map(c => getColumnName(c));
|
||||
};
|
||||
|
||||
export const getTableColumn = (table, columnName) => {
|
||||
return getTableColumns(table).find(
|
||||
column => getColumnName(column) === columnName
|
||||
);
|
||||
};
|
||||
|
||||
export const getColumnType = column => {
|
||||
let _columnType = column.data_type;
|
||||
|
||||
if (_columnType === 'USER-DEFINED') {
|
||||
_columnType = column.udt_name;
|
||||
}
|
||||
|
||||
return _columnType;
|
||||
};
|
||||
|
||||
export const isColumnAutoIncrement = column => {
|
||||
const columnDefault = column.column_default;
|
||||
|
||||
const autoIncrementDefaultRegex = /^nextval\('(.*)_seq'::regclass\)$/;
|
||||
|
||||
return (
|
||||
columnDefault &&
|
||||
columnDefault.match(new RegExp(autoIncrementDefaultRegex, 'gi'))
|
||||
);
|
||||
};
|
||||
|
||||
/*** Table/View relationship utils ***/
|
||||
|
||||
export const getTableRelationships = table => {
|
||||
return table.relationships;
|
||||
};
|
||||
|
||||
export const getRelationshipName = relationship => {
|
||||
return relationship.rel_name;
|
||||
};
|
||||
|
||||
export const getRelationshipDef = relationship => {
|
||||
return relationship.rel_def;
|
||||
};
|
||||
|
||||
export const getRelationshipType = relationship => {
|
||||
return relationship.rel_type;
|
||||
};
|
||||
|
||||
export const getTableRelationshipNames = table => {
|
||||
return getTableRelationships(table).map(r => getRelationshipName(r));
|
||||
};
|
||||
|
||||
export function getTableRelationship(table, relationshipName) {
|
||||
return getTableRelationships(table).find(
|
||||
relationship => getRelationshipName(relationship) === relationshipName
|
||||
);
|
||||
}
|
||||
|
||||
export function getRelationshipRefTable(table, relationship) {
|
||||
let _refTable = null;
|
||||
|
||||
const relationshipDef = getRelationshipDef(relationship);
|
||||
const relationshipType = getRelationshipType(relationship);
|
||||
|
||||
// if manual relationship
|
||||
if (relationshipDef.manual_configuration) {
|
||||
_refTable = relationshipDef.manual_configuration.remote_table;
|
||||
}
|
||||
|
||||
// if foreign-key based relationship
|
||||
if (relationshipDef.foreign_key_constraint_on) {
|
||||
// if array relationship
|
||||
if (relationshipType === 'array') {
|
||||
_refTable = relationshipDef.foreign_key_constraint_on.table;
|
||||
}
|
||||
|
||||
// if object relationship
|
||||
if (relationshipType === 'object') {
|
||||
const fkCol = relationshipDef.foreign_key_constraint_on;
|
||||
|
||||
for (let i = 0; i < table.foreign_key_constraints.length; i++) {
|
||||
const fkConstraint = table.foreign_key_constraints[i];
|
||||
const fkConstraintCol = Object.keys(fkConstraint.column_mapping)[0];
|
||||
if (fkCol === fkConstraintCol) {
|
||||
_refTable = generateTableDef(
|
||||
fkConstraint.ref_table,
|
||||
fkConstraint.ref_table_table_schema
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof _refTable === 'string') {
|
||||
_refTable = generateTableDef(_refTable);
|
||||
}
|
||||
|
||||
return _refTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} currentSchema
|
||||
* @param {string} currentTable
|
||||
* @param {Array<{[key: string]: any}>} allSchemas
|
||||
*
|
||||
* @returns {Array<{
|
||||
* columnName: string,
|
||||
* enumTableName: string,
|
||||
* enumColumnName: string,
|
||||
* }>}
|
||||
*/
|
||||
export const getEnumColumnMappings = (allSchemas, tableName, tableSchema) => {
|
||||
const currentTable = findTable(
|
||||
allSchemas,
|
||||
generateTableDef(tableName, tableSchema)
|
||||
);
|
||||
|
||||
const relationsMap = [];
|
||||
if (!currentTable.foreign_key_constraints.length) return;
|
||||
|
||||
currentTable.foreign_key_constraints.map(
|
||||
({ ref_table, ref_table_table_schema, column_mapping }) => {
|
||||
const refTableSchema = findTable(
|
||||
allSchemas,
|
||||
generateTableDef(ref_table, ref_table_table_schema)
|
||||
);
|
||||
if (!refTableSchema || !refTableSchema.is_enum) return;
|
||||
|
||||
const keys = Object.keys(column_mapping);
|
||||
if (!keys.length) return;
|
||||
|
||||
const _columnName = keys[0];
|
||||
const _enumColumnName = column_mapping[_columnName];
|
||||
|
||||
if (_columnName && _enumColumnName) {
|
||||
relationsMap.push({
|
||||
columnName: _columnName,
|
||||
enumTableName: ref_table,
|
||||
enumColumnName: _enumColumnName,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return relationsMap;
|
||||
};
|
||||
|
||||
/*** Table/View permissions utils ***/
|
||||
|
||||
export const getTablePermissions = (table, role = null, action = null) => {
|
||||
let tablePermissions = table.permissions;
|
||||
|
||||
if (role) {
|
||||
tablePermissions = tablePermissions.find(p => p.role_name === role);
|
||||
|
||||
if (tablePermissions && action) {
|
||||
tablePermissions = tablePermissions.permissions[action];
|
||||
}
|
||||
}
|
||||
|
||||
return tablePermissions;
|
||||
};
|
||||
|
||||
/*** Table/View Check Constraints utils ***/
|
||||
|
||||
export const getTableCheckConstraints = table => {
|
||||
return table.check_constraints;
|
||||
};
|
||||
|
||||
export const getCheckConstraintName = constraint => {
|
||||
return constraint.constraint_name;
|
||||
};
|
||||
|
||||
export const findTableCheckConstraint = (checkConstraints, constraintName) => {
|
||||
return checkConstraints.find(
|
||||
c => getCheckConstraintName(c) === constraintName
|
||||
);
|
||||
};
|
||||
|
||||
/*** Function utils ***/
|
||||
|
||||
export const getFunctionSchema = pgFunction => {
|
||||
return pgFunction.function_schema;
|
||||
};
|
||||
|
||||
export const getFunctionName = pgFunction => {
|
||||
return pgFunction.function_name;
|
||||
};
|
||||
|
||||
export const getFunctionDefinition = pgFunction => {
|
||||
return pgFunction.function_definition;
|
||||
};
|
||||
|
||||
export const getSchemaFunctions = (allFunctions, fnSchema) => {
|
||||
return allFunctions.filter(fn => getFunctionSchema(fn) === fnSchema);
|
||||
};
|
||||
|
||||
export const findFunction = (allFunctions, functionName, functionSchema) => {
|
||||
return allFunctions.find(
|
||||
f =>
|
||||
getFunctionName(f) === functionName &&
|
||||
getFunctionSchema(f) === functionSchema
|
||||
);
|
||||
};
|
||||
|
||||
/*** Schema utils ***/
|
||||
|
||||
export const getSchemaName = schema => {
|
||||
return schema.schema_name;
|
||||
};
|
||||
|
||||
export const getSchemaTables = (allTables, tableSchema) => {
|
||||
return allTables.filter(t => getTableSchema(t) === tableSchema);
|
||||
};
|
||||
|
||||
export const getSchemaTableNames = (allTables, tableSchema) => {
|
||||
return getSchemaTables(allTables, tableSchema).map(t => getTableName(t));
|
||||
};
|
||||
|
||||
/*** Custom table fields utils ***/
|
||||
|
||||
export const getTableCustomRootFields = table => {
|
||||
if (table.configuration) {
|
||||
return table.configuration.custom_root_fields || {};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getTableCustomColumnNames = table => {
|
||||
if (table.configuration) {
|
||||
return table.configuration.custom_column_names || {};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/*** Table/View Computed Field utils ***/
|
||||
|
||||
export const getTableComputedFields = table => {
|
||||
return table.computed_fields;
|
||||
};
|
||||
|
||||
export const getComputedFieldName = computedField => {
|
||||
return computedField.computed_field_name;
|
||||
};
|
||||
|
||||
export const getGroupedTableComputedFields = (table, allFunctions) => {
|
||||
const groupedComputedFields = { scalar: [], table: [] };
|
||||
|
||||
getTableComputedFields(table).forEach(computedField => {
|
||||
const computedFieldFnDef = computedField.definition.function;
|
||||
const computedFieldFn = findFunction(
|
||||
allFunctions,
|
||||
computedFieldFnDef.name,
|
||||
computedFieldFnDef.schema
|
||||
);
|
||||
|
||||
if (computedFieldFn && computedFieldFn.return_type_type === 'b') {
|
||||
groupedComputedFields.scalar.push(computedField);
|
||||
} else {
|
||||
groupedComputedFields.table.push(computedField);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedComputedFields;
|
||||
};
|
||||
|
||||
// export const getDependentTables = (table) => {
|
||||
|
||||
// return [
|
||||
// {
|
||||
// table_schema: table.table_schema,
|
||||
// table_name: table.table_name,
|
||||
// },
|
||||
// ...table.foreign_key_constraints.map(fk_obj => ({
|
||||
// table_name: fk_obj.ref_table,
|
||||
// table_schema: fk_obj.ref_table_table_schema
|
||||
// })),
|
||||
// ...table.opp_foreign_key_constraints.map(fk_obj => ({
|
||||
// table_name: fk_obj.table_name,
|
||||
// table_schema: fk_obj.table_schema,
|
||||
// }))
|
||||
// ]
|
||||
|
||||
// };
|
582
console/src/components/Common/utils/pgUtils.tsx
Normal file
582
console/src/components/Common/utils/pgUtils.tsx
Normal file
@ -0,0 +1,582 @@
|
||||
import React from 'react';
|
||||
import { isEqual, isString } from './jsUtils';
|
||||
import { Nullable } from './tsUtils';
|
||||
import { TableDefinition, FunctionDefinition } from './v1QueryUtils';
|
||||
|
||||
/** * Table/View utils ** */
|
||||
|
||||
export type TableRelationship = {
|
||||
rel_name: string;
|
||||
rel_def: {
|
||||
manual_configuration?: any;
|
||||
foreign_key_constraint_on?: any; // TODO
|
||||
};
|
||||
rel_type: 'object' | 'array';
|
||||
};
|
||||
|
||||
export type TablePermission = {
|
||||
role_name: string;
|
||||
permissions: {
|
||||
[action: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
export interface BaseTableColumn {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
}
|
||||
|
||||
export interface TableColumn extends BaseTableColumn {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
udt_name: string;
|
||||
column_default: string;
|
||||
}
|
||||
|
||||
export type ForeignKeyConstraint = {
|
||||
ref_table: string;
|
||||
ref_table_table_schema: string;
|
||||
column_mapping: {
|
||||
[lcol: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CheckConstraint = {
|
||||
constraint_name: string;
|
||||
check: string;
|
||||
};
|
||||
|
||||
export type ComputedField = {
|
||||
computed_field_name: string;
|
||||
definition: {
|
||||
function: FunctionDefinition;
|
||||
};
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
schema_name: string;
|
||||
};
|
||||
|
||||
export interface BaseTable {
|
||||
table_name: string;
|
||||
table_schema: string;
|
||||
columns: BaseTableColumn[];
|
||||
is_enum: boolean;
|
||||
}
|
||||
export const makeBaseTable = (
|
||||
name: string,
|
||||
schema: string,
|
||||
columns: BaseTableColumn[],
|
||||
isEnum = false
|
||||
): BaseTable => ({
|
||||
table_name: name,
|
||||
table_schema: schema,
|
||||
columns,
|
||||
is_enum: isEnum,
|
||||
});
|
||||
|
||||
export interface Table extends BaseTable {
|
||||
table_name: string;
|
||||
table_schema: string;
|
||||
table_type: string;
|
||||
is_table_tracked: boolean;
|
||||
columns: TableColumn[];
|
||||
relationships: TableRelationship[];
|
||||
permissions: TablePermission[];
|
||||
foreign_key_constraints: ForeignKeyConstraint[];
|
||||
check_constraints: CheckConstraint[];
|
||||
configuration?: {
|
||||
custom_column_names: {
|
||||
[column: string]: string;
|
||||
};
|
||||
custom_root_fields: {
|
||||
select: Nullable<string>;
|
||||
select_by_pk: Nullable<string>;
|
||||
select_aggregate?: Nullable<string>;
|
||||
insert?: Nullable<string>;
|
||||
insert_one?: Nullable<string>;
|
||||
update?: Nullable<string>;
|
||||
update_by_pk?: Nullable<string>;
|
||||
delete?: Nullable<string>;
|
||||
delete_by_pk?: Nullable<string>;
|
||||
};
|
||||
};
|
||||
computed_fields: ComputedField[];
|
||||
is_enum: boolean;
|
||||
view_info: {
|
||||
is_trigger_insertable_into: 'YES' | 'NO';
|
||||
is_insertable_into: 'YES' | 'NO';
|
||||
is_updatable: 'YES' | 'NO';
|
||||
is_trigger_updatable: 'YES' | 'NO';
|
||||
is_trigger_deletable: 'YES' | 'NO';
|
||||
};
|
||||
}
|
||||
|
||||
export type PGFunction = {
|
||||
function_name: string;
|
||||
function_schema: string;
|
||||
function_definition: string;
|
||||
return_type_type: string;
|
||||
};
|
||||
|
||||
export type PGSchema = {
|
||||
schema_name: string;
|
||||
};
|
||||
|
||||
export const getTableName = (table: Table) => {
|
||||
return table.table_name;
|
||||
};
|
||||
|
||||
export const getTableSchema = (table: Table) => {
|
||||
return table.table_schema;
|
||||
};
|
||||
|
||||
export const getTableType = (table: Table) => {
|
||||
return table.table_type;
|
||||
};
|
||||
|
||||
// TODO: figure out better pattern for overloading fns
|
||||
// tableName and tableNameWithSchema are either/or arguments
|
||||
export const generateTableDef = (
|
||||
tableName: string,
|
||||
tableSchema: Nullable<string> = 'public',
|
||||
tableNameWithSchema: Nullable<string> = null
|
||||
) => {
|
||||
if (tableNameWithSchema) {
|
||||
return {
|
||||
schema: tableNameWithSchema.split('.')[0],
|
||||
name: tableNameWithSchema.split('.')[1],
|
||||
};
|
||||
}
|
||||
return {
|
||||
schema: tableSchema || 'public',
|
||||
name: tableName,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTableDef = (table: Table) => {
|
||||
return generateTableDef(getTableName(table), getTableSchema(table));
|
||||
};
|
||||
|
||||
export const getQualifiedTableDef = (tableDef: TableDefinition | string) => {
|
||||
return isString(tableDef) ? generateTableDef(tableDef as string) : tableDef;
|
||||
};
|
||||
|
||||
export const getTableNameWithSchema = (
|
||||
tableDef: TableDefinition,
|
||||
wrapDoubleQuotes = false
|
||||
) => {
|
||||
let fullTableName;
|
||||
|
||||
if (wrapDoubleQuotes) {
|
||||
fullTableName = `"${tableDef.schema}"."${tableDef.name}"`;
|
||||
} else {
|
||||
fullTableName = `${tableDef.schema}.${tableDef.name}`;
|
||||
}
|
||||
|
||||
return fullTableName;
|
||||
};
|
||||
|
||||
export const checkIfTable = (table: Table) => {
|
||||
return table.table_type === 'TABLE';
|
||||
};
|
||||
|
||||
export const displayTableName = (table: Table) => {
|
||||
const tableName = getTableName(table);
|
||||
const isTable = checkIfTable(table);
|
||||
|
||||
return isTable ? <span>{tableName}</span> : <i>{tableName}</i>;
|
||||
};
|
||||
|
||||
export const findTable = (allTables: Table[], tableDef: TableDefinition) => {
|
||||
return allTables.find(t => isEqual(getTableDef(t), tableDef));
|
||||
};
|
||||
|
||||
export const getTrackedTables = (tables: Table[]) => {
|
||||
return tables.filter(t => t.is_table_tracked);
|
||||
};
|
||||
|
||||
export const getUntrackedTables = (tables: Table[]) => {
|
||||
return tables.filter(t => !t.is_table_tracked);
|
||||
};
|
||||
|
||||
export const getOnlyTables = (tablesOrViews: Table[]) => {
|
||||
return tablesOrViews.filter(t => checkIfTable(t));
|
||||
};
|
||||
|
||||
export const getOnlyViews = (tablesOrViews: Table[]) => {
|
||||
return tablesOrViews.filter(t => !checkIfTable(t));
|
||||
};
|
||||
|
||||
export const QUERY_TYPES = ['insert', 'select', 'update', 'delete'];
|
||||
|
||||
export const getTableSupportedQueries = (table: Table) => {
|
||||
let supportedQueryTypes;
|
||||
|
||||
if (checkIfTable(table)) {
|
||||
supportedQueryTypes = QUERY_TYPES;
|
||||
} else {
|
||||
// is View
|
||||
supportedQueryTypes = [];
|
||||
|
||||
// Add insert/update permission if it is insertable/updatable as returned by pg
|
||||
if (table.view_info) {
|
||||
if (
|
||||
table.view_info.is_insertable_into === 'YES' ||
|
||||
table.view_info.is_trigger_insertable_into === 'YES'
|
||||
) {
|
||||
supportedQueryTypes.push('insert');
|
||||
}
|
||||
|
||||
supportedQueryTypes.push('select'); // to maintain order
|
||||
|
||||
if (table.view_info.is_updatable === 'YES') {
|
||||
supportedQueryTypes.push('update');
|
||||
supportedQueryTypes.push('delete');
|
||||
} else {
|
||||
if (table.view_info.is_trigger_updatable === 'YES') {
|
||||
supportedQueryTypes.push('update');
|
||||
}
|
||||
|
||||
if (table.view_info.is_trigger_deletable === 'YES') {
|
||||
supportedQueryTypes.push('delete');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
supportedQueryTypes.push('select');
|
||||
}
|
||||
}
|
||||
|
||||
return supportedQueryTypes;
|
||||
};
|
||||
|
||||
/** * Table/View column utils ** */
|
||||
|
||||
export const getTableColumns = (table: Nullable<Table>) => {
|
||||
return table ? table.columns : [];
|
||||
};
|
||||
|
||||
export const getColumnName = (column: TableColumn) => {
|
||||
return column.column_name;
|
||||
};
|
||||
|
||||
export const getTableColumnNames = (table: Table) => {
|
||||
return getTableColumns(table).map(c => getColumnName(c));
|
||||
};
|
||||
|
||||
export const getTableColumn = (table: Table, columnName: string) => {
|
||||
return getTableColumns(table).find(
|
||||
column => getColumnName(column) === columnName
|
||||
);
|
||||
};
|
||||
|
||||
export const getColumnType = (column: TableColumn) => {
|
||||
let columnType = column.data_type;
|
||||
|
||||
if (columnType === 'USER-DEFINED') {
|
||||
columnType = column.udt_name;
|
||||
}
|
||||
|
||||
return columnType;
|
||||
};
|
||||
|
||||
export const isColumnAutoIncrement = (column: TableColumn) => {
|
||||
const columnDefault = column.column_default;
|
||||
|
||||
const autoIncrementDefaultRegex = /^nextval\('(.*)_seq'::regclass\)$/;
|
||||
|
||||
return (
|
||||
columnDefault &&
|
||||
columnDefault.match(new RegExp(autoIncrementDefaultRegex, 'gi'))
|
||||
);
|
||||
};
|
||||
|
||||
/** * Table/View relationship utils ** */
|
||||
|
||||
export const getTableRelationships = (table: Table) => {
|
||||
return table.relationships;
|
||||
};
|
||||
|
||||
export const getRelationshipName = (relationship: TableRelationship) => {
|
||||
return relationship.rel_name;
|
||||
};
|
||||
|
||||
export const getRelationshipDef = (relationship: TableRelationship) => {
|
||||
return relationship.rel_def;
|
||||
};
|
||||
|
||||
export const getRelationshipType = (relationship: TableRelationship) => {
|
||||
return relationship.rel_type;
|
||||
};
|
||||
|
||||
export const getTableRelationshipNames = (table: Table) => {
|
||||
return getTableRelationships(table).map(r => getRelationshipName(r));
|
||||
};
|
||||
|
||||
export function getTableRelationship(table: Table, relationshipName: string) {
|
||||
return getTableRelationships(table).find(
|
||||
relationship => getRelationshipName(relationship) === relationshipName
|
||||
);
|
||||
}
|
||||
|
||||
export function getRelationshipRefTable(
|
||||
table: Table,
|
||||
relationship: TableRelationship
|
||||
) {
|
||||
let refTable = null;
|
||||
|
||||
const relationshipDef = getRelationshipDef(relationship);
|
||||
const relationshipType = getRelationshipType(relationship);
|
||||
|
||||
// if manual relationship
|
||||
if (relationshipDef.manual_configuration) {
|
||||
refTable = relationshipDef.manual_configuration.remote_table;
|
||||
}
|
||||
|
||||
// if foreign-key based relationship
|
||||
if (relationshipDef.foreign_key_constraint_on) {
|
||||
// if array relationship
|
||||
if (relationshipType === 'array') {
|
||||
refTable = relationshipDef.foreign_key_constraint_on.table;
|
||||
}
|
||||
|
||||
// if object relationship
|
||||
if (relationshipType === 'object') {
|
||||
const fkCol = relationshipDef.foreign_key_constraint_on;
|
||||
|
||||
for (let i = 0; i < table.foreign_key_constraints.length; i++) {
|
||||
const fkConstraint = table.foreign_key_constraints[i];
|
||||
const fkConstraintCol = Object.keys(fkConstraint.column_mapping)[0];
|
||||
if (fkCol === fkConstraintCol) {
|
||||
refTable = generateTableDef(
|
||||
fkConstraint.ref_table,
|
||||
fkConstraint.ref_table_table_schema
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof refTable === 'string') {
|
||||
refTable = generateTableDef(refTable);
|
||||
}
|
||||
|
||||
return refTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} currentSchema
|
||||
* @param {string} currentTable
|
||||
* @param {Array<{[key: string]: any}>} allSchemas
|
||||
*
|
||||
* @returns {Array<{
|
||||
* columnName: string,
|
||||
* enumTableName: string,
|
||||
* enumColumnName: string,
|
||||
* }>}
|
||||
*/
|
||||
|
||||
export const getEnumColumnMappings = (
|
||||
allSchemas: Table[],
|
||||
tableName: string,
|
||||
tableSchema: string
|
||||
) => {
|
||||
const currentTable = findTable(
|
||||
allSchemas,
|
||||
generateTableDef(tableName, tableSchema)
|
||||
);
|
||||
|
||||
const relationsMap: any[] = [];
|
||||
if (!currentTable) return null;
|
||||
if (!currentTable.foreign_key_constraints.length) return null;
|
||||
|
||||
currentTable.foreign_key_constraints.forEach(
|
||||
({ ref_table, ref_table_table_schema, column_mapping }) => {
|
||||
const refTable = findTable(
|
||||
allSchemas,
|
||||
generateTableDef(ref_table, ref_table_table_schema)
|
||||
);
|
||||
|
||||
if (!refTable || !refTable.is_enum) return;
|
||||
|
||||
const keys = Object.keys(column_mapping);
|
||||
if (!keys.length) return;
|
||||
|
||||
const columnName = keys[0];
|
||||
const enumColumnName = column_mapping[columnName];
|
||||
|
||||
if (columnName && enumColumnName) {
|
||||
relationsMap.push({
|
||||
columnName,
|
||||
enumTableName: ref_table,
|
||||
enumColumnName,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return relationsMap;
|
||||
};
|
||||
|
||||
/** * Table/View permissions utils ** */
|
||||
|
||||
export const getTablePermissions = (
|
||||
table: Table,
|
||||
role: string | null = null,
|
||||
action: string | null = null
|
||||
) => {
|
||||
const tablePermissions = table.permissions;
|
||||
|
||||
if (role) {
|
||||
const rolePermissions = tablePermissions.find(p => p.role_name === role);
|
||||
|
||||
if (rolePermissions && action) {
|
||||
return rolePermissions.permissions[action];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return tablePermissions;
|
||||
};
|
||||
|
||||
/** * Table/View Check Constraints utils ** */
|
||||
|
||||
export const getTableCheckConstraints = (table: Table) => {
|
||||
return table.check_constraints;
|
||||
};
|
||||
|
||||
export const getCheckConstraintName = (constraint: CheckConstraint) => {
|
||||
return constraint.constraint_name;
|
||||
};
|
||||
|
||||
export const findTableCheckConstraint = (
|
||||
checkConstraints: CheckConstraint[],
|
||||
constraintName: string
|
||||
) => {
|
||||
return checkConstraints.find(
|
||||
c => getCheckConstraintName(c) === constraintName
|
||||
);
|
||||
};
|
||||
|
||||
/** * Function utils ** */
|
||||
|
||||
export const getFunctionSchema = (pgFunction: PGFunction) => {
|
||||
return pgFunction.function_schema;
|
||||
};
|
||||
|
||||
export const getFunctionName = (pgFunction: PGFunction) => {
|
||||
return pgFunction.function_name;
|
||||
};
|
||||
|
||||
export const getFunctionDefinition = (pgFunction: PGFunction) => {
|
||||
return pgFunction.function_definition;
|
||||
};
|
||||
|
||||
export const getSchemaFunctions = (
|
||||
allFunctions: PGFunction[],
|
||||
fnSchema: string
|
||||
) => {
|
||||
return allFunctions.filter(fn => getFunctionSchema(fn) === fnSchema);
|
||||
};
|
||||
|
||||
export const findFunction = (
|
||||
allFunctions: PGFunction[],
|
||||
functionName: string,
|
||||
functionSchema: string
|
||||
) => {
|
||||
return allFunctions.find(
|
||||
f =>
|
||||
getFunctionName(f) === functionName &&
|
||||
getFunctionSchema(f) === functionSchema
|
||||
);
|
||||
};
|
||||
|
||||
/** * Schema utils ** */
|
||||
|
||||
export const getSchemaName = (schema: PGSchema) => {
|
||||
return schema.schema_name;
|
||||
};
|
||||
|
||||
export const getSchemaTables = (allTables: Table[], tableSchema: string) => {
|
||||
return allTables.filter(t => getTableSchema(t) === tableSchema);
|
||||
};
|
||||
|
||||
export const getSchemaTableNames = (
|
||||
allTables: Table[],
|
||||
tableSchema: string
|
||||
) => {
|
||||
return getSchemaTables(allTables, tableSchema).map(t => getTableName(t));
|
||||
};
|
||||
|
||||
/** * Custom table fields utils ** */
|
||||
|
||||
export const getTableCustomRootFields = (table: Table) => {
|
||||
if (table.configuration) {
|
||||
return table.configuration.custom_root_fields || {};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getTableCustomColumnNames = (table: Table) => {
|
||||
if (table.configuration) {
|
||||
return table.configuration.custom_column_names || {};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/** * Table/View Computed Field utils ** */
|
||||
|
||||
export const getTableComputedFields = (table: Table) => {
|
||||
return table.computed_fields;
|
||||
};
|
||||
|
||||
export const getComputedFieldName = (computedField: ComputedField) => {
|
||||
return computedField.computed_field_name;
|
||||
};
|
||||
|
||||
export const getGroupedTableComputedFields = (
|
||||
table: Table,
|
||||
allFunctions: PGFunction[]
|
||||
) => {
|
||||
const groupedComputedFields: {
|
||||
scalar: ComputedField[];
|
||||
table: ComputedField[];
|
||||
} = { scalar: [], table: [] };
|
||||
|
||||
getTableComputedFields(table).forEach(computedField => {
|
||||
const computedFieldFnDef = computedField.definition.function;
|
||||
const computedFieldFn = findFunction(
|
||||
allFunctions,
|
||||
computedFieldFnDef.name,
|
||||
computedFieldFnDef.schema
|
||||
);
|
||||
|
||||
if (computedFieldFn && computedFieldFn.return_type_type === 'b') {
|
||||
groupedComputedFields.scalar.push(computedField);
|
||||
} else {
|
||||
groupedComputedFields.table.push(computedField);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedComputedFields;
|
||||
};
|
||||
|
||||
// export const getDependentTables = (table) => {
|
||||
|
||||
// return [
|
||||
// {
|
||||
// table_schema: table.table_schema,
|
||||
// table_name: table.table_name,
|
||||
// },
|
||||
// ...table.foreign_key_constraints.map(fk_obj => ({
|
||||
// table_name: fk_obj.ref_table,
|
||||
// table_schema: fk_obj.ref_table_table_schema
|
||||
// })),
|
||||
// ...table.opp_foreign_key_constraints.map(fk_obj => ({
|
||||
// table_name: fk_obj.table_name,
|
||||
// table_schema: fk_obj.table_schema,
|
||||
// }))
|
||||
// ]
|
||||
|
||||
// };
|
11
console/src/components/Common/utils/reactUtils.ts
Normal file
11
console/src/components/Common/utils/reactUtils.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Dispatch } from '../../../types';
|
||||
|
||||
export const getReactHelmetTitle = (feature: string, service: string) => {
|
||||
return `${feature} - ${service} | Hasura`;
|
||||
};
|
||||
|
||||
/*
|
||||
* called "mapDispatchToPropsEmpty" because it just maps
|
||||
* the "dispatch" function and not any custom dispatchers
|
||||
*/
|
||||
export const mapDispatchToPropsEmpty = (dispatch: Dispatch) => ({ dispatch });
|
@ -68,3 +68,91 @@ export const getActionsBaseRoute = () => {
|
||||
export const getActionsCreateRoute = () => {
|
||||
return `${getActionsBaseRoute()}/add`;
|
||||
};
|
||||
|
||||
// Events route utils
|
||||
|
||||
export const eventsPrefix = 'events';
|
||||
export const scheduledEventsPrefix = 'cron';
|
||||
export const adhocEventsPrefix = 'one-off-scheduled-events';
|
||||
export const dataEventsPrefix = 'data';
|
||||
export const routeType = 'absolute' | 'relative';
|
||||
|
||||
export const getSTRoute = (type, relativeRoute) => {
|
||||
if (type === 'relative') {
|
||||
return `${relativeRoute}`;
|
||||
}
|
||||
return `/${eventsPrefix}/${scheduledEventsPrefix}/${relativeRoute}`;
|
||||
};
|
||||
export const getETRoute = (type, relativeRoute) => {
|
||||
if (type === 'relative') {
|
||||
return `${relativeRoute}`;
|
||||
}
|
||||
return `/${eventsPrefix}/${dataEventsPrefix}/${relativeRoute}`;
|
||||
};
|
||||
export const getAdhocEventsRoute = (type, relativeRoute) => {
|
||||
if (type === 'relative') {
|
||||
return `${relativeRoute}`;
|
||||
}
|
||||
return `/${eventsPrefix}/${adhocEventsPrefix}/${relativeRoute}`;
|
||||
};
|
||||
|
||||
export const isDataEventsRoute = route => {
|
||||
return route.includes(`/${eventsPrefix}/${dataEventsPrefix}`);
|
||||
};
|
||||
export const isScheduledEventsRoute = route => {
|
||||
return route.includes(`/${eventsPrefix}/${scheduledEventsPrefix}`);
|
||||
};
|
||||
export const isAdhocScheduledEventRoute = route => {
|
||||
return route.includes(`/${eventsPrefix}/${adhocEventsPrefix}`);
|
||||
};
|
||||
export const getAddSTRoute = type => {
|
||||
return getSTRoute(type, 'add');
|
||||
};
|
||||
export const getScheduledEventsLandingRoute = type => {
|
||||
return getSTRoute(type, 'manage');
|
||||
};
|
||||
export const getSTModifyRoute = (stName, type) => {
|
||||
return getSTRoute(type, `${stName}/modify`);
|
||||
};
|
||||
export const getSTPendingEventsRoute = (stName, type) => {
|
||||
return getSTRoute(type, `${stName}/pending`);
|
||||
};
|
||||
export const getSTProcessedEventsRoute = (stName, type) => {
|
||||
return getSTRoute(type, `${stName}/processed`);
|
||||
};
|
||||
export const getSTInvocationLogsRoute = (stName, type) => {
|
||||
return getSTRoute(type, `${stName}/logs`);
|
||||
};
|
||||
export const getAddETRoute = type => {
|
||||
return getETRoute(type, 'add');
|
||||
};
|
||||
export const getDataEventsLandingRoute = type => {
|
||||
return getETRoute(type, 'manage');
|
||||
};
|
||||
export const getETModifyRoute = (etName, type) => {
|
||||
return getETRoute(type, `${etName}/modify`);
|
||||
};
|
||||
export const getETPendingEventsRoute = (etName, type) => {
|
||||
return getETRoute(type, `${etName}/pending`);
|
||||
};
|
||||
export const getETProcessedEventsRoute = (etName, type) => {
|
||||
return getETRoute(type, `${etName}/processed`);
|
||||
};
|
||||
export const getETInvocationLogsRoute = (etName, type) => {
|
||||
return getETRoute(type, `${etName}/logs`);
|
||||
};
|
||||
export const getAddAdhocEventRoute = type => {
|
||||
return getAdhocEventsRoute(type, 'add');
|
||||
};
|
||||
export const getAdhocEventsLogsRoute = type => {
|
||||
return getAdhocEventsRoute(type, 'logs');
|
||||
};
|
||||
export const getAdhocPendingEventsRoute = type => {
|
||||
return getAdhocEventsRoute(type, 'pending');
|
||||
};
|
||||
export const getAdhocProcessedEventsRoute = type => {
|
||||
return getAdhocEventsRoute(type, 'processed');
|
||||
};
|
||||
export const getAdhocEventsInfoRoute = type => {
|
||||
return getAdhocEventsRoute(type, 'info');
|
||||
};
|
||||
|
@ -1,31 +1,23 @@
|
||||
interface SqlUtilsOptions {
|
||||
tableName: string;
|
||||
schemaName: string;
|
||||
constraintName: string;
|
||||
check?: string;
|
||||
selectedPkColumns?: string[];
|
||||
}
|
||||
export const sqlEscapeText = (rawText: string) => {
|
||||
let text = rawText;
|
||||
|
||||
export const sqlEscapeText = (text: string) => {
|
||||
let escapedText = text;
|
||||
|
||||
if (escapedText) {
|
||||
escapedText = escapedText.replace(/'/g, "\\'");
|
||||
if (text) {
|
||||
text = text.replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
return `E'${escapedText}'`;
|
||||
return `E'${text}'`;
|
||||
};
|
||||
|
||||
// detect DDL statements in SQL
|
||||
export const checkSchemaModification = (_sql: string) => {
|
||||
export const checkSchemaModification = (sql: string) => {
|
||||
let isSchemaModification = false;
|
||||
|
||||
const sqlStatements = _sql
|
||||
const sqlStatements = sql
|
||||
.toLowerCase()
|
||||
.split(';')
|
||||
.map(s => s.trim());
|
||||
|
||||
sqlStatements.forEach((statement: string) => {
|
||||
sqlStatements.forEach(statement => {
|
||||
if (
|
||||
statement.startsWith('create ') ||
|
||||
statement.startsWith('alter ') ||
|
||||
@ -70,14 +62,14 @@ export const getCreatePkSql = ({
|
||||
tableName,
|
||||
selectedPkColumns,
|
||||
constraintName,
|
||||
}: SqlUtilsOptions) => {
|
||||
// if no primary key columns provided, return empty query
|
||||
if (!selectedPkColumns || selectedPkColumns.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
}: {
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
selectedPkColumns: string[];
|
||||
constraintName: string;
|
||||
}) => {
|
||||
return `alter table "${schemaName}"."${tableName}"
|
||||
add constraint "${constraintName}"
|
||||
add constraint "${constraintName}"
|
||||
primary key ( ${selectedPkColumns.map(pkc => `"${pkc}"`).join(', ')} );`;
|
||||
};
|
||||
|
||||
@ -85,14 +77,17 @@ export const getDropPkSql = ({
|
||||
schemaName,
|
||||
tableName,
|
||||
constraintName,
|
||||
}: SqlUtilsOptions) => {
|
||||
}: {
|
||||
schemaName: string;
|
||||
tableName: string;
|
||||
constraintName: string;
|
||||
}) => {
|
||||
return `alter table "${schemaName}"."${tableName}" drop constraint "${constraintName}";`;
|
||||
};
|
||||
|
||||
export const terminateSql = (sql: string) => {
|
||||
const sqlTerminated = sql.trim();
|
||||
|
||||
return sqlTerminated[sqlTerminated.length - 1] !== ';'
|
||||
? `${sqlTerminated};`
|
||||
: sqlTerminated;
|
||||
const sqlSanitised = sql.trim();
|
||||
return sqlSanitised[sqlSanitised.length - 1] !== ';'
|
||||
? `${sqlSanitised};`
|
||||
: sqlSanitised;
|
||||
};
|
||||
|
@ -1,2 +1,12 @@
|
||||
export const UNSAFE_keys = <T extends object>(source: T) =>
|
||||
Object.keys(source) as Array<keyof T>;
|
||||
|
||||
export type Json =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| Json[]
|
||||
| { [prop: string]: Json };
|
||||
|
||||
export type Nullable<T> = T | null | undefined;
|
||||
|
@ -1,7 +1,6 @@
|
||||
export const getPathRoot = path => {
|
||||
return path.split('/')[1];
|
||||
};
|
||||
|
||||
export const stripTrailingSlash = url => {
|
||||
if (url && url.endsWith('/')) {
|
||||
return url.slice(0, -1);
|
||||
|
@ -1,345 +0,0 @@
|
||||
import { terminateSql } from './sqlUtils';
|
||||
|
||||
export const getRunSqlQuery = (sql, shouldCascade, readOnly) => {
|
||||
return {
|
||||
type: 'run_sql',
|
||||
args: {
|
||||
sql: terminateSql(sql),
|
||||
cascade: !!shouldCascade,
|
||||
read_only: !!readOnly,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getCreatePermissionQuery = (
|
||||
action,
|
||||
tableDef,
|
||||
role,
|
||||
permission
|
||||
) => {
|
||||
return {
|
||||
type: 'create_' + action + '_permission',
|
||||
args: {
|
||||
table: tableDef,
|
||||
role: role,
|
||||
permission: permission,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropPermissionQuery = (action, tableDef, role) => {
|
||||
return {
|
||||
type: 'drop_' + action + '_permission',
|
||||
args: {
|
||||
table: tableDef,
|
||||
role: role,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateSetCustomTypesQuery = customTypes => {
|
||||
return {
|
||||
type: 'set_custom_types',
|
||||
args: customTypes,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateCreateActionQuery = (name, definition, comment) => {
|
||||
return {
|
||||
type: 'create_action',
|
||||
args: {
|
||||
name,
|
||||
definition,
|
||||
comment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateDropActionQuery = name => {
|
||||
return {
|
||||
type: 'drop_action',
|
||||
args: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchActionsQuery = () => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_action',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*.*'],
|
||||
order_by: [{ column: 'action_name', type: 'asc' }],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchCustomTypesQuery = () => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_custom_types',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*.*'],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getSetCustomRootFieldsQuery = (
|
||||
tableDef,
|
||||
rootFields,
|
||||
customColumnNames
|
||||
) => {
|
||||
return {
|
||||
type: 'set_table_custom_fields',
|
||||
version: 2,
|
||||
args: {
|
||||
table: tableDef,
|
||||
custom_root_fields: rootFields,
|
||||
custom_column_names: customColumnNames,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchAllRolesQuery = () => ({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
schema: 'hdb_catalog',
|
||||
name: 'hdb_role',
|
||||
},
|
||||
columns: ['role_name'],
|
||||
order_by: { column: 'role_name', type: 'asc' },
|
||||
},
|
||||
});
|
||||
|
||||
export const getCreateActionPermissionQuery = (def, actionName) => {
|
||||
return {
|
||||
type: 'create_action_permission',
|
||||
args: {
|
||||
action: actionName,
|
||||
role: def.role,
|
||||
definition: {
|
||||
select: {
|
||||
filter: def.filter,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getUpdateActionQuery = (def, actionName, actionComment) => {
|
||||
return {
|
||||
type: 'update_action',
|
||||
args: {
|
||||
name: actionName,
|
||||
definition: def,
|
||||
comment: actionComment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropActionPermissionQuery = (role, actionName) => {
|
||||
return {
|
||||
type: 'drop_action_permission',
|
||||
args: {
|
||||
action: actionName,
|
||||
role,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getSetTableEnumQuery = (tableDef, isEnum) => {
|
||||
return {
|
||||
type: 'set_table_is_enum',
|
||||
args: {
|
||||
table: tableDef,
|
||||
is_enum: isEnum,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrackTableQuery = tableDef => {
|
||||
return {
|
||||
type: 'add_existing_table_or_view',
|
||||
args: tableDef,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUntrackTableQuery = tableDef => {
|
||||
return {
|
||||
type: 'untrack_table',
|
||||
args: {
|
||||
table: tableDef,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getAddComputedFieldQuery = (
|
||||
tableDef,
|
||||
computedFieldName,
|
||||
definition,
|
||||
comment
|
||||
) => {
|
||||
return {
|
||||
type: 'add_computed_field',
|
||||
args: {
|
||||
table: tableDef,
|
||||
name: computedFieldName,
|
||||
definition: {
|
||||
...definition,
|
||||
},
|
||||
comment: comment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropComputedFieldQuery = (tableDef, computedFieldName) => {
|
||||
return {
|
||||
type: 'drop_computed_field',
|
||||
args: {
|
||||
table: tableDef,
|
||||
name: computedFieldName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDeleteQuery = (pkClause, tableName, schemaName) => {
|
||||
return {
|
||||
type: 'delete',
|
||||
args: {
|
||||
table: {
|
||||
name: tableName,
|
||||
schema: schemaName,
|
||||
},
|
||||
where: pkClause,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getBulkDeleteQuery = (pkClauses, tableName, schemaName) =>
|
||||
pkClauses.map(pkClause => getDeleteQuery(pkClause, tableName, schemaName));
|
||||
|
||||
export const getEnumOptionsQuery = (request, currentSchema) => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: request.enumTableName,
|
||||
schema: currentSchema,
|
||||
},
|
||||
columns: [request.enumColumnName],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const inconsistentObjectsQuery = {
|
||||
type: 'get_inconsistent_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const dropInconsistentObjectsQuery = {
|
||||
type: 'drop_inconsistent_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const getReloadMetadataQuery = shouldReloadRemoteSchemas => ({
|
||||
type: 'reload_metadata',
|
||||
args: {
|
||||
reload_remote_schemas: shouldReloadRemoteSchemas,
|
||||
},
|
||||
});
|
||||
|
||||
export const getReloadRemoteSchemaCacheQuery = remoteSchemaName => {
|
||||
return {
|
||||
type: 'reload_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const exportMetadataQuery = {
|
||||
type: 'export_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const generateReplaceMetadataQuery = metadataJson => ({
|
||||
type: 'replace_metadata',
|
||||
args: metadataJson,
|
||||
});
|
||||
|
||||
export const resetMetadataQuery = {
|
||||
type: 'clear_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const generateSelectQuery = (
|
||||
type,
|
||||
tableDef,
|
||||
{ where, limit, offset, order_by, columns }
|
||||
) => ({
|
||||
type,
|
||||
args: {
|
||||
columns,
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order_by,
|
||||
table: tableDef,
|
||||
},
|
||||
});
|
||||
|
||||
export const getFetchManualTriggersQuery = tableName => ({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*'],
|
||||
order_by: {
|
||||
column: 'name',
|
||||
type: 'asc',
|
||||
nulls: 'last',
|
||||
},
|
||||
where: {
|
||||
table_name: tableName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getConsoleOptsQuery = () =>
|
||||
generateSelectQuery(
|
||||
'select',
|
||||
{ name: 'hdb_version', schema: 'hdb_catalog' },
|
||||
{
|
||||
columns: ['hasura_uuid', 'console_state'],
|
||||
}
|
||||
);
|
||||
|
||||
export const getSaveRemoteRelQuery = (args, isNew) => ({
|
||||
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
||||
args,
|
||||
});
|
||||
|
||||
export const getDropRemoteRelQuery = (name, table) => ({
|
||||
type: 'delete_remote_relationship',
|
||||
args: {
|
||||
name,
|
||||
table,
|
||||
},
|
||||
});
|
||||
|
||||
export const getRemoteSchemaIntrospectionQuery = remoteSchemaName => ({
|
||||
type: 'introspect_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
},
|
||||
});
|
680
console/src/components/Common/utils/v1QueryUtils.ts
Normal file
680
console/src/components/Common/utils/v1QueryUtils.ts
Normal file
@ -0,0 +1,680 @@
|
||||
import { terminateSql } from './sqlUtils';
|
||||
import { LocalScheduledTriggerState } from '../../Services/Events/CronTriggers/state';
|
||||
import { LocalAdhocEventState } from '../../Services/Events/AdhocEvents/Add/state';
|
||||
import { LocalEventTriggerState } from '../../Services/Events/EventTriggers/state';
|
||||
import { RemoteRelationshipPayload } from '../../Services/Data/TableRelationships/RemoteRelationships/utils';
|
||||
import { transformHeaders } from '../Headers/utils';
|
||||
import { generateTableDef } from './pgUtils';
|
||||
import { Nullable } from './tsUtils';
|
||||
|
||||
// TODO add type for the where clause
|
||||
|
||||
// TODO extend all queries with v1 query type
|
||||
|
||||
export type OrderByType = 'asc' | 'desc';
|
||||
export type OrderByNulls = 'first' | 'last';
|
||||
|
||||
export type OrderBy = {
|
||||
column: string;
|
||||
type: OrderByType;
|
||||
nulls: Nullable<OrderByNulls>;
|
||||
};
|
||||
export const makeOrderBy = (
|
||||
column: string,
|
||||
type: OrderByType,
|
||||
nulls: Nullable<OrderByNulls> = 'last'
|
||||
): OrderBy => ({
|
||||
column,
|
||||
type,
|
||||
nulls,
|
||||
});
|
||||
|
||||
export type WhereClause = any;
|
||||
|
||||
export type TableDefinition = {
|
||||
name: string;
|
||||
schema: string;
|
||||
};
|
||||
|
||||
export type FunctionDefinition = {
|
||||
name: string;
|
||||
schema: string;
|
||||
};
|
||||
|
||||
type GraphQLArgument = {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type Header = {
|
||||
name: string;
|
||||
value?: string;
|
||||
value_from_env?: string;
|
||||
};
|
||||
|
||||
export const getRunSqlQuery = (
|
||||
sql: string,
|
||||
shouldCascade: boolean,
|
||||
readOnly: boolean
|
||||
) => {
|
||||
return {
|
||||
type: 'run_sql',
|
||||
args: {
|
||||
sql: terminateSql(sql),
|
||||
cascade: !!shouldCascade,
|
||||
read_only: !!readOnly,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getCreatePermissionQuery = (
|
||||
action: string,
|
||||
tableDef: TableDefinition,
|
||||
role: string,
|
||||
permission: any
|
||||
) => {
|
||||
return {
|
||||
type: `create_${action}_permission`,
|
||||
args: {
|
||||
table: tableDef,
|
||||
role,
|
||||
permission,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropPermissionQuery = (
|
||||
action: string,
|
||||
tableDef: TableDefinition,
|
||||
role: string
|
||||
) => {
|
||||
return {
|
||||
type: `drop_${action}_permission`,
|
||||
args: {
|
||||
table: tableDef,
|
||||
role,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type CustomTypeScalar = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type CustomTypeEnumValue = {
|
||||
value: string;
|
||||
description: string;
|
||||
};
|
||||
type CustomTypeEnum = {
|
||||
name: string;
|
||||
values: CustomTypeEnumValue[];
|
||||
description: string;
|
||||
};
|
||||
|
||||
type CustomTypeObjectField = {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
type CustomTypeObject = {
|
||||
name: string;
|
||||
description: string;
|
||||
fields: CustomTypeObjectField[];
|
||||
};
|
||||
|
||||
type CustomTypeInputObjectField = {
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
};
|
||||
type CustomTypeInputObject = {
|
||||
name: string;
|
||||
description: string;
|
||||
fields: CustomTypeInputObjectField[];
|
||||
};
|
||||
|
||||
type CustomTypes = {
|
||||
scalars: CustomTypeScalar[];
|
||||
enums: CustomTypeEnum[];
|
||||
objects: CustomTypeObject[];
|
||||
input_objects: CustomTypeInputObject[];
|
||||
};
|
||||
|
||||
export const generateSetCustomTypesQuery = (customTypes: CustomTypes) => {
|
||||
return {
|
||||
type: 'set_custom_types',
|
||||
args: customTypes,
|
||||
};
|
||||
};
|
||||
|
||||
type ActionDefinition = {
|
||||
arguments: GraphQLArgument[];
|
||||
kind: 'synchronous' | 'asynchronous';
|
||||
output_type: string;
|
||||
handler: string;
|
||||
headers: Header[];
|
||||
forward_client_headers: boolean;
|
||||
};
|
||||
|
||||
export const generateCreateActionQuery = (
|
||||
name: string,
|
||||
definition: ActionDefinition,
|
||||
comment: string
|
||||
) => {
|
||||
return {
|
||||
type: 'create_action',
|
||||
args: {
|
||||
name,
|
||||
definition,
|
||||
comment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const generateDropActionQuery = (name: string) => {
|
||||
return {
|
||||
type: 'drop_action',
|
||||
args: {
|
||||
name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchActionsQuery = () => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_action',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*.*'],
|
||||
order_by: [{ column: 'action_name', type: 'asc' }],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchCustomTypesQuery = () => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_custom_types',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*.*'],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type CustomRootFields = {
|
||||
select: string;
|
||||
select_by_pk: string;
|
||||
select_aggregate: string;
|
||||
insert: string;
|
||||
update: string;
|
||||
delete: string;
|
||||
};
|
||||
|
||||
type CustomColumnNames = {
|
||||
[columnName: string]: string;
|
||||
};
|
||||
|
||||
export const getSetCustomRootFieldsQuery = (
|
||||
tableDef: TableDefinition,
|
||||
rootFields: CustomRootFields,
|
||||
customColumnNames: CustomColumnNames
|
||||
) => {
|
||||
return {
|
||||
type: 'set_table_custom_fields',
|
||||
version: 2,
|
||||
args: {
|
||||
table: tableDef,
|
||||
custom_root_fields: rootFields,
|
||||
custom_column_names: customColumnNames,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchAllRolesQuery = () => ({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
schema: 'hdb_catalog',
|
||||
name: 'hdb_role',
|
||||
},
|
||||
columns: ['role_name'],
|
||||
order_by: [{ column: 'role_name', type: 'asc' }],
|
||||
},
|
||||
});
|
||||
|
||||
// TODO Refactor and accept role, filter and action name
|
||||
export const getCreateActionPermissionQuery = (
|
||||
def: { role: string; filter: any },
|
||||
actionName: string
|
||||
) => {
|
||||
return {
|
||||
type: 'create_action_permission',
|
||||
args: {
|
||||
action: actionName,
|
||||
role: def.role,
|
||||
definition: {
|
||||
select: {
|
||||
filter: def.filter,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getUpdateActionQuery = (
|
||||
def: ActionDefinition,
|
||||
actionName: string,
|
||||
actionComment: string
|
||||
) => {
|
||||
return {
|
||||
type: 'update_action',
|
||||
args: {
|
||||
name: actionName,
|
||||
definition: def,
|
||||
comment: actionComment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropActionPermissionQuery = (
|
||||
role: string,
|
||||
actionName: string
|
||||
) => {
|
||||
return {
|
||||
type: 'drop_action_permission',
|
||||
args: {
|
||||
action: actionName,
|
||||
role,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getSetTableEnumQuery = (
|
||||
tableDef: TableDefinition,
|
||||
isEnum: boolean
|
||||
) => {
|
||||
return {
|
||||
type: 'set_table_is_enum',
|
||||
args: {
|
||||
table: tableDef,
|
||||
is_enum: isEnum,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrackTableQuery = (tableDef: TableDefinition) => {
|
||||
return {
|
||||
type: 'add_existing_table_or_view',
|
||||
args: tableDef,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUntrackTableQuery = (tableDef: TableDefinition) => {
|
||||
return {
|
||||
type: 'untrack_table',
|
||||
args: {
|
||||
table: tableDef,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getAddComputedFieldQuery = (
|
||||
tableDef: TableDefinition,
|
||||
computedFieldName: string,
|
||||
definition: any, // TODO
|
||||
comment: string
|
||||
) => {
|
||||
return {
|
||||
type: 'add_computed_field',
|
||||
args: {
|
||||
table: tableDef,
|
||||
name: computedFieldName,
|
||||
definition: {
|
||||
...definition,
|
||||
},
|
||||
comment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropComputedFieldQuery = (
|
||||
tableDef: TableDefinition,
|
||||
computedFieldName: string
|
||||
) => {
|
||||
return {
|
||||
type: 'drop_computed_field',
|
||||
args: {
|
||||
table: tableDef,
|
||||
name: computedFieldName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDeleteQuery = (
|
||||
pkClause: WhereClause,
|
||||
tableName: string,
|
||||
schemaName: string
|
||||
) => {
|
||||
return {
|
||||
type: 'delete',
|
||||
args: {
|
||||
table: {
|
||||
name: tableName,
|
||||
schema: schemaName,
|
||||
},
|
||||
where: pkClause,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getBulkDeleteQuery = (
|
||||
pkClauses: WhereClause,
|
||||
tableName: string,
|
||||
schemaName: string
|
||||
) =>
|
||||
pkClauses.map((pkClause: WhereClause) =>
|
||||
getDeleteQuery(pkClause, tableName, schemaName)
|
||||
);
|
||||
|
||||
export const getEnumOptionsQuery = (
|
||||
request: { enumTableName: string; enumColumnName: string },
|
||||
currentSchema: string
|
||||
) => {
|
||||
return {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: request.enumTableName,
|
||||
schema: currentSchema,
|
||||
},
|
||||
columns: [request.enumColumnName],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const inconsistentObjectsQuery = {
|
||||
type: 'get_inconsistent_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const dropInconsistentObjectsQuery = {
|
||||
type: 'drop_inconsistent_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const getReloadMetadataQuery = (shouldReloadRemoteSchemas: boolean) => ({
|
||||
type: 'reload_metadata',
|
||||
args: {
|
||||
reload_remote_schemas: shouldReloadRemoteSchemas,
|
||||
},
|
||||
});
|
||||
|
||||
export const getReloadRemoteSchemaCacheQuery = (remoteSchemaName: string) => {
|
||||
return {
|
||||
type: 'reload_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const exportMetadataQuery = {
|
||||
type: 'export_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
// type the metadata
|
||||
export const generateReplaceMetadataQuery = (metadataJson: any) => ({
|
||||
type: 'replace_metadata',
|
||||
args: metadataJson,
|
||||
});
|
||||
|
||||
export const resetMetadataQuery = {
|
||||
type: 'clear_metadata',
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const fetchEventTriggersQuery = {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*'],
|
||||
order_by: [{ column: 'name', type: 'asc' }],
|
||||
},
|
||||
};
|
||||
|
||||
export const fetchScheduledTriggersQuery = {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_cron_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*'],
|
||||
order_by: [{ column: 'name', type: 'asc' }],
|
||||
},
|
||||
};
|
||||
|
||||
export const getBulkQuery = (args: any[]) => {
|
||||
return {
|
||||
type: 'bulk',
|
||||
args,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateCreateEventTriggerQuery = (
|
||||
state: LocalEventTriggerState,
|
||||
replace = false
|
||||
) => {
|
||||
return {
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
name: state.name.trim(),
|
||||
table: state.table,
|
||||
webhook:
|
||||
state.webhook.type === 'static' ? state.webhook.value.trim() : null,
|
||||
webhook_from_env:
|
||||
state.webhook.type === 'env' ? state.webhook.value.trim() : null,
|
||||
insert: state.operations.insert
|
||||
? {
|
||||
columns: '*',
|
||||
}
|
||||
: null,
|
||||
update: state.operations.update
|
||||
? {
|
||||
columns: state.operationColumns.map(c => c.name),
|
||||
payload: state.operationColumns.map(c => c.name),
|
||||
}
|
||||
: null,
|
||||
delete: state.operations.delete
|
||||
? {
|
||||
columns: '*',
|
||||
}
|
||||
: null,
|
||||
enable_manual: state.operations.enable_manual,
|
||||
retry_conf: state.retryConf,
|
||||
headers: transformHeaders(state.headers),
|
||||
replace,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropEventTriggerQuery = (name: string) => ({
|
||||
type: 'delete_event_trigger',
|
||||
args: {
|
||||
name: name.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
export const generateCreateScheduledTriggerQuery = (
|
||||
state: LocalScheduledTriggerState,
|
||||
replace = false
|
||||
) => ({
|
||||
type: 'create_cron_trigger',
|
||||
args: {
|
||||
name: state.name.trim(),
|
||||
webhook: state.webhook,
|
||||
schedule: state.schedule,
|
||||
payload: JSON.parse(state.payload),
|
||||
headers: transformHeaders(state.headers),
|
||||
retry_conf: {
|
||||
num_retries: state.retryConf.num_retries,
|
||||
retry_interval_seconds: state.retryConf.interval_sec,
|
||||
timeout_seconds: state.retryConf.timeout_sec,
|
||||
tolerance_seconds: state.retryConf.tolerance_sec,
|
||||
},
|
||||
comment: state.comment,
|
||||
include_in_metadata: state.includeInMetadata,
|
||||
replace,
|
||||
},
|
||||
});
|
||||
|
||||
export const generateUpdateScheduledTriggerQuery = (
|
||||
state: LocalScheduledTriggerState
|
||||
) => generateCreateScheduledTriggerQuery(state, true);
|
||||
|
||||
export const getDropScheduledTriggerQuery = (name: string) => ({
|
||||
type: 'delete_cron_trigger',
|
||||
args: {
|
||||
name: name.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
export const getCreateScheduledEventQuery = (state: LocalAdhocEventState) => {
|
||||
return {
|
||||
type: 'create_scheduled_event',
|
||||
args: {
|
||||
webhook: state.webhook,
|
||||
schedule_at: state.time.toISOString(),
|
||||
headers: transformHeaders(state.headers),
|
||||
retry_conf: {
|
||||
num_retries: state.retryConf.num_retries,
|
||||
retry_interval_seconds: state.retryConf.interval_sec,
|
||||
timeout_seconds: state.retryConf.timeout_sec,
|
||||
tolerance_seconds: state.retryConf.tolerance_sec,
|
||||
},
|
||||
payload: state.payload,
|
||||
comment: state.comment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type SelectColumn = string | { name: string; columns: SelectColumn[] };
|
||||
|
||||
export const getSelectQuery = (
|
||||
type: 'select' | 'count',
|
||||
table: TableDefinition,
|
||||
columns: SelectColumn[],
|
||||
where: Nullable<WhereClause>,
|
||||
offset: Nullable<number>,
|
||||
limit: Nullable<number>,
|
||||
order_by: Nullable<OrderBy[]>
|
||||
) => {
|
||||
return {
|
||||
type,
|
||||
args: {
|
||||
table,
|
||||
columns,
|
||||
where,
|
||||
offset,
|
||||
limit,
|
||||
order_by,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getFetchInvocationLogsQuery = (
|
||||
where: Nullable<WhereClause>,
|
||||
offset: Nullable<number>,
|
||||
order_by: Nullable<OrderBy[]>,
|
||||
limit: Nullable<number>
|
||||
) => {
|
||||
return getSelectQuery(
|
||||
'select',
|
||||
generateTableDef('hdb_scheduled_event_invocation_logs', 'hdb_catalog'),
|
||||
['*'],
|
||||
where,
|
||||
offset,
|
||||
limit,
|
||||
order_by
|
||||
);
|
||||
};
|
||||
|
||||
export type SelectQueryGenerator = typeof getFetchInvocationLogsQuery;
|
||||
|
||||
export const getFetchManualTriggersQuery = (tableDef: TableDefinition) =>
|
||||
getSelectQuery(
|
||||
'select',
|
||||
generateTableDef('event_triggers', 'hdb_catalog'),
|
||||
['*'],
|
||||
{
|
||||
table_name: tableDef.name,
|
||||
schema_name: tableDef.schema,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
column: 'name',
|
||||
type: 'asc',
|
||||
nulls: 'last',
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
export const getRedeliverDataEventQuery = (eventId: string) => ({
|
||||
type: 'redeliver_event',
|
||||
args: {
|
||||
event_id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
export const getSaveRemoteRelQuery = (
|
||||
args: RemoteRelationshipPayload,
|
||||
isNew: boolean
|
||||
) => ({
|
||||
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
||||
args,
|
||||
});
|
||||
|
||||
export const getDropRemoteRelQuery = (
|
||||
name: string,
|
||||
table: TableDefinition
|
||||
) => ({
|
||||
type: 'delete_remote_relationship',
|
||||
args: {
|
||||
name,
|
||||
table,
|
||||
},
|
||||
});
|
||||
|
||||
export const getRemoteSchemaIntrospectionQuery = (
|
||||
remoteSchemaName: string
|
||||
) => ({
|
||||
type: 'introspect_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
},
|
||||
});
|
||||
|
||||
export const getConsoleOptsQuery = () =>
|
||||
getSelectQuery(
|
||||
'select',
|
||||
{ name: 'hdb_version', schema: 'hdb_catalog' },
|
||||
['hasura_uuid', 'console_state'],
|
||||
{},
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
@ -1,54 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
import globals from '../../Globals';
|
||||
|
||||
export class NotFoundError extends Error {}
|
||||
|
||||
class PageNotFound extends Component {
|
||||
render() {
|
||||
const errorImage = `${globals.assetsPath}/common/img/hasura_icon_green.svg`;
|
||||
const styles = require('./ErrorPage.scss');
|
||||
|
||||
const { resetCallback } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.viewContainer}>
|
||||
<Helmet title="404 - Page Not Found | Hasura" />
|
||||
<div className={'container ' + styles.centerContent}>
|
||||
<div className={'row ' + styles.message}>
|
||||
<div className="col-xs-8">
|
||||
<h1>404</h1>
|
||||
<br />
|
||||
<div>
|
||||
This page doesn't exist. Head back{' '}
|
||||
<Link to="/" onClick={resetCallback}>
|
||||
Home
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-xs-4">
|
||||
<img
|
||||
src={errorImage}
|
||||
className="img-responsive"
|
||||
name="hasura"
|
||||
title="We think you are lost!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageNotFound.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
resetCallback: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect()(PageNotFound);
|
50
console/src/components/Error/PageNotFound.tsx
Normal file
50
console/src/components/Error/PageNotFound.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
import globals from '../../Globals';
|
||||
import styles from './ErrorPage.scss';
|
||||
|
||||
export class NotFoundError extends Error {}
|
||||
|
||||
type PageNotFoundProps = {
|
||||
resetCallback: () => void;
|
||||
};
|
||||
|
||||
const PageNotFound = (props: PageNotFoundProps) => {
|
||||
const errorImage = `${globals.assetsPath}/common/img/hasura_icon_green.svg`;
|
||||
|
||||
const { resetCallback } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.viewContainer}>
|
||||
<Helmet title="404 - Page Not Found | Hasura" />
|
||||
<div className={`container ${styles.centerContent}`}>
|
||||
<div className={`row ${styles.message}`}>
|
||||
<div className="col-xs-8">
|
||||
<h1>404</h1>
|
||||
<br />
|
||||
<div>
|
||||
This page does not exist. Head back{' '}
|
||||
<Link to="/" onClick={resetCallback}>
|
||||
Home
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-xs-4">
|
||||
<img
|
||||
src={errorImage}
|
||||
className="img-responsive"
|
||||
title="We think you are lost!"
|
||||
alt="Not found"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect()(PageNotFound);
|
@ -739,12 +739,20 @@ class Main extends React.Component {
|
||||
tooltips.remoteSchema,
|
||||
'/remote-schemas/manage/schemas'
|
||||
)}
|
||||
{getSidebarItem(
|
||||
{/* {getSidebarItem(
|
||||
'Events',
|
||||
'fa-cloud',
|
||||
tooltips.events,
|
||||
'/events/manage/triggers'
|
||||
)}
|
||||
{' '}
|
||||
*/}{' '}
|
||||
{getSidebarItem(
|
||||
'Events',
|
||||
'fa-cloud',
|
||||
tooltips.events,
|
||||
'/events/data/manage'
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="dropdown_wrapper" className={styles.clusterInfoWrapper}>
|
||||
|
@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class TopicDescription extends React.Component {
|
||||
render() {
|
||||
const Rectangle = require('./images/Rectangle.svg');
|
||||
const styles = require('../../RemoteSchema/RemoteSchema.scss');
|
||||
const { title, imgUrl, imgAlt, description } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.subHeaderText}>
|
||||
<img className={'img-responsive'} src={Rectangle} alt={'Rectangle'} />
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.remoteSchemaImg}>
|
||||
<img className={'img-responsive'} src={imgUrl} alt={imgAlt} />
|
||||
</div>
|
||||
<div className={styles.descriptionText + ' ' + styles.wd60}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TopicDescription.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
imgUrl: PropTypes.string.isRequired,
|
||||
imgAlt: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
};
|
||||
export default TopicDescription;
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import styles from '../../RemoteSchema/RemoteSchema.scss';
|
||||
|
||||
const Rectangle = require('./images/Rectangle.svg');
|
||||
|
||||
type TopicDescriptionProps = {
|
||||
title: string;
|
||||
imgUrl: string;
|
||||
imgAlt: string;
|
||||
description: React.ReactNode;
|
||||
};
|
||||
|
||||
const TopicDescription = (props: TopicDescriptionProps) => {
|
||||
const { title, imgUrl, imgAlt, description } = props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.subHeaderText}>
|
||||
<img className="img-responsive" src={Rectangle} alt="Rectangle" />
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.remoteSchemaImg}>
|
||||
<img className="img-responsive" src={imgUrl} alt={imgAlt} />
|
||||
</div>
|
||||
<div className={`${styles.descriptionText} ${styles.wd60}`}>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicDescription;
|
@ -1,16 +1,63 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { showNotification } from '../../App/Actions';
|
||||
import {
|
||||
removeAll as removeNotifications,
|
||||
show as displayNotification,
|
||||
NotificationLevel,
|
||||
} from 'react-notification-system-redux';
|
||||
import Button from '../../Common/Button/Button';
|
||||
import { Thunk } from '../../../types';
|
||||
import { Json } from '../../Common/utils/tsUtils';
|
||||
|
||||
import './Notification/NotificationOverrides.css';
|
||||
import { isObject, isString } from '../../Common/utils/jsUtils';
|
||||
|
||||
const styles = require('./Notification/Notification.scss');
|
||||
|
||||
const getNotificationDetails = (detailsJson, children = null) => {
|
||||
export interface Notification {
|
||||
title?: string | JSX.Element;
|
||||
message?: string | JSX.Element;
|
||||
level?: 'error' | 'warning' | 'info' | 'success';
|
||||
position?: 'tr' | 'tl' | 'tc' | 'br' | 'bl' | 'bc';
|
||||
autoDismiss?: number;
|
||||
dismissible?: boolean;
|
||||
children?: React.ReactNode;
|
||||
uid?: number | string;
|
||||
action?: {
|
||||
label: string;
|
||||
callback?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const showNotification = (
|
||||
options: Notification,
|
||||
level: NotificationLevel
|
||||
): Thunk => {
|
||||
return dispatch => {
|
||||
if (level === 'success') {
|
||||
dispatch(removeNotifications());
|
||||
}
|
||||
|
||||
dispatch(
|
||||
displayNotification(
|
||||
{
|
||||
position: options.position || 'tr',
|
||||
autoDismiss: ['error', 'warning'].includes(level) ? 0 : 5,
|
||||
dismissible: ['error', 'warning'].includes(level),
|
||||
...options,
|
||||
},
|
||||
level
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const getNotificationDetails = (
|
||||
detailsJson: Json,
|
||||
children: React.ReactNode
|
||||
) => {
|
||||
return (
|
||||
<div className={'notification-details'}>
|
||||
<div className="notification-details">
|
||||
<AceEditor
|
||||
readOnly
|
||||
showPrintMargin={false}
|
||||
@ -28,7 +75,11 @@ const getNotificationDetails = (detailsJson, children = null) => {
|
||||
);
|
||||
};
|
||||
|
||||
const showErrorNotification = (title, message, error) => {
|
||||
const showErrorNotification = (
|
||||
title: string,
|
||||
message: string,
|
||||
error?: Record<string, any>
|
||||
): Thunk => {
|
||||
const getErrorMessage = () => {
|
||||
let notificationMessage;
|
||||
|
||||
@ -41,10 +92,9 @@ const showErrorNotification = (title, message, error) => {
|
||||
error.message.error === 'query execution failed')
|
||||
) {
|
||||
if (error.message.internal) {
|
||||
notificationMessage =
|
||||
error.message.code + ': ' + error.message.internal.error.message;
|
||||
notificationMessage = `${error.message.code}: ${error.message.internal.error.message}`;
|
||||
} else {
|
||||
notificationMessage = error.code + ': ' + error.message.error;
|
||||
notificationMessage = `${error.code}: ${error.message.error}`;
|
||||
}
|
||||
} else if ('info' in error) {
|
||||
notificationMessage = error.info;
|
||||
@ -58,12 +108,12 @@ const showErrorNotification = (title, message, error) => {
|
||||
} else if (error.message && isString(error.message)) {
|
||||
notificationMessage = error.message;
|
||||
} else if (error.message && 'code' in error.message) {
|
||||
notificationMessage = error.message.code + ' : ' + message;
|
||||
notificationMessage = `${error.message.code} : ${message}`;
|
||||
} else {
|
||||
notificationMessage = error.code;
|
||||
}
|
||||
} else if ('internal' in error && 'error' in error.internal) {
|
||||
notificationMessage = error.code + ' : ' + error.internal.error.message;
|
||||
notificationMessage = `${error.code} : ${error.internal.error.message}`;
|
||||
} else if ('custom' in error) {
|
||||
notificationMessage = error.custom;
|
||||
} else if ('code' in error && 'error' in error && 'path' in error) {
|
||||
@ -78,9 +128,8 @@ const showErrorNotification = (title, message, error) => {
|
||||
};
|
||||
|
||||
const getRefreshBtn = () => {
|
||||
let refreshBtn;
|
||||
if (error && 'action' in error) {
|
||||
refreshBtn = (
|
||||
return (
|
||||
<Button
|
||||
className={styles.add_mar_top_small}
|
||||
color="yellow"
|
||||
@ -94,8 +143,7 @@ const showErrorNotification = (title, message, error) => {
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return refreshBtn;
|
||||
return null;
|
||||
};
|
||||
|
||||
const getErrorJson = () => {
|
||||
@ -119,7 +167,7 @@ const showErrorNotification = (title, message, error) => {
|
||||
|
||||
return dispatch => {
|
||||
const getNotificationAction = () => {
|
||||
let action = null;
|
||||
let action;
|
||||
|
||||
if (errorJson) {
|
||||
const errorDetails = [
|
||||
@ -130,13 +178,15 @@ const showErrorNotification = (title, message, error) => {
|
||||
label: 'Details',
|
||||
callback: () => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'error',
|
||||
position: 'br', // HACK: to avoid expansion of existing notifications
|
||||
title,
|
||||
message: errorMessage,
|
||||
children: errorDetails,
|
||||
})
|
||||
showNotification(
|
||||
{
|
||||
position: 'br',
|
||||
title,
|
||||
message: errorMessage,
|
||||
children: errorDetails,
|
||||
},
|
||||
'error'
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -146,53 +196,68 @@ const showErrorNotification = (title, message, error) => {
|
||||
};
|
||||
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'error',
|
||||
title,
|
||||
message: errorMessage,
|
||||
action: getNotificationAction(),
|
||||
})
|
||||
showNotification(
|
||||
{
|
||||
title,
|
||||
message: errorMessage,
|
||||
action: getNotificationAction(),
|
||||
},
|
||||
'error'
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showSuccessNotification = (title, message) => {
|
||||
const showSuccessNotification = (title: string, message?: string): Thunk => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'success',
|
||||
title,
|
||||
message: message ? message : null,
|
||||
})
|
||||
showNotification(
|
||||
{
|
||||
level: 'success',
|
||||
title,
|
||||
message,
|
||||
},
|
||||
'success'
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showInfoNotification = title => {
|
||||
const showInfoNotification = (title: string): Thunk => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
title,
|
||||
autoDismiss: 0,
|
||||
})
|
||||
showNotification(
|
||||
{
|
||||
title,
|
||||
autoDismiss: 0,
|
||||
},
|
||||
'info'
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showWarningNotification = (title, message, dataObj) => {
|
||||
const children = [];
|
||||
const showWarningNotification = (
|
||||
title: string,
|
||||
message: string,
|
||||
dataObj: Json
|
||||
): Thunk => {
|
||||
const children: JSX.Element[] = [];
|
||||
if (dataObj) {
|
||||
children.push(getNotificationDetails(dataObj));
|
||||
children.push(getNotificationDetails(dataObj, null));
|
||||
}
|
||||
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'warning',
|
||||
title,
|
||||
message,
|
||||
children,
|
||||
})
|
||||
showNotification(
|
||||
{
|
||||
level: 'warning',
|
||||
title,
|
||||
message,
|
||||
children,
|
||||
},
|
||||
'warning'
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
@ -463,7 +463,7 @@ class AddTable extends Component {
|
||||
<div
|
||||
className={`${styles.addTablesBody} ${styles.clear_fix} ${styles.padd_left}`}
|
||||
>
|
||||
<Helmet title="Add Table - Data | Hasura" />
|
||||
<Helmet title={`Add Table - Data | Hasura`} />
|
||||
<div className={styles.subHeader}>
|
||||
<h2 className={styles.heading_text}>Add a new table</h2>
|
||||
<div className="clearfix" />
|
||||
|
7
console/src/components/Services/Data/Common/Headers.ts
Normal file
7
console/src/components/Services/Data/Common/Headers.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { GetReduxState } from '../../../../types';
|
||||
|
||||
const dataHeaders = (getState: GetReduxState) => {
|
||||
return getState().tables.dataHeaders;
|
||||
};
|
||||
|
||||
export default dataHeaders;
|
@ -12,7 +12,7 @@ import globals from '../../../../Globals';
|
||||
import returnMigrateUrl from '../Common/getMigrateUrl';
|
||||
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants';
|
||||
import { loadMigrationStatus } from '../../../Main/Actions';
|
||||
import { handleMigrationErrors } from '../../EventTrigger/EventActions';
|
||||
import { handleMigrationErrors } from '../../../../utils/migration';
|
||||
|
||||
import { showSuccessNotification } from '../../Common/Notification';
|
||||
|
||||
|
@ -11,7 +11,7 @@ import dataHeaders from '../Common/Headers';
|
||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
import {
|
||||
getBulkDeleteQuery,
|
||||
generateSelectQuery,
|
||||
getSelectQuery,
|
||||
getFetchManualTriggersQuery,
|
||||
getDeleteQuery,
|
||||
getRunSqlQuery,
|
||||
@ -73,10 +73,14 @@ const vMakeRowsRequest = () => {
|
||||
const requestBody = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
generateSelectQuery(
|
||||
getSelectQuery(
|
||||
'select',
|
||||
generateTableDef(originalTable, currentSchema),
|
||||
view.query
|
||||
view.query.columns,
|
||||
view.query.where,
|
||||
view.query.offset,
|
||||
view.query.limit,
|
||||
view.query.order_by
|
||||
),
|
||||
getRunSqlQuery(getEstimateCountQuery(currentSchema, originalTable)),
|
||||
],
|
||||
@ -124,10 +128,14 @@ const vMakeCountRequest = () => {
|
||||
} = getState().tables;
|
||||
const url = Endpoints.query;
|
||||
|
||||
const requestBody = generateSelectQuery(
|
||||
const requestBody = getSelectQuery(
|
||||
'count',
|
||||
generateTableDef(originalTable, currentSchema),
|
||||
view.query
|
||||
view.query.columns,
|
||||
view.query.where,
|
||||
view.query.offset,
|
||||
view.query.limit,
|
||||
view.query.order_by
|
||||
);
|
||||
|
||||
const options = {
|
||||
@ -176,7 +184,10 @@ const vMakeTableRequests = () => (dispatch, getState) => {
|
||||
const fetchManualTriggers = tableName => {
|
||||
return (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const body = getFetchManualTriggersQuery(tableName);
|
||||
const { currentSchema } = getState().tables;
|
||||
const body = getFetchManualTriggersQuery(
|
||||
generateTableDef(tableName, currentSchema)
|
||||
);
|
||||
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
|
@ -7,7 +7,7 @@ import DragFoldTable, {
|
||||
|
||||
import Dropdown from '../../../Common/Dropdown/Dropdown';
|
||||
|
||||
import InvokeManualTrigger from '../../EventTrigger/Common/InvokeManualTrigger/InvokeManualTrigger';
|
||||
import InvokeManualTrigger from '../../Events/EventTriggers/InvokeManualTrigger/InvokeManualTrigger';
|
||||
|
||||
import {
|
||||
vExpandRel,
|
||||
|
@ -54,14 +54,12 @@ import {
|
||||
getUntrackTableQuery,
|
||||
getTrackTableQuery,
|
||||
} from '../../../Common/utils/v1QueryUtils';
|
||||
|
||||
import {
|
||||
fetchColumnCastsQuery,
|
||||
convertArrayToJson,
|
||||
sanitiseRootFields,
|
||||
sanitiseColumnNames,
|
||||
} from './utils';
|
||||
|
||||
import {
|
||||
getSchemaBaseRoute,
|
||||
getTableModifyRoute,
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction } from 'redux';
|
||||
import Button from '../../../Common/Button';
|
||||
import { isJsonString, getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
import { FilterState } from './utils';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import { permChangePermissions, permChangeTypes } from './Actions';
|
||||
import styles from '../../../Common/Permissions/PermissionStyles.scss';
|
||||
import { Dispatch } from '../../../../types';
|
||||
|
||||
interface PermButtonSectionProps {
|
||||
readOnlyMode: string;
|
||||
query: string;
|
||||
localFilterString: FilterState;
|
||||
dispatch: (d: ThunkDispatch<{}, {}, AnyAction>) => void;
|
||||
dispatch: Dispatch;
|
||||
permissionsState: FilterState;
|
||||
permsChanged: string;
|
||||
currQueryPermissions: string;
|
||||
|
@ -3,11 +3,13 @@ import styles from '../../TableModify/ModifyTable.scss';
|
||||
import { RemoteRelationshipServer } from './utils';
|
||||
import RemoteRelationshipList from './components/RemoteRelationshipList';
|
||||
import { fetchRemoteSchemas } from '../../../RemoteSchema/Actions';
|
||||
import { Table } from '../../../../Common/utils/pgUtils';
|
||||
import { Dispatch } from '../../../../../types';
|
||||
|
||||
type Props = {
|
||||
relationships: RemoteRelationshipServer[];
|
||||
reduxDispatch: any;
|
||||
table: any;
|
||||
reduxDispatch: Dispatch;
|
||||
table: Table;
|
||||
remoteSchemas: string[];
|
||||
};
|
||||
|
||||
|
@ -22,9 +22,10 @@ import {
|
||||
Configuration as ConfigTooltip,
|
||||
} from '../Tooltips';
|
||||
import Explorer from './Explorer';
|
||||
import { Table } from '../../../../../Common/utils/pgUtils';
|
||||
|
||||
type Props = {
|
||||
table: any; // TODO use "Table" type after ST is merged
|
||||
table: Table;
|
||||
remoteSchemas: string[];
|
||||
isLast: boolean;
|
||||
state: RemoteRelationship;
|
||||
|
@ -5,13 +5,15 @@ import RemoteRelEditor from './RemoteRelEditor';
|
||||
import RemoteRelCollapsedLabel from './EditorCollapsed';
|
||||
import { useRemoteRelationship } from '../state';
|
||||
import { saveRemoteRelationship, dropRemoteRelationship } from '../../Actions';
|
||||
import { Table } from '../../../../../Common/utils/pgUtils';
|
||||
import { Dispatch } from '../../../../../../types';
|
||||
|
||||
type Props = {
|
||||
relationship?: RemoteRelationshipServer;
|
||||
table: any;
|
||||
table: Table;
|
||||
isLast: boolean;
|
||||
remoteSchemas: string[];
|
||||
reduxDispatch: any; // TODO use Dispatch after ST is merged
|
||||
reduxDispatch: Dispatch;
|
||||
};
|
||||
|
||||
const EditorWrapper: React.FC<Props> = ({
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { RemoteRelationshipServer } from '../utils';
|
||||
import RemoteRelationshipEditor from './RemoteRelEditorWrapper';
|
||||
import { Table } from '../../../../../Common/utils/pgUtils';
|
||||
import { Dispatch } from '../../../../../../types';
|
||||
|
||||
type Props = {
|
||||
relationships: RemoteRelationshipServer[];
|
||||
table: any;
|
||||
table: Table;
|
||||
remoteSchemas: string[];
|
||||
reduxDispatch: any; // TODO use Dispatch after ST is merged
|
||||
reduxDispatch: Dispatch;
|
||||
};
|
||||
|
||||
const RemoteRelationshipList: React.FC<Props> = ({
|
||||
|
@ -13,8 +13,9 @@ import {
|
||||
TreeArgElement,
|
||||
ArgValueKind,
|
||||
} from './utils';
|
||||
import { Table } from '../../../../Common/utils/pgUtils';
|
||||
|
||||
const getDefaultState = (table: any): RemoteRelationship => ({
|
||||
const getDefaultState = (table: Table): RemoteRelationship => ({
|
||||
name: '',
|
||||
remoteSchema: '',
|
||||
remoteFields: [],
|
||||
@ -224,7 +225,7 @@ const reducer = (
|
||||
|
||||
// type "table" once ST PR is merged
|
||||
export const useRemoteRelationship = (
|
||||
table: any,
|
||||
table: Table,
|
||||
relationship?: RemoteRelationshipServer
|
||||
) => {
|
||||
const [state, dispatch] = React.useReducer(
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
isNumber,
|
||||
} from '../../../../Common/utils/jsUtils';
|
||||
import { getUnderlyingType } from '../../../../../shared/utils/graphqlSchemaUtils';
|
||||
import { TableDefinition } from '../../../../Common/utils/v1QueryUtils';
|
||||
|
||||
export type ArgValueKind = 'column' | 'static';
|
||||
export type ArgValue = {
|
||||
@ -264,7 +265,17 @@ const getTypedArgValueInput = (argValue: ArgValue, type: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getRemoteRelPayload = (relationship: RemoteRelationship) => {
|
||||
export type RemoteRelationshipPayload = {
|
||||
name: string;
|
||||
remote_schema: string;
|
||||
remote_field: Record<string, RemoteRelationshipFieldServer>;
|
||||
hasura_fields: string[];
|
||||
table: TableDefinition;
|
||||
};
|
||||
|
||||
export const getRemoteRelPayload = (
|
||||
relationship: RemoteRelationship
|
||||
): RemoteRelationshipPayload => {
|
||||
const hasuraFields: string[] = [];
|
||||
const getRemoteFieldArguments = (field: RemoteField) => {
|
||||
const getArgumentObject = (depth: number, parent?: string) => {
|
||||
@ -325,7 +336,7 @@ export const getRemoteRelPayload = (relationship: RemoteRelationship) => {
|
||||
return {
|
||||
name: relationship.name,
|
||||
remote_schema: relationship.remoteSchema,
|
||||
remote_field: getRemoteFieldObject(0),
|
||||
remote_field: getRemoteFieldObject(0) || {},
|
||||
hasura_fields: hasuraFields
|
||||
.map(f => f.substr(1))
|
||||
.filter((v, i, s) => s.indexOf(v) === i),
|
||||
|
@ -1,414 +0,0 @@
|
||||
import defaultState from './AddState';
|
||||
import _push from '../push';
|
||||
import { loadTriggers, makeMigrationCall, setTrigger } from '../EventActions';
|
||||
import { showSuccessNotification } from '../../Common/Notification';
|
||||
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
|
||||
import { updateSchemaInfo } from '../../Data/DataActions';
|
||||
|
||||
const SET_DEFAULTS = 'AddTrigger/SET_DEFAULTS';
|
||||
const SET_TRIGGERNAME = 'AddTrigger/SET_TRIGGERNAME';
|
||||
const SET_TABLENAME = 'AddTrigger/SET_TABLENAME';
|
||||
const SET_SCHEMANAME = 'AddTrigger/SET_SCHEMANAME';
|
||||
const SET_WEBHOOK_URL = 'AddTrigger/SET_WEBHOOK_URL';
|
||||
const SET_RETRY_NUM = 'AddTrigger/SET_RETRY_NUM';
|
||||
const SET_RETRY_INTERVAL = 'AddTrigger/SET_RETRY_INTERVAL';
|
||||
const SET_RETRY_TIMEOUT = 'AddTrigger/SET_RETRY_TIMEOUT';
|
||||
const MAKING_REQUEST = 'AddTrigger/MAKING_REQUEST';
|
||||
const REQUEST_SUCCESS = 'AddTrigger/REQUEST_SUCCESS';
|
||||
const REQUEST_ERROR = 'AddTrigger/REQUEST_ERROR';
|
||||
const VALIDATION_ERROR = 'AddTrigger/VALIDATION_ERROR';
|
||||
const TOGGLE_COLUMNS = 'AddTrigger/TOGGLE_COLUMNS';
|
||||
const TOGGLE_ALL_COLUMNS = 'AddTrigger/TOGGLE_ALL_COLUMNS';
|
||||
const TOGGLE_OPERATION = 'AddTrigger/TOGGLE_OPERATION';
|
||||
const TOGGLE_ENABLE_MANUAL_CONFIG = 'AddTrigger/TOGGLE_ENABLE_MANUAL_CONFIG';
|
||||
// const TOGGLE_QUERY_TYPE_SELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_SELECTED';
|
||||
// const TOGGLE_QUERY_TYPE_DESELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_DESELECTED';
|
||||
const REMOVE_HEADER = 'AddTrigger/REMOVE_HEADER';
|
||||
const SET_HEADERKEY = 'AddTrigger/SET_HEADERKEY';
|
||||
const SET_HEADERTYPE = 'AddTrigger/SET_HEADERTYPE';
|
||||
const SET_HEADERVALUE = 'AddTrigger/SET_HEADERVALUE';
|
||||
const ADD_HEADER = 'AddTrigger/ADD_HEADER';
|
||||
const UPDATE_WEBHOOK_URL_TYPE = 'AddTrigger/UPDATE_WEBHOOK_URL_TYPE';
|
||||
|
||||
const setTriggerName = value => ({ type: SET_TRIGGERNAME, value });
|
||||
const setTableName = value => ({ type: SET_TABLENAME, value });
|
||||
const setSchemaName = value => ({ type: SET_SCHEMANAME, value });
|
||||
const setWebhookURL = value => ({ type: SET_WEBHOOK_URL, value });
|
||||
const setRetryNum = value => ({ type: SET_RETRY_NUM, value });
|
||||
const setRetryInterval = value => ({ type: SET_RETRY_INTERVAL, value });
|
||||
const setRetryTimeout = value => ({ type: SET_RETRY_TIMEOUT, value });
|
||||
const setDefaults = () => ({ type: SET_DEFAULTS });
|
||||
const addHeader = () => ({ type: ADD_HEADER });
|
||||
const removeHeader = i => ({ type: REMOVE_HEADER, index: i });
|
||||
const setHeaderKey = (key, index) => ({
|
||||
type: SET_HEADERKEY,
|
||||
key,
|
||||
index,
|
||||
});
|
||||
const setHeaderType = (headerType, index) => ({
|
||||
type: SET_HEADERTYPE,
|
||||
headerType,
|
||||
index,
|
||||
});
|
||||
const setHeaderValue = (headerValue, index) => ({
|
||||
type: SET_HEADERVALUE,
|
||||
headerValue,
|
||||
index,
|
||||
});
|
||||
|
||||
// General error during validation.
|
||||
// const validationError = (error) => ({type: VALIDATION_ERROR, error: error});
|
||||
const validationError = error => {
|
||||
alert(error);
|
||||
return { type: VALIDATION_ERROR, error };
|
||||
};
|
||||
|
||||
const getWebhookKey = (type, val) => {
|
||||
return { [type === 'url' ? 'webhook' : 'webhook_from_env']: val };
|
||||
};
|
||||
|
||||
const createTrigger = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: MAKING_REQUEST });
|
||||
dispatch(showSuccessNotification('Creating Trigger...'));
|
||||
const currentState = getState().addTrigger;
|
||||
const currentSchema = currentState.schemaName;
|
||||
const triggerName = currentState.triggerName;
|
||||
const tableName = currentState.tableName;
|
||||
const webhook = currentState.webhookURL;
|
||||
const webhookType = currentState.webhookUrlType;
|
||||
|
||||
// apply migrations
|
||||
const migrationName = 'create_trigger_' + triggerName.trim();
|
||||
const payload = {
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
table: { name: tableName, schema: currentSchema },
|
||||
...getWebhookKey(webhookType, webhook),
|
||||
},
|
||||
};
|
||||
const downPayload = {
|
||||
type: 'delete_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
},
|
||||
};
|
||||
// operation definition
|
||||
if (currentState.selectedOperations.insert) {
|
||||
payload.args.insert = { columns: currentState.operations.insert };
|
||||
}
|
||||
if ('enableManual' in currentState) {
|
||||
payload.args.enable_manual = currentState.enableManual;
|
||||
}
|
||||
if (currentState.selectedOperations.update) {
|
||||
payload.args.update = { columns: currentState.operations.update };
|
||||
}
|
||||
if (currentState.selectedOperations.delete) {
|
||||
payload.args.delete = { columns: currentState.operations.delete };
|
||||
}
|
||||
// retry logic
|
||||
if (currentState.retryConf) {
|
||||
payload.args.retry_conf = currentState.retryConf;
|
||||
}
|
||||
|
||||
payload.args.retry_conf = {
|
||||
num_retries:
|
||||
currentState.retryConf.num_retries === ''
|
||||
? 0
|
||||
: parseInt(currentState.retryConf.num_retries, 10),
|
||||
interval_sec:
|
||||
currentState.retryConf.interval_sec === ''
|
||||
? 10
|
||||
: parseInt(currentState.retryConf.interval_sec, 10),
|
||||
timeout_sec:
|
||||
currentState.retryConf.timeout_sec === ''
|
||||
? 60
|
||||
: parseInt(currentState.retryConf.timeout_sec, 10),
|
||||
};
|
||||
|
||||
// create header payload
|
||||
const headers = [];
|
||||
currentState.headers.map(header => {
|
||||
if (header.key !== '' && header.type !== '') {
|
||||
if (header.type === 'static') {
|
||||
headers.push({ name: header.key, value: header.value });
|
||||
} else if (header.type === 'env') {
|
||||
headers.push({ name: header.key, value_from_env: header.value });
|
||||
}
|
||||
}
|
||||
});
|
||||
payload.args.headers = headers;
|
||||
const upQueryArgs = [];
|
||||
upQueryArgs.push(payload);
|
||||
const downQueryArgs = [];
|
||||
downQueryArgs.push(downPayload);
|
||||
const upQuery = {
|
||||
type: 'bulk',
|
||||
args: upQueryArgs,
|
||||
};
|
||||
const downQuery = {
|
||||
type: 'bulk',
|
||||
args: downQueryArgs,
|
||||
};
|
||||
const requestMsg = 'Creating trigger...';
|
||||
const successMsg = 'Trigger Created';
|
||||
const errorMsg = 'Create trigger failed';
|
||||
|
||||
const customOnSuccess = () => {
|
||||
dispatch(setTrigger(triggerName.trim()));
|
||||
dispatch(loadTriggers([triggerName])).then(() => {
|
||||
dispatch(
|
||||
_push('/manage/triggers/' + triggerName.trim() + '/processed')
|
||||
);
|
||||
});
|
||||
return;
|
||||
};
|
||||
const customOnError = err => {
|
||||
dispatch({ type: REQUEST_ERROR, data: errorMsg });
|
||||
dispatch({ type: UPDATE_MIGRATION_STATUS_ERROR, data: err });
|
||||
return;
|
||||
};
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQuery.args,
|
||||
downQuery.args,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg,
|
||||
true
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const loadTableList = schemaName => {
|
||||
return dispatch => dispatch(updateSchemaInfo({ schemas: [schemaName] }));
|
||||
};
|
||||
|
||||
const operationToggleColumn = (column, operation) => {
|
||||
return (dispatch, getState) => {
|
||||
const currentOperations = getState().addTrigger.operations;
|
||||
const currentCols = currentOperations[operation];
|
||||
// check if column is in currentCols. if not, push
|
||||
const isExists = currentCols.includes(column);
|
||||
let finalCols = currentCols;
|
||||
if (isExists) {
|
||||
finalCols = currentCols.filter(col => col !== column);
|
||||
} else {
|
||||
finalCols.push(column);
|
||||
}
|
||||
dispatch({ type: TOGGLE_COLUMNS, cols: finalCols, op: operation });
|
||||
};
|
||||
};
|
||||
|
||||
const operationToggleAllColumns = columns => {
|
||||
return dispatch => {
|
||||
dispatch({ type: TOGGLE_ALL_COLUMNS, cols: columns });
|
||||
};
|
||||
};
|
||||
|
||||
const setOperationSelection = type => {
|
||||
return dispatch => {
|
||||
dispatch({ type: TOGGLE_OPERATION, data: type });
|
||||
/*
|
||||
if (isChecked) {
|
||||
dispatch({ type: TOGGLE_QUERY_TYPE_SELECTED, data: type });
|
||||
} else {
|
||||
dispatch({ type: TOGGLE_QUERY_TYPE_DESELECTED, data: type });
|
||||
}
|
||||
*/
|
||||
};
|
||||
};
|
||||
|
||||
const addTriggerReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case ADD_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: [...state.headers, { key: '', type: 'static', value: '' }],
|
||||
};
|
||||
case REMOVE_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, action.index),
|
||||
...state.headers.slice(action.index + 1),
|
||||
],
|
||||
};
|
||||
case SET_HEADERKEY:
|
||||
const i = action.index;
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, i),
|
||||
{ ...state.headers[i], key: action.key },
|
||||
...state.headers.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case SET_HEADERTYPE:
|
||||
const ij = action.index;
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, ij),
|
||||
{ ...state.headers[ij], type: action.headerType },
|
||||
...state.headers.slice(ij + 1),
|
||||
],
|
||||
};
|
||||
case SET_HEADERVALUE:
|
||||
const ik = action.index;
|
||||
return {
|
||||
...state,
|
||||
headers: [
|
||||
...state.headers.slice(0, ik),
|
||||
{ ...state.headers[ik], value: action.headerValue },
|
||||
...state.headers.slice(ik + 1),
|
||||
],
|
||||
};
|
||||
case SET_DEFAULTS:
|
||||
return {
|
||||
...defaultState,
|
||||
operations: {
|
||||
...defaultState.operations,
|
||||
insert: [],
|
||||
update: [],
|
||||
delete: [],
|
||||
},
|
||||
selectedOperations: {
|
||||
...defaultState.selectedOperations,
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
};
|
||||
case MAKING_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: true,
|
||||
lastError: null,
|
||||
lastSuccess: null,
|
||||
};
|
||||
case REQUEST_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: false,
|
||||
lastError: null,
|
||||
lastSuccess: true,
|
||||
};
|
||||
case REQUEST_ERROR:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: false,
|
||||
lastError: action.data,
|
||||
lastSuccess: null,
|
||||
};
|
||||
case VALIDATION_ERROR:
|
||||
return { ...state, internalError: action.error, lastSuccess: null };
|
||||
case SET_TRIGGERNAME:
|
||||
return { ...state, triggerName: action.value };
|
||||
case SET_WEBHOOK_URL:
|
||||
return { ...state, webhookURL: action.value };
|
||||
case SET_RETRY_NUM:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
num_retries: action.value,
|
||||
},
|
||||
};
|
||||
case SET_RETRY_INTERVAL:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
interval_sec: action.value,
|
||||
},
|
||||
};
|
||||
case SET_RETRY_TIMEOUT:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
timeout_sec: action.value,
|
||||
},
|
||||
};
|
||||
case SET_TABLENAME:
|
||||
return { ...state, tableName: action.value };
|
||||
case SET_SCHEMANAME:
|
||||
return { ...state, schemaName: action.value };
|
||||
case TOGGLE_COLUMNS:
|
||||
const operations = state.operations;
|
||||
operations[action.op] = action.cols;
|
||||
return { ...state, operations: { ...operations } };
|
||||
case TOGGLE_ALL_COLUMNS:
|
||||
return {
|
||||
...state,
|
||||
operations: {
|
||||
insert: '*',
|
||||
delete: '*',
|
||||
update: action.cols,
|
||||
},
|
||||
};
|
||||
case TOGGLE_OPERATION:
|
||||
return {
|
||||
...state,
|
||||
selectedOperations: {
|
||||
...state.selectedOperations,
|
||||
[action.data]: !state.selectedOperations[action.data],
|
||||
},
|
||||
};
|
||||
|
||||
case TOGGLE_ENABLE_MANUAL_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
enableManual: !state.enableManual,
|
||||
};
|
||||
/*
|
||||
case TOGGLE_QUERY_TYPE_SELECTED:
|
||||
const selectedOperations = state.selectedOperations;
|
||||
selectedOperations[action.data] = true;
|
||||
return { ...state, selectedOperations: { ...selectedOperations } };
|
||||
case TOGGLE_QUERY_TYPE_DESELECTED:
|
||||
const deselectedOperations = state.selectedOperations;
|
||||
deselectedOperations[action.data] = false;
|
||||
return { ...state, selectedOperations: { ...deselectedOperations } };
|
||||
*/
|
||||
case UPDATE_WEBHOOK_URL_TYPE:
|
||||
return {
|
||||
...state,
|
||||
webhookUrlType: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default addTriggerReducer;
|
||||
export {
|
||||
addHeader,
|
||||
setHeaderKey,
|
||||
setHeaderValue,
|
||||
setHeaderType,
|
||||
removeHeader,
|
||||
setTriggerName,
|
||||
setTableName,
|
||||
setSchemaName,
|
||||
setWebhookURL,
|
||||
setRetryNum,
|
||||
setRetryInterval,
|
||||
setRetryTimeout,
|
||||
createTrigger,
|
||||
loadTableList,
|
||||
operationToggleColumn,
|
||||
operationToggleAllColumns,
|
||||
setOperationSelection,
|
||||
setDefaults,
|
||||
UPDATE_WEBHOOK_URL_TYPE,
|
||||
TOGGLE_ENABLE_MANUAL_CONFIG,
|
||||
};
|
||||
export { validationError };
|
@ -1,26 +0,0 @@
|
||||
const defaultState = {
|
||||
triggerName: '',
|
||||
tableName: '',
|
||||
schemaName: 'public',
|
||||
operations: { insert: [], update: [], delete: [] },
|
||||
enableManual: false,
|
||||
selectedOperations: {
|
||||
insert: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
webhookURL: '',
|
||||
webhookUrlType: 'url',
|
||||
retryConf: {
|
||||
num_retries: 0,
|
||||
interval_sec: 10,
|
||||
timeout_sec: 60,
|
||||
},
|
||||
ongoingRequest: false,
|
||||
lastError: null,
|
||||
internalError: null,
|
||||
lastSuccess: null,
|
||||
headers: [{ key: '', type: 'static', value: '' }],
|
||||
};
|
||||
|
||||
export default defaultState;
|
@ -1,629 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import * as tooltip from './Tooltips';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import Button from '../../../Common/Button/Button';
|
||||
import Operations from './Operations';
|
||||
|
||||
import {
|
||||
removeHeader,
|
||||
setHeaderKey,
|
||||
setHeaderValue,
|
||||
setHeaderType,
|
||||
addHeader,
|
||||
setTriggerName,
|
||||
setTableName,
|
||||
setSchemaName,
|
||||
setWebhookURL,
|
||||
setRetryNum,
|
||||
setRetryInterval,
|
||||
setRetryTimeout,
|
||||
operationToggleColumn,
|
||||
operationToggleAllColumns,
|
||||
setOperationSelection,
|
||||
setDefaults,
|
||||
UPDATE_WEBHOOK_URL_TYPE,
|
||||
loadTableList,
|
||||
} from './AddActions';
|
||||
import { listDuplicate } from '../../../../utils/data';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
import { createTrigger } from './AddActions';
|
||||
|
||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
||||
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
|
||||
import {
|
||||
getOnlyTables,
|
||||
getSchemaName,
|
||||
getSchemaTables,
|
||||
getTableName,
|
||||
getTrackedTables,
|
||||
} from '../../../Common/utils/pgUtils';
|
||||
|
||||
class AddTrigger extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props.dispatch(loadTableList('public'));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// set defaults
|
||||
this.props.dispatch(setDefaults());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// set defaults
|
||||
this.props.dispatch(setDefaults());
|
||||
}
|
||||
|
||||
updateWebhookUrlType(e) {
|
||||
const field = e.target.getAttribute('value');
|
||||
if (field === 'env' || field === 'url') {
|
||||
this.props.dispatch({ type: UPDATE_WEBHOOK_URL_TYPE, data: field });
|
||||
this.props.dispatch(setWebhookURL(''));
|
||||
}
|
||||
}
|
||||
|
||||
submitValidation(e) {
|
||||
// validations
|
||||
e.preventDefault();
|
||||
let isValid = true;
|
||||
let errorMsg = '';
|
||||
let customMsg = '';
|
||||
if (this.props.triggerName === '') {
|
||||
isValid = false;
|
||||
errorMsg = 'Trigger name cannot be empty';
|
||||
customMsg = 'Trigger name cannot be empty. Please add a name';
|
||||
} else if (!this.props.tableName) {
|
||||
isValid = false;
|
||||
errorMsg = 'Table cannot be empty';
|
||||
customMsg = 'Please select a table name';
|
||||
} else if (this.props.webhookURL === '') {
|
||||
isValid = false;
|
||||
errorMsg = 'Webhook URL cannot be empty';
|
||||
customMsg = 'Webhook URL cannot be empty. Please add a valid URL';
|
||||
} else if (this.props.retryConf) {
|
||||
const iNumRetries =
|
||||
this.props.retryConf.num_retries === ''
|
||||
? 0
|
||||
: parseInt(this.props.retryConf.num_retries, 10);
|
||||
const iRetryInterval =
|
||||
this.props.retryConf.interval_sec === ''
|
||||
? 10
|
||||
: parseInt(this.props.retryConf.interval_sec, 10);
|
||||
const iTimeout =
|
||||
this.props.retryConf.timeout_sec === ''
|
||||
? 60
|
||||
: parseInt(this.props.retryConf.timeout_sec, 10);
|
||||
|
||||
if (iNumRetries < 0 || isNaN(iNumRetries)) {
|
||||
isValid = false;
|
||||
errorMsg = 'Number of retries is not valid';
|
||||
customMsg = 'Numer of retries must be a non-negative number';
|
||||
}
|
||||
if (iRetryInterval <= 0 || isNaN(iRetryInterval)) {
|
||||
isValid = false;
|
||||
errorMsg = 'Retry interval is not valid';
|
||||
customMsg = 'Retry interval must be a postiive number';
|
||||
}
|
||||
if (isNaN(iTimeout) || iTimeout <= 0) {
|
||||
isValid = false;
|
||||
errorMsg = 'Timeout is not valid';
|
||||
customMsg = 'Timeout must be a positive number';
|
||||
}
|
||||
} else if (this.props.selectedOperations.insert) {
|
||||
// check if columns are selected.
|
||||
if (this.props.operations.insert.length === 0) {
|
||||
isValid = false;
|
||||
errorMsg = 'No columns selected for insert operation';
|
||||
customMsg =
|
||||
'Please select a minimum of one column for insert operation';
|
||||
}
|
||||
} else if (this.props.selectedOperations.update) {
|
||||
// check if columns are selected.
|
||||
if (this.props.operations.update.length === 0) {
|
||||
isValid = false;
|
||||
errorMsg = 'No columns selected for update operation';
|
||||
customMsg =
|
||||
'Please select a minimum of one column for update operation';
|
||||
}
|
||||
} else if (this.props.headers.length === 1) {
|
||||
if (this.props.headers[0].key !== '') {
|
||||
// let the default value through and ignore it while querying?
|
||||
// Need a better method
|
||||
if (this.props.headers[0].type === '') {
|
||||
isValid = false;
|
||||
errorMsg = 'No type selected for trigger header';
|
||||
customMsg = 'Please select a type for the trigger header';
|
||||
}
|
||||
}
|
||||
} else if (this.props.headers.length > 1) {
|
||||
// repitition check
|
||||
const repeatList = listDuplicate(
|
||||
this.props.headers.map(header => header.key)
|
||||
);
|
||||
if (repeatList.length > 0) {
|
||||
isValid = false;
|
||||
errorMsg = 'Duplicate entries in trigger headers';
|
||||
customMsg = `You have the following column names repeated: [${repeatList}]`;
|
||||
}
|
||||
// Check for empty header keys and key/value validation?
|
||||
}
|
||||
if (isValid) {
|
||||
this.props.dispatch(createTrigger());
|
||||
} else {
|
||||
this.props.dispatch(
|
||||
showErrorNotification('Error creating trigger!', errorMsg, {
|
||||
custom: customMsg,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
tableName,
|
||||
allSchemas,
|
||||
schemaName,
|
||||
schemaList,
|
||||
selectedOperations,
|
||||
operations,
|
||||
dispatch,
|
||||
ongoingRequest,
|
||||
lastError,
|
||||
lastSuccess,
|
||||
internalError,
|
||||
headers,
|
||||
webhookURL,
|
||||
webhookUrlType,
|
||||
enableManual,
|
||||
} = this.props;
|
||||
|
||||
const styles = require('../TableCommon/EventTable.scss');
|
||||
|
||||
let createBtnText = 'Create Event Trigger';
|
||||
if (ongoingRequest) {
|
||||
createBtnText = 'Creating...';
|
||||
} else if (lastError) {
|
||||
createBtnText = 'Creating Failed. Try again';
|
||||
} else if (internalError) {
|
||||
createBtnText = 'Creating Failed. Try again';
|
||||
} else if (lastSuccess) {
|
||||
createBtnText = 'Created! Redirecting...';
|
||||
}
|
||||
|
||||
const handleOperationSelection = e => {
|
||||
dispatch(setOperationSelection(e.target.value));
|
||||
};
|
||||
|
||||
const updateTableList = e => {
|
||||
const selectedSchemaName = e.target.value;
|
||||
dispatch(setSchemaName(selectedSchemaName));
|
||||
dispatch(loadTableList(selectedSchemaName));
|
||||
};
|
||||
|
||||
const updateTableSelection = e => {
|
||||
const selectedTableName = e.target.value;
|
||||
dispatch(setTableName(selectedTableName));
|
||||
const tableSchema = allSchemas.find(
|
||||
t => t.table_name === selectedTableName && t.table_schema === schemaName
|
||||
);
|
||||
const columns = [];
|
||||
if (tableSchema) {
|
||||
tableSchema.columns.map(colObj => {
|
||||
const column = colObj.column_name;
|
||||
columns.push(column);
|
||||
});
|
||||
}
|
||||
dispatch(operationToggleAllColumns(columns));
|
||||
};
|
||||
|
||||
const getColumnList = type => {
|
||||
const dispatchToggleColumn = e => {
|
||||
const column = e.target.value;
|
||||
dispatch(operationToggleColumn(column, type));
|
||||
};
|
||||
const tableSchema = allSchemas.find(
|
||||
t => t.table_name === tableName && t.table_schema === schemaName
|
||||
);
|
||||
|
||||
if (!tableSchema) {
|
||||
return <i>Select a table first to get column list</i>;
|
||||
}
|
||||
|
||||
return tableSchema.columns.map((colObj, i) => {
|
||||
const column = colObj.column_name;
|
||||
const columnDataType = colObj.udt_name;
|
||||
const checked = operations[type]
|
||||
? operations[type].includes(column)
|
||||
: false;
|
||||
|
||||
const isDisabled = false;
|
||||
const inputHtml = (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
value={column}
|
||||
onChange={dispatchToggleColumn}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`${styles.padd_remove} ${styles.columnListElement}`}
|
||||
>
|
||||
<div className={'checkbox '}>
|
||||
<label>
|
||||
{inputHtml}
|
||||
{column}
|
||||
<small> ({columnDataType})</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const trackedSchemaTables = getOnlyTables(
|
||||
getTrackedTables(getSchemaTables(allSchemas, schemaName))
|
||||
);
|
||||
|
||||
const advancedColumnSection = (
|
||||
<div>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Listen columns for update
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.advancedOperationDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
</h4>
|
||||
{selectedOperations.update ? (
|
||||
<div className={styles.clear_fix + ' ' + styles.listenColumnWrapper}>
|
||||
{getColumnList('update')}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.clear_fix + ' ' + styles.listenColumnWrapper}>
|
||||
<i>Applicable only if update operation is selected.</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const headersList = headers.map((header, i) => {
|
||||
let removeIcon;
|
||||
if (i + 1 === headers.length) {
|
||||
removeIcon = <i className={`${styles.fontAwosomeClose}`} />;
|
||||
} else {
|
||||
removeIcon = (
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||
onClick={() => {
|
||||
dispatch(removeHeader(i));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.display_flex} form-group`}>
|
||||
<input
|
||||
type="text"
|
||||
className={`${styles.input} form-control ${styles.add_mar_right}`}
|
||||
value={header.key}
|
||||
placeholder="key"
|
||||
onChange={e => {
|
||||
dispatch(setHeaderKey(e.target.value, i));
|
||||
}}
|
||||
data-test={`header-${i}`}
|
||||
/>
|
||||
<div className={styles.dropDownGroup}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'Value', value: 'static' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={
|
||||
(header.type === 'static' && 'Value') ||
|
||||
(header.type === 'env' && 'From env var') ||
|
||||
'Value'
|
||||
}
|
||||
dataKey={
|
||||
(header.type === 'static' && 'static') ||
|
||||
(header.type === 'env' && 'env')
|
||||
}
|
||||
title={header.type === 'env' ? 'From env var' : 'Value'}
|
||||
dataKey={header.type === 'env' ? 'env' : 'static'}
|
||||
onButtonChange={e => {
|
||||
dispatch(setHeaderType(e.target.getAttribute('value'), i));
|
||||
}}
|
||||
onInputChange={e => {
|
||||
dispatch(setHeaderValue(e.target.value, i));
|
||||
if (i + 1 === headers.length) {
|
||||
dispatch(addHeader());
|
||||
}
|
||||
}}
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={header.value}
|
||||
id={`header-value-${i}`}
|
||||
inputPlaceHolder={
|
||||
header.type === 'env' ? 'HEADER_FROM_ENV' : 'value'
|
||||
}
|
||||
testId={`header-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div>{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.addTablesBody} ${styles.clear_fix} ${styles.padd_left}`}
|
||||
>
|
||||
<Helmet title="Create Trigger - Events | Hasura" />
|
||||
<div className={styles.subHeader}>
|
||||
<h2 className={styles.heading_text}>Create a new event trigger</h2>
|
||||
<div className="clearfix" />
|
||||
</div>
|
||||
<br />
|
||||
<div className={`container-fluid ${styles.padd_left_remove}`}>
|
||||
<form onSubmit={this.submitValidation.bind(this)}>
|
||||
<div
|
||||
className={`${styles.addCol} col-xs-12 ${styles.padd_left_remove}`}
|
||||
>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Trigger Name
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.triggerNameDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
data-test="trigger-name"
|
||||
placeholder="trigger_name"
|
||||
required
|
||||
pattern="^[A-Za-z]+[A-Za-z0-9_\\-]*$"
|
||||
className={`${styles.tableNameInput} form-control`}
|
||||
onChange={e => {
|
||||
dispatch(setTriggerName(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
<h4 className={styles.subheading_text}>
|
||||
Schema/Table
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.postgresDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
</h4>
|
||||
<select
|
||||
onChange={updateTableList}
|
||||
data-test="select-schema"
|
||||
className={styles.selectTrigger + ' form-control'}
|
||||
>
|
||||
{schemaList.map(s => {
|
||||
const sName = getSchemaName(s);
|
||||
return (
|
||||
<option
|
||||
value={sName}
|
||||
key={sName}
|
||||
selected={sName === schemaName}
|
||||
>
|
||||
{sName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<select
|
||||
onChange={updateTableSelection}
|
||||
data-test="select-table"
|
||||
required
|
||||
className={
|
||||
styles.selectTrigger + ' form-control ' + styles.add_mar_left
|
||||
}
|
||||
>
|
||||
<option value="">Select table</option>
|
||||
{trackedSchemaTables.map(t => {
|
||||
const tName = getTableName(t);
|
||||
return (
|
||||
<option key={tName} value={tName}>
|
||||
{tName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<hr />
|
||||
<div
|
||||
className={
|
||||
styles.add_mar_bottom + ' ' + styles.selectOperations
|
||||
}
|
||||
>
|
||||
<Operations
|
||||
dispatch={dispatch}
|
||||
enableManual={enableManual}
|
||||
selectedOperations={selectedOperations}
|
||||
handleOperationSelection={handleOperationSelection}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles.add_mar_bottom}>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Webhook URL
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.webhookUrlDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
</h4>
|
||||
<div>
|
||||
<div className={styles.dropdown_wrapper}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'URL', value: 'url' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={
|
||||
(webhookUrlType === 'url' && 'URL') ||
|
||||
(webhookUrlType === 'env' && 'From env var') ||
|
||||
'Value'
|
||||
}
|
||||
dataKey={
|
||||
(webhookUrlType === 'url' && 'url') ||
|
||||
(webhookUrlType === 'env' && 'env')
|
||||
}
|
||||
onButtonChange={this.updateWebhookUrlType.bind(this)}
|
||||
onInputChange={e => {
|
||||
dispatch(setWebhookURL(e.target.value));
|
||||
}}
|
||||
required
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={webhookURL}
|
||||
id="webhook-url"
|
||||
inputPlaceHolder={
|
||||
(webhookUrlType === 'url' &&
|
||||
'http://httpbin.org/post') ||
|
||||
(webhookUrlType === 'env' && 'MY_WEBHOOK_URL')
|
||||
}
|
||||
testId="webhook"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<small>
|
||||
Note: Specifying the webhook URL via an environmental variable
|
||||
is recommended if you have different URLs for multiple
|
||||
environments.
|
||||
</small>
|
||||
</div>
|
||||
<hr />
|
||||
<CollapsibleToggle
|
||||
title={
|
||||
<h4 className={styles.subheading_text}>Advanced Settings</h4>
|
||||
}
|
||||
testId="advanced-settings"
|
||||
>
|
||||
<div>
|
||||
{advancedColumnSection}
|
||||
<hr />
|
||||
<div className={styles.add_mar_top}>
|
||||
<h4 className={styles.subheading_text}>Retry Logic</h4>
|
||||
<div className={styles.retrySection}>
|
||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
||||
<label
|
||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
||||
>
|
||||
Number of retries (default: 0)
|
||||
</label>
|
||||
</div>
|
||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
||||
<input
|
||||
onChange={e => {
|
||||
dispatch(setRetryNum(e.target.value));
|
||||
}}
|
||||
data-test="no-of-retries"
|
||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
||||
type="text"
|
||||
placeholder="no of retries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.retrySection}>
|
||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
||||
<label
|
||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
||||
>
|
||||
Retry Interval in seconds (default: 10)
|
||||
</label>
|
||||
</div>
|
||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
||||
<input
|
||||
onChange={e => {
|
||||
dispatch(setRetryInterval(e.target.value));
|
||||
}}
|
||||
data-test="interval-seconds"
|
||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
||||
type="text"
|
||||
placeholder="interval time in seconds"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.retrySection}>
|
||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
||||
<label
|
||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
||||
>
|
||||
Timeout in seconds (default: 60)
|
||||
</label>
|
||||
</div>
|
||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
||||
<input
|
||||
onChange={e => {
|
||||
dispatch(setRetryTimeout(e.target.value));
|
||||
}}
|
||||
data-test="timeout-seconds"
|
||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
||||
type="text"
|
||||
placeholder="timeout in seconds"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles.add_mar_top}>
|
||||
<h4 className={styles.subheading_text}>Headers</h4>
|
||||
{headersList}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleToggle>
|
||||
<hr />
|
||||
<Button
|
||||
type="submit"
|
||||
color="yellow"
|
||||
size="sm"
|
||||
data-test="trigger-create"
|
||||
>
|
||||
{createBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddTrigger.propTypes = {
|
||||
triggerName: PropTypes.string,
|
||||
tableName: PropTypes.string,
|
||||
schemaName: PropTypes.string,
|
||||
schemaList: PropTypes.array,
|
||||
allSchemas: PropTypes.array.isRequired,
|
||||
selectedOperations: PropTypes.object,
|
||||
operations: PropTypes.object,
|
||||
ongoingRequest: PropTypes.bool.isRequired,
|
||||
lastError: PropTypes.object,
|
||||
internalError: PropTypes.string,
|
||||
lastSuccess: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
...state.addTrigger,
|
||||
schemaList: state.tables.schemaList,
|
||||
allSchemas: state.tables.allSchemas,
|
||||
serverVersion: state.main.serverVersion ? state.main.serverVersion : '',
|
||||
};
|
||||
};
|
||||
|
||||
const addTriggerConnector = connect => connect(mapStateToProps)(AddTrigger);
|
||||
|
||||
export default addTriggerConnector;
|
@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import * as tooltip from './Tooltips';
|
||||
import { TOGGLE_ENABLE_MANUAL_CONFIG } from './AddActions';
|
||||
import KnowMoreLink from '../../../Common/KnowMoreLink/KnowMoreLink';
|
||||
|
||||
const Operations = ({
|
||||
enableManual,
|
||||
selectedOperations,
|
||||
handleOperationSelection,
|
||||
dispatch,
|
||||
}) => {
|
||||
const styles = require('../TableCommon/EventTable.scss');
|
||||
|
||||
const databaseOperations = [
|
||||
{
|
||||
name: 'insert',
|
||||
testIdentifier: 'insert-operation',
|
||||
isChecked: selectedOperations.insert,
|
||||
onChange: handleOperationSelection,
|
||||
displayName: 'Insert',
|
||||
},
|
||||
{
|
||||
name: 'update',
|
||||
testIdentifier: 'update-operation',
|
||||
isChecked: selectedOperations.update,
|
||||
onChange: handleOperationSelection,
|
||||
displayName: 'Update',
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
testIdentifier: 'delete-operation',
|
||||
isChecked: selectedOperations.delete,
|
||||
onChange: handleOperationSelection,
|
||||
displayName: 'Delete',
|
||||
},
|
||||
];
|
||||
|
||||
const getManualInvokeOperation = () => {
|
||||
const handleManualOperationSelection = () => {
|
||||
dispatch({ type: TOGGLE_ENABLE_MANUAL_CONFIG });
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'enable_manual',
|
||||
testIdentifier: 'enable-manual-operation',
|
||||
isChecked: enableManual,
|
||||
onChange: handleManualOperationSelection,
|
||||
displayName: (
|
||||
<span>
|
||||
Via console
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.manualOperationsDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
|
||||
<KnowMoreLink href="https://hasura.io/docs/1.0/graphql/manual/event-triggers/invoke-trigger-console.html" />
|
||||
</span>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getOperationsList = () => {
|
||||
const manualOperation = getManualInvokeOperation();
|
||||
|
||||
const allOperations = databaseOperations;
|
||||
if (manualOperation) {
|
||||
allOperations.push(manualOperation);
|
||||
}
|
||||
|
||||
return allOperations.map((o, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`${styles.display_inline} ${styles.add_mar_right}`}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
onChange={o.onChange}
|
||||
data-test={o.testIdentifier}
|
||||
className={`${styles.display_inline} ${styles.add_mar_right}`}
|
||||
type="checkbox"
|
||||
value={o.name}
|
||||
checked={o.isChecked}
|
||||
/>
|
||||
{o.displayName}
|
||||
</label>
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.add_mar_bottom + ' ' + styles.selectOperations}>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Trigger Operations
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.operationsDescription}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
</h4>
|
||||
<div className={styles.add_mar_left_small}>{getOperationsList()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Operations.propTypes = {
|
||||
enableManual: PropTypes.bool.isRequired,
|
||||
selectedOperations: PropTypes.object.isRequired,
|
||||
handleOperationSelection: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Operations;
|
@ -1,4 +0,0 @@
|
||||
const dataHeaders = currentState => {
|
||||
return currentState().tables.dataHeaders;
|
||||
};
|
||||
export default dataHeaders;
|
@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
|
||||
export const statusCodeDescription = (
|
||||
<Tooltip id="tooltip-trigger-status-code-description">
|
||||
Status code of the webhook response
|
||||
</Tooltip>
|
||||
);
|
@ -1,651 +0,0 @@
|
||||
import Endpoints, { globalCookiePolicy } from '../../../Endpoints';
|
||||
import requestAction from '../../../utils/requestAction';
|
||||
import defaultState from './EventState';
|
||||
import processedEventsReducer from './ProcessedEvents/ViewActions';
|
||||
import pendingEventsReducer from './PendingEvents/ViewActions';
|
||||
import runningEventsReducer from './RunningEvents/ViewActions';
|
||||
import streamingLogsReducer from './StreamingLogs/LogActions';
|
||||
import {
|
||||
showSuccessNotification,
|
||||
showErrorNotification,
|
||||
} from '../Common/Notification';
|
||||
import dataHeaders from './Common/Headers';
|
||||
import { loadMigrationStatus } from '../../Main/Actions';
|
||||
import returnMigrateUrl from './Common/getMigrateUrl';
|
||||
import globals from '../../../Globals';
|
||||
import push from './push';
|
||||
import { loadInconsistentObjects } from '../Settings/Actions';
|
||||
import { filterInconsistentMetadataObjects } from '../Settings/utils';
|
||||
import { replace } from 'react-router-redux';
|
||||
import { getEventTriggersQuery } from './utils';
|
||||
|
||||
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
|
||||
import { REQUEST_COMPLETE, REQUEST_ONGOING } from './Modify/Actions';
|
||||
|
||||
const SET_TRIGGER = 'Event/SET_TRIGGER';
|
||||
const LOAD_TRIGGER_LIST = 'Event/LOAD_TRIGGER_LIST';
|
||||
const LOAD_PROCESSED_EVENTS = 'Event/LOAD_PROCESSED_EVENTS';
|
||||
const LOAD_PENDING_EVENTS = 'Event/LOAD_PENDING_EVENTS';
|
||||
const LOAD_RUNNING_EVENTS = 'Event/LOAD_RUNNING_EVENTS';
|
||||
const ADMIN_SECRET_ERROR = 'Event/ADMIN_SECRET_ERROR';
|
||||
const UPDATE_DATA_HEADERS = 'Event/UPDATE_DATA_HEADERS';
|
||||
const LISTING_TRIGGER = 'Event/LISTING_TRIGGER';
|
||||
const LOAD_EVENT_LOGS = 'Event/LOAD_EVENT_LOGS';
|
||||
const MODAL_OPEN = 'Event/MODAL_OPEN';
|
||||
const SET_REDELIVER_EVENT = 'Event/SET_REDELIVER_EVENT';
|
||||
const LOAD_EVENT_INVOCATIONS = 'Event/LOAD_EVENT_INVOCATIONS';
|
||||
const REDELIVER_EVENT_SUCCESS = 'Event/REDELIVER_EVENT_SUCCESS';
|
||||
const REDELIVER_EVENT_FAILURE = 'Event/REDELIVER_EVENT_FAILURE';
|
||||
|
||||
const MAKE_REQUEST = 'Event/MAKE_REQUEST';
|
||||
const REQUEST_SUCCESS = 'Event/REQUEST_SUCCESS';
|
||||
const REQUEST_ERROR = 'Event/REQUEST_ERROR';
|
||||
|
||||
/* ************ action creators *********************** */
|
||||
const loadTriggers = triggerNames => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const body = getEventTriggersQuery(triggerNames);
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
if (data.result_type !== 'TuplesOk') {
|
||||
console.error('Failed to event trigger info' + JSON.stringify(data[1]));
|
||||
return;
|
||||
}
|
||||
let triggerData = JSON.parse(data.result[1]);
|
||||
if (triggerNames.length !== 0) {
|
||||
// getExisting state
|
||||
const existingTriggers = getState().triggers.triggerList.filter(
|
||||
trigger => triggerNames.some(item => item !== trigger.name)
|
||||
);
|
||||
const triggerLists = existingTriggers.concat(triggerData);
|
||||
triggerData = triggerLists.sort((a, b) => {
|
||||
return a.name === b.name ? 0 : +(a.name > b.name) || -1;
|
||||
});
|
||||
}
|
||||
|
||||
// hydrate undefined config values
|
||||
triggerData.forEach(trigger => {
|
||||
if (!trigger.configuration.headers) {
|
||||
trigger.configuration.headers = [];
|
||||
}
|
||||
});
|
||||
|
||||
const { inconsistentObjects } = getState().metadata;
|
||||
let consistentTriggers = triggerData;
|
||||
if (inconsistentObjects.length > 1) {
|
||||
consistentTriggers = filterInconsistentMetadataObjects(
|
||||
triggerData,
|
||||
inconsistentObjects,
|
||||
'events'
|
||||
);
|
||||
}
|
||||
dispatch({
|
||||
type: LOAD_TRIGGER_LIST,
|
||||
triggerList: consistentTriggers,
|
||||
});
|
||||
dispatch(loadInconsistentObjects({ shouldReloadMetadata: false }));
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadPendingEvents = () => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const body = {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: { delivered: false, error: false, tries: 0, archived: false },
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_PENDING_EVENTS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadRunningEvents = () => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const body = {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: {
|
||||
delivered: false,
|
||||
error: false,
|
||||
tries: { $gt: 0 },
|
||||
archived: false,
|
||||
},
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_RUNNING_EVENTS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadEventLogs = triggerName => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const triggerOptions = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*'],
|
||||
where: {
|
||||
name: triggerName,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, triggerOptions)).then(
|
||||
triggerData => {
|
||||
if (triggerData.length !== 0) {
|
||||
const body = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_invocation_logs',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'event',
|
||||
columns: ['*'],
|
||||
},
|
||||
],
|
||||
where: {
|
||||
event: { trigger_name: triggerData[0].name, archived: false },
|
||||
},
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const logOptions = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
dispatch(requestAction(url, logOptions)).then(
|
||||
logsData => {
|
||||
dispatch({ type: LOAD_EVENT_LOGS, data: logsData[0] });
|
||||
},
|
||||
error => {
|
||||
console.error(
|
||||
'Failed to load trigger logs' + JSON.stringify(error)
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
dispatch(replace('/404'));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error(
|
||||
'Failed to fetch trigger information' + JSON.stringify(error)
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadEventInvocations = eventId => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_invocation_logs',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'event',
|
||||
columns: ['*'],
|
||||
},
|
||||
],
|
||||
where: { event_id: eventId },
|
||||
order_by: ['-created_at'],
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_EVENT_INVOCATIONS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const redeliverEvent = eventId => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'redeliver_event',
|
||||
args: {
|
||||
event_id: eventId,
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: REDELIVER_EVENT_SUCCESS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
dispatch({ type: REDELIVER_EVENT_FAILURE, data: error });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setTrigger = triggerName => ({ type: SET_TRIGGER, triggerName });
|
||||
|
||||
const setRedeliverEvent = eventId => dispatch => {
|
||||
/*
|
||||
Redeliver event and mark the redeliverEventId to the redelivered event so that it can be tracked.
|
||||
*/
|
||||
return dispatch(redeliverEvent(eventId)).then(() => {
|
||||
return Promise.all([
|
||||
dispatch({ type: SET_REDELIVER_EVENT, eventId }),
|
||||
dispatch(loadEventInvocations(eventId)),
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
/* **********Shared functions between table actions********* */
|
||||
|
||||
const handleMigrationErrors = (title, errorMsg) => dispatch => {
|
||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
||||
// handle errors for run_sql based workflow
|
||||
dispatch(showErrorNotification(title, errorMsg.code, errorMsg));
|
||||
} else if (errorMsg.code === 'migration_failed') {
|
||||
dispatch(showErrorNotification(title, 'Migration Failed', errorMsg));
|
||||
} else if (errorMsg.code === 'data_api_error') {
|
||||
const parsedErrorMsg = errorMsg;
|
||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
||||
dispatch(
|
||||
showErrorNotification(title, parsedErrorMsg.message.error, parsedErrorMsg)
|
||||
);
|
||||
} else {
|
||||
// any other unhandled codes
|
||||
const parsedErrorMsg = errorMsg;
|
||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
||||
dispatch(showErrorNotification(title, errorMsg.code, parsedErrorMsg));
|
||||
}
|
||||
};
|
||||
|
||||
const makeMigrationCall = (
|
||||
dispatch,
|
||||
getState,
|
||||
upQueries,
|
||||
downQueries,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg
|
||||
) => {
|
||||
const upQuery = {
|
||||
type: 'bulk',
|
||||
args: upQueries,
|
||||
};
|
||||
|
||||
const downQuery = {
|
||||
type: 'bulk',
|
||||
args: downQueries,
|
||||
};
|
||||
|
||||
const migrationBody = {
|
||||
name: migrationName,
|
||||
up: upQuery.args,
|
||||
down: downQuery.args,
|
||||
};
|
||||
|
||||
const currMigrationMode = getState().main.migrationMode;
|
||||
|
||||
const migrateUrl = returnMigrateUrl(currMigrationMode);
|
||||
|
||||
let finalReqBody;
|
||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
||||
finalReqBody = upQuery;
|
||||
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
|
||||
finalReqBody = migrationBody;
|
||||
}
|
||||
const url = migrateUrl;
|
||||
const options = {
|
||||
method: 'POST',
|
||||
credentials: globalCookiePolicy,
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(finalReqBody),
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
if (globals.consoleMode === CLI_CONSOLE_MODE) {
|
||||
dispatch(loadMigrationStatus()); // don't call for server mode
|
||||
}
|
||||
customOnSuccess();
|
||||
if (successMsg) {
|
||||
dispatch(showSuccessNotification(successMsg));
|
||||
}
|
||||
};
|
||||
|
||||
const onError = err => {
|
||||
customOnError(err);
|
||||
dispatch(handleMigrationErrors(errorMsg, err));
|
||||
};
|
||||
|
||||
dispatch({ type: MAKE_REQUEST });
|
||||
dispatch(showSuccessNotification(requestMsg));
|
||||
dispatch(requestAction(url, options, REQUEST_SUCCESS, REQUEST_ERROR)).then(
|
||||
onSuccess,
|
||||
onError
|
||||
);
|
||||
};
|
||||
|
||||
const deleteTrigger = triggerName => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(showSuccessNotification('Deleting Trigger...'));
|
||||
const currentTriggerInfo = getState().triggers.triggerList.filter(
|
||||
t => t.name === triggerName
|
||||
)[0];
|
||||
// apply migrations
|
||||
const migrationName = 'delete_trigger_' + triggerName.trim();
|
||||
const payload = {
|
||||
type: 'delete_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
},
|
||||
};
|
||||
const downPayload = {
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
table: {
|
||||
name: currentTriggerInfo.table_name,
|
||||
schema: currentTriggerInfo.table_schema,
|
||||
},
|
||||
retry_conf: { ...currentTriggerInfo.configuration.retry_conf },
|
||||
...currentTriggerInfo.configuration.definition,
|
||||
headers: [...currentTriggerInfo.configuration.headers],
|
||||
},
|
||||
};
|
||||
if (currentTriggerInfo.configuration.webhook_from_env) {
|
||||
downPayload.args.webhook_from_env =
|
||||
currentTriggerInfo.configuration.webhook_from_env;
|
||||
} else {
|
||||
downPayload.args.webhook = currentTriggerInfo.configuration.webhook;
|
||||
}
|
||||
const upQueryArgs = [];
|
||||
upQueryArgs.push(payload);
|
||||
const downQueryArgs = [];
|
||||
downQueryArgs.push(downPayload);
|
||||
const upQuery = {
|
||||
type: 'bulk',
|
||||
args: upQueryArgs,
|
||||
};
|
||||
const downQuery = {
|
||||
type: 'bulk',
|
||||
args: downQueryArgs,
|
||||
};
|
||||
const requestMsg = 'Deleting trigger...';
|
||||
const successMsg = 'Trigger deleted';
|
||||
const errorMsg = 'Delete trigger failed';
|
||||
|
||||
const customOnSuccess = () => {
|
||||
// dispatch({ type: REQUEST_SUCCESS });
|
||||
dispatch({ type: REQUEST_COMPLETE }); // modify trigger action
|
||||
dispatch(showSuccessNotification('Trigger Deleted'));
|
||||
dispatch(push('/manage/triggers'));
|
||||
// remove this trigger from state
|
||||
const existingTriggers = getState().triggers.triggerList.filter(
|
||||
trigger => trigger.name !== triggerName
|
||||
);
|
||||
dispatch({
|
||||
type: LOAD_TRIGGER_LIST,
|
||||
triggerList: existingTriggers,
|
||||
});
|
||||
return;
|
||||
};
|
||||
const customOnError = () => {
|
||||
dispatch({ type: REQUEST_COMPLETE }); // modify trigger action
|
||||
dispatch({ type: REQUEST_ERROR, data: errorMsg });
|
||||
return;
|
||||
};
|
||||
|
||||
// modify trigger action
|
||||
dispatch({ type: REQUEST_ONGOING, data: 'delete' });
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQuery.args,
|
||||
downQuery.args,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg,
|
||||
true
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/* ******************************************************* */
|
||||
const eventReducer = (state = defaultState, action) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
if (action.type.indexOf('ProcessedEvents/') === 0) {
|
||||
return {
|
||||
...state,
|
||||
view: processedEventsReducer(
|
||||
state.currentTrigger,
|
||||
state.triggerList,
|
||||
state.view,
|
||||
action
|
||||
),
|
||||
};
|
||||
}
|
||||
if (action.type.indexOf('PendingEvents/') === 0) {
|
||||
return {
|
||||
...state,
|
||||
view: pendingEventsReducer(
|
||||
state.currentTrigger,
|
||||
state.triggerList,
|
||||
state.view,
|
||||
action
|
||||
),
|
||||
};
|
||||
}
|
||||
if (action.type.indexOf('RunningEvents/') === 0) {
|
||||
return {
|
||||
...state,
|
||||
view: runningEventsReducer(
|
||||
state.currentTrigger,
|
||||
state.triggerList,
|
||||
state.view,
|
||||
action
|
||||
),
|
||||
};
|
||||
}
|
||||
if (action.type.indexOf('StreamingLogs/') === 0) {
|
||||
return {
|
||||
...state,
|
||||
log: streamingLogsReducer(
|
||||
state.currentTrigger,
|
||||
state.triggerList,
|
||||
state.log,
|
||||
action
|
||||
),
|
||||
};
|
||||
}
|
||||
switch (action.type) {
|
||||
case LOAD_TRIGGER_LIST:
|
||||
return {
|
||||
...state,
|
||||
triggerList: action.triggerList,
|
||||
listingTrigger: action.triggerList,
|
||||
};
|
||||
case LISTING_TRIGGER:
|
||||
return {
|
||||
...state,
|
||||
listingTrigger: action.updatedList,
|
||||
};
|
||||
case LOAD_PROCESSED_EVENTS:
|
||||
return {
|
||||
...state,
|
||||
processedEvents: action.data,
|
||||
};
|
||||
case LOAD_PENDING_EVENTS:
|
||||
return {
|
||||
...state,
|
||||
pendingEvents: action.data,
|
||||
};
|
||||
case LOAD_RUNNING_EVENTS:
|
||||
return {
|
||||
...state,
|
||||
runningEvents: action.data,
|
||||
};
|
||||
case LOAD_EVENT_LOGS:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, rows: action.data, count: action.data.length },
|
||||
};
|
||||
case SET_TRIGGER:
|
||||
return { ...state, currentTrigger: action.triggerName };
|
||||
case ADMIN_SECRET_ERROR:
|
||||
return { ...state, adminSecretError: action.data };
|
||||
case UPDATE_DATA_HEADERS:
|
||||
return { ...state, dataHeaders: action.data };
|
||||
case MODAL_OPEN:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, isModalOpen: action.data },
|
||||
};
|
||||
case SET_REDELIVER_EVENT:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, redeliverEventId: action.eventId },
|
||||
};
|
||||
case LOAD_EVENT_INVOCATIONS:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, eventInvocations: action.data },
|
||||
};
|
||||
case REDELIVER_EVENT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, redeliverInvocationId: action.data },
|
||||
};
|
||||
case REDELIVER_EVENT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
log: { ...state.log, redeliverEventFailure: action.data },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default eventReducer;
|
||||
export {
|
||||
setTrigger,
|
||||
loadTriggers,
|
||||
deleteTrigger,
|
||||
loadPendingEvents,
|
||||
loadRunningEvents,
|
||||
loadEventLogs,
|
||||
handleMigrationErrors,
|
||||
makeMigrationCall,
|
||||
setRedeliverEvent,
|
||||
loadEventInvocations,
|
||||
redeliverEvent,
|
||||
ADMIN_SECRET_ERROR,
|
||||
UPDATE_DATA_HEADERS,
|
||||
LISTING_TRIGGER,
|
||||
MODAL_OPEN,
|
||||
SET_REDELIVER_EVENT,
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
|
||||
import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer';
|
||||
import EventSubSidebar from './EventSubSidebar';
|
||||
|
||||
const appPrefix = '/events';
|
||||
|
||||
const EventPageContainer = ({
|
||||
schema,
|
||||
currentSchema,
|
||||
children,
|
||||
location,
|
||||
dispatch,
|
||||
}) => {
|
||||
const styles = require('../../Common/TableCommon/Table.scss');
|
||||
|
||||
const currentLocation = location.pathname;
|
||||
|
||||
const sidebarContent = (
|
||||
<ul>
|
||||
<li
|
||||
role="presentation"
|
||||
className={
|
||||
currentLocation.includes('events/manage') ? styles.active : ''
|
||||
}
|
||||
>
|
||||
<Link className={styles.linkBorder} to={appPrefix + '/manage'}>
|
||||
Manage
|
||||
</Link>
|
||||
<EventSubSidebar
|
||||
location={location}
|
||||
schema={schema}
|
||||
currentSchema={currentSchema}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
const helmet = 'Events | Hasura';
|
||||
|
||||
const leftContainer = <LeftContainer>{sidebarContent}</LeftContainer>;
|
||||
|
||||
return (
|
||||
<PageContainer helmet={helmet} leftContainer={leftContainer}>
|
||||
{children}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
schema: state.tables.allSchemas,
|
||||
schemaList: state.tables.schemaList,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
};
|
||||
};
|
||||
|
||||
const eventPageConnector = connect =>
|
||||
connect(mapStateToProps)(EventPageContainer);
|
||||
|
||||
export default eventPageConnector;
|
@ -1,13 +0,0 @@
|
||||
import eventTriggerReducer from './EventActions';
|
||||
import addTriggerReducer from './Add/AddActions';
|
||||
import modifyTriggerReducer from './Modify/Actions';
|
||||
import invokeEventTriggerReducer from './Common/InvokeManualTrigger/InvokeManualTriggerAction';
|
||||
|
||||
const eventReducer = {
|
||||
triggers: eventTriggerReducer,
|
||||
addTrigger: addTriggerReducer,
|
||||
modifyTrigger: modifyTriggerReducer,
|
||||
invokeEventTrigger: invokeEventTriggerReducer,
|
||||
};
|
||||
|
||||
export default eventReducer;
|
@ -1,159 +0,0 @@
|
||||
import React from 'react';
|
||||
// import {push} fropm 'react-router-redux';
|
||||
import { Route, IndexRedirect } from 'react-router';
|
||||
import globals from '../../../Globals';
|
||||
|
||||
import {
|
||||
landingConnector,
|
||||
addTriggerConnector,
|
||||
modifyTriggerConnector,
|
||||
processedEventsConnector,
|
||||
pendingEventsConnector,
|
||||
runningEventsConnector,
|
||||
eventPageConnector,
|
||||
streamingLogsConnector,
|
||||
} from '.';
|
||||
|
||||
import { rightContainerConnector } from '../../Common/Layout';
|
||||
|
||||
import {
|
||||
loadTriggers,
|
||||
loadPendingEvents,
|
||||
loadRunningEvents,
|
||||
} from '../EventTrigger/EventActions';
|
||||
|
||||
const makeEventRouter = (
|
||||
connect,
|
||||
store,
|
||||
composeOnEnterHooks,
|
||||
requireSchema,
|
||||
requirePendingEvents,
|
||||
requireRunningEvents
|
||||
) => {
|
||||
return (
|
||||
<Route
|
||||
path="events"
|
||||
component={eventPageConnector(connect)}
|
||||
onEnter={composeOnEnterHooks([requireSchema])}
|
||||
>
|
||||
<IndexRedirect to="manage" />
|
||||
<Route path="manage" component={rightContainerConnector(connect)}>
|
||||
<IndexRedirect to="triggers" />
|
||||
<Route path="triggers" component={landingConnector(connect)} />
|
||||
<Route
|
||||
path="triggers/:trigger/processed"
|
||||
component={processedEventsConnector(connect)}
|
||||
/>
|
||||
<Route
|
||||
path="triggers/:trigger/pending"
|
||||
component={pendingEventsConnector(connect)}
|
||||
onEnter={composeOnEnterHooks([requirePendingEvents])}
|
||||
/>
|
||||
<Route
|
||||
path="triggers/:trigger/running"
|
||||
component={runningEventsConnector(connect)}
|
||||
onEnter={composeOnEnterHooks([requireRunningEvents])}
|
||||
/>
|
||||
<Route
|
||||
path="triggers/:trigger/logs"
|
||||
component={streamingLogsConnector(connect)}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="manage/triggers/add"
|
||||
component={addTriggerConnector(connect)}
|
||||
/>
|
||||
<Route
|
||||
path="manage/triggers/:trigger/modify"
|
||||
component={modifyTriggerConnector(connect)}
|
||||
/>
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
|
||||
const eventRouterUtils = (connect, store, composeOnEnterHooks) => {
|
||||
const requireSchema = (nextState, replaceState, cb) => {
|
||||
// check if admin secret is available in localstorage. if so use that.
|
||||
// if localstorage admin secret didn't work, redirect to login (meaning value has changed)
|
||||
// if admin secret is not available in localstorage, check if cli is giving it via window.__env
|
||||
// if admin secret is not available in localstorage and cli, make a api call to data without admin secret.
|
||||
// if the api fails, then redirect to login - this is a fresh user/browser flow
|
||||
const {
|
||||
triggers: { triggerList },
|
||||
} = store.getState();
|
||||
|
||||
if (triggerList.length) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([store.dispatch(loadTriggers([]))]).then(
|
||||
() => {
|
||||
cb();
|
||||
},
|
||||
() => {
|
||||
// alert('Could not load schema.');
|
||||
replaceState(globals.urlPrefix);
|
||||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const requirePendingEvents = (nextState, replaceState, cb) => {
|
||||
const {
|
||||
triggers: { pendingEvents },
|
||||
} = store.getState();
|
||||
|
||||
if (pendingEvents.length) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([store.dispatch(loadPendingEvents())]).then(
|
||||
() => {
|
||||
cb();
|
||||
},
|
||||
() => {
|
||||
// alert('Could not load schema.');
|
||||
replaceState(globals.urlPrefix);
|
||||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const requireRunningEvents = (nextState, replaceState, cb) => {
|
||||
const {
|
||||
triggers: { runningEvents },
|
||||
} = store.getState();
|
||||
|
||||
if (runningEvents.length) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([store.dispatch(loadRunningEvents())]).then(
|
||||
() => {
|
||||
cb();
|
||||
},
|
||||
() => {
|
||||
// alert('Could not load schema.');
|
||||
replaceState(globals.urlPrefix);
|
||||
cb();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
makeEventRouter: makeEventRouter(
|
||||
connect,
|
||||
store,
|
||||
composeOnEnterHooks,
|
||||
requireSchema,
|
||||
requirePendingEvents,
|
||||
requireRunningEvents
|
||||
),
|
||||
requireSchema,
|
||||
};
|
||||
};
|
||||
|
||||
export default eventRouterUtils;
|
@ -1,84 +0,0 @@
|
||||
const defaultCurFilter = {
|
||||
where: { $and: [{ '': { '': '' } }] },
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order_by: [{ column: '', type: 'asc', nulls: 'last' }],
|
||||
};
|
||||
|
||||
const defaultViewState = {
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
},
|
||||
rows: [],
|
||||
expandedRow: '',
|
||||
count: 0,
|
||||
curFilter: defaultCurFilter,
|
||||
activePath: [],
|
||||
ongoingRequest: false,
|
||||
lastError: {},
|
||||
lastSuccess: {},
|
||||
};
|
||||
|
||||
const defaultLogState = {
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'event',
|
||||
columns: ['*'],
|
||||
},
|
||||
],
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
order_by: ['-created_at'],
|
||||
},
|
||||
rows: [],
|
||||
expandedRow: '',
|
||||
count: 0,
|
||||
curFilter: defaultCurFilter,
|
||||
activePath: [],
|
||||
isLoadingOlder: false,
|
||||
isLoadingNewer: false,
|
||||
isOldAvailable: true,
|
||||
isNewAvailable: true,
|
||||
isModalOpen: false,
|
||||
redeliverEventId: null,
|
||||
eventInvocations: [],
|
||||
redeliverInvocationId: null,
|
||||
redeliverEventFailure: null,
|
||||
ongoingRequest: false,
|
||||
lastError: {},
|
||||
lastSuccess: {},
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
currentTrigger: null,
|
||||
view: { ...defaultViewState },
|
||||
log: { ...defaultLogState },
|
||||
triggerList: [],
|
||||
listingTrigger: [],
|
||||
processedEvents: [],
|
||||
pendingEvents: [],
|
||||
runningEvents: [],
|
||||
eventLogs: [],
|
||||
schemaList: ['public'],
|
||||
currentSchema: 'public',
|
||||
adminSecretError: false,
|
||||
dataHeaders: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultState;
|
||||
export { defaultViewState, defaultLogState, defaultCurFilter };
|
@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
// import globals from '../../../Globals';
|
||||
|
||||
import LeftSubSidebar from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar';
|
||||
|
||||
import { LISTING_TRIGGER } from './EventActions';
|
||||
|
||||
const appPrefix = '/events';
|
||||
|
||||
const EventSubSidebar = ({
|
||||
currentTrigger,
|
||||
triggerList,
|
||||
listingTrigger,
|
||||
// children,
|
||||
dispatch,
|
||||
location,
|
||||
readOnlyMode,
|
||||
}) => {
|
||||
const styles = require('../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss');
|
||||
|
||||
function triggerSearch(e) {
|
||||
const searchTerm = e.target.value;
|
||||
// form new schema
|
||||
const matchedTables = [];
|
||||
triggerList.map(trigger => {
|
||||
if (trigger.name.indexOf(searchTerm) !== -1) {
|
||||
matchedTables.push(trigger);
|
||||
}
|
||||
});
|
||||
// update schema with matchedTables
|
||||
dispatch({ type: LISTING_TRIGGER, updatedList: matchedTables });
|
||||
}
|
||||
|
||||
const getSearchInput = () => {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
onChange={triggerSearch.bind(this)}
|
||||
className="form-control"
|
||||
placeholder="search event triggers"
|
||||
data-test="search-triggers"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getChildList = () => {
|
||||
let triggerLinks = (
|
||||
<li className={styles.noChildren}>
|
||||
<i>No triggers available</i>
|
||||
</li>
|
||||
);
|
||||
|
||||
const triggers = {};
|
||||
listingTrigger.map(t => {
|
||||
triggers[t.name] = t;
|
||||
});
|
||||
|
||||
const currentLocation = location.pathname;
|
||||
|
||||
if (listingTrigger && listingTrigger.length) {
|
||||
triggerLinks = Object.keys(triggers)
|
||||
.sort()
|
||||
.map((trigger, i) => {
|
||||
let activeTableClass = '';
|
||||
if (
|
||||
trigger === currentTrigger &&
|
||||
currentLocation.indexOf(currentTrigger) !== -1
|
||||
) {
|
||||
activeTableClass = styles.activeLink;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={activeTableClass} key={i}>
|
||||
<Link
|
||||
to={appPrefix + '/manage/triggers/' + trigger + '/processed'}
|
||||
data-test={trigger}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-send-o'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{trigger}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return triggerLinks;
|
||||
};
|
||||
|
||||
return (
|
||||
<LeftSubSidebar
|
||||
showAddBtn={!readOnlyMode}
|
||||
searchInput={getSearchInput()}
|
||||
heading={`Event Triggers (${triggerList.length})`}
|
||||
addLink={'/events/manage/triggers/add'}
|
||||
addLabel={'Create'}
|
||||
addTestString={'sidebar-add-table'}
|
||||
childListTestString={'table-links'}
|
||||
>
|
||||
{getChildList()}
|
||||
</LeftSubSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentTrigger: state.triggers.currentTrigger,
|
||||
triggerList: state.triggers.triggerList,
|
||||
listingTrigger: state.triggers.listingTrigger,
|
||||
readOnlyMode: state.main.readOnlyMode,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(EventSubSidebar);
|
@ -1,125 +0,0 @@
|
||||
/* eslint-disable space-infix-ops */
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { push } from 'react-router-redux';
|
||||
import { loadTriggers } from '../EventActions';
|
||||
import globals from '../../../../Globals';
|
||||
import Button from '../../../Common/Button/Button';
|
||||
import TopicDescription from '../../Common/Landing/TopicDescription';
|
||||
import TryItOut from '../../Common/Landing/TryItOut';
|
||||
|
||||
const appPrefix = globals.urlPrefix + '/events';
|
||||
|
||||
class EventTrigger extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize this table
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(loadTriggers([]));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, readOnlyMode } = this.props;
|
||||
|
||||
const styles = require('../../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss');
|
||||
|
||||
const queryDefinition = `mutation {
|
||||
insert_user(objects: [{name: "testuser"}] ){
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
|
||||
const getAddBtn = () => {
|
||||
if (readOnlyMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = e => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(push(`${appPrefix}/manage/triggers/add`));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test="data-create-trigger"
|
||||
color="yellow"
|
||||
size="sm"
|
||||
className={styles.add_mar_left}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const footerEvent = (
|
||||
<span>
|
||||
Head to the Events tab and see an event invoked under{' '}
|
||||
<span className={styles.fontWeightBold}> test-trigger</span>.
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.padd_left_remove} container-fluid ${styles.padd_top}`}
|
||||
>
|
||||
<div className={styles.padd_left}>
|
||||
<Helmet title="Event Triggers | Hasura" />
|
||||
<div className={styles.display_flex}>
|
||||
<h2 className={`${styles.headerText} ${styles.inline_block}`}>
|
||||
Event Triggers
|
||||
</h2>
|
||||
{getAddBtn()}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<TopicDescription
|
||||
title="What are Event Triggers?"
|
||||
imgUrl={`${globals.assetsPath}/common/img/event-trigger.png`}
|
||||
imgAlt="Event Triggers"
|
||||
description="Hasura can be used to create event triggers on tables. An Event Trigger atomically captures events (insert, update, delete) on a specified table and then reliably calls a webhook that can carry out any custom logic."
|
||||
/>
|
||||
<hr className={styles.clear_fix} />
|
||||
|
||||
<TryItOut
|
||||
service="eventTrigger"
|
||||
title="Steps to deploy an example Event Trigger to Glitch"
|
||||
queryDefinition={queryDefinition}
|
||||
footerDescription={footerEvent}
|
||||
glitchLink="https://glitch.com/edit/#!/hasura-sample-event-trigger"
|
||||
googleCloudLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/google-cloud-functions/nodejs8"
|
||||
MicrosoftAzureLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/azure-functions/nodejs"
|
||||
awsLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/aws-lambda/nodejs8"
|
||||
adMoreLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/"
|
||||
isAvailable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EventTrigger.propTypes = {
|
||||
schema: PropTypes.array.isRequired,
|
||||
untrackedRelations: PropTypes.array.isRequired,
|
||||
currentSchema: PropTypes.string.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
schema: state.tables.allSchemas,
|
||||
schemaList: state.tables.schemaList,
|
||||
untrackedRelations: state.tables.untrackedRelations,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
listingTrigger: state.triggers.listingTrigger,
|
||||
readOnlyMode: state.main.readOnlyMode,
|
||||
});
|
||||
|
||||
const eventTriggerConnector = connect => connect(mapStateToProps)(EventTrigger);
|
||||
|
||||
export default eventTriggerConnector;
|
@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { deleteTrigger } from '../EventActions';
|
||||
import Button from '../../../Common/Button/Button';
|
||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
||||
|
||||
const verifyDeleteTrigger = (triggerName, dispatch) => {
|
||||
const confirmMessage = `This will permanently delete the event trigger "${triggerName}"`;
|
||||
const isOk = getConfirmation(confirmMessage, true, triggerName);
|
||||
if (isOk) {
|
||||
dispatch(deleteTrigger(triggerName));
|
||||
}
|
||||
};
|
||||
|
||||
const Buttons = ({ styles, dispatch, triggerName, ongoingRequest }) => (
|
||||
<div className={styles.add_mar_bottom}>
|
||||
<Button
|
||||
color="red"
|
||||
size="sm"
|
||||
data-test="delete-trigger"
|
||||
onClick={() => verifyDeleteTrigger(triggerName, dispatch)}
|
||||
disabled={ongoingRequest === 'delete'}
|
||||
>
|
||||
{ongoingRequest === 'delete' ? 'Deleting ...' : 'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Buttons;
|
@ -1,340 +0,0 @@
|
||||
import defaultState from './State';
|
||||
import { loadTriggers, makeMigrationCall, setTrigger } from '../EventActions';
|
||||
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
|
||||
import { showErrorNotification } from '../../Common/Notification';
|
||||
|
||||
import { MANUAL_TRIGGER_VAR } from './utils';
|
||||
|
||||
const SET_DEFAULTS = 'ModifyTrigger/SET_DEFAULTS';
|
||||
export const setDefaults = () => ({ type: SET_DEFAULTS });
|
||||
|
||||
const SET_WEBHOOK_URL = 'ModifyTrigger/SET_WEBHOOK_URL';
|
||||
const SET_WEBHOOK_URL_TYPE = 'ModifyTrigger/SET_WEBHOOK_URL_TYPE';
|
||||
export const setWebhookUrl = data => ({ type: SET_WEBHOOK_URL, data });
|
||||
export const setWebhookUrlType = data => ({
|
||||
type: SET_WEBHOOK_URL_TYPE,
|
||||
data,
|
||||
});
|
||||
|
||||
const SET_RETRY_NUM = 'ModifyTrigger/SET_RETRY_NUM';
|
||||
const SET_RETRY_INTERVAL = 'ModifyTrigger/SET_RETRY_INTERVAL';
|
||||
const SET_RETRY_TIMEOUT = 'ModifyTrigger/SET_RETRY_TIMEOUT';
|
||||
export const setRetryNum = data => ({ type: SET_RETRY_NUM, data });
|
||||
export const setRetryInterval = data => ({ type: SET_RETRY_INTERVAL, data });
|
||||
export const setRetryTimeout = data => ({ type: SET_RETRY_TIMEOUT, data });
|
||||
|
||||
const TOGGLE_COLUMN = 'ModifyTrigger/TOGGLE_COLUMNS';
|
||||
const TOGGLE_QUERY_TYPE = 'ModifyTrigger/TOGGLE_QUERY_TYPE_SELECTED';
|
||||
const TOGGLE_MANUAL_QUERY_TYPE = 'ModifyTrigger/TOGGLE_MANUAL_QUERY_SELECTED';
|
||||
export const RESET_MODIFY_STATE = 'ModifyTrigger/RESET_MODIFY_STATE';
|
||||
|
||||
export const toggleQueryType = ({ query, columns, value }) => ({
|
||||
type: TOGGLE_QUERY_TYPE,
|
||||
query,
|
||||
columns,
|
||||
value,
|
||||
});
|
||||
export const toggleManualType = ({ value }) => ({
|
||||
type: TOGGLE_MANUAL_QUERY_TYPE,
|
||||
data: value,
|
||||
});
|
||||
export const toggleColumn = (query, column) => ({
|
||||
type: TOGGLE_COLUMN,
|
||||
query,
|
||||
column,
|
||||
});
|
||||
|
||||
const REMOVE_HEADER = 'ModifyTrigger/REMOVE_HEADER';
|
||||
const SET_HEADERKEY = 'ModifyTrigger/SET_HEADERKEY';
|
||||
const SET_HEADERTYPE = 'ModifyTrigger/SET_HEADERTYPE';
|
||||
const SET_HEADERVALUE = 'ModifyTrigger/SET_HEADERVALUE';
|
||||
const ADD_HEADER = 'ModifyTrigger/ADD_HEADER';
|
||||
export const addHeader = () => ({ type: ADD_HEADER });
|
||||
export const removeHeader = data => ({ type: REMOVE_HEADER, data });
|
||||
export const setHeaderKey = (data, index) => ({
|
||||
type: SET_HEADERKEY,
|
||||
data,
|
||||
index,
|
||||
});
|
||||
export const setHeaderType = (data, index) => ({
|
||||
type: SET_HEADERTYPE,
|
||||
data,
|
||||
index,
|
||||
});
|
||||
export const setHeaderValue = (data, index) => ({
|
||||
type: SET_HEADERVALUE,
|
||||
data,
|
||||
index,
|
||||
});
|
||||
|
||||
export const REQUEST_ONGOING = 'ModifyTrigger/REQUEST_ONGOING';
|
||||
export const REQUEST_COMPLETE = 'ModifyTrigger/REQUEST_COMPLETE';
|
||||
|
||||
export const showValidationError = message => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showErrorNotification('Error modifying trigger!', 'Invalid input', {
|
||||
custom: message,
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const save = (property, triggerName) => {
|
||||
return (dispatch, getState) => {
|
||||
const { modifyTrigger } = getState();
|
||||
const oldTrigger = getState().triggers.triggerList.find(
|
||||
tr => tr.name === triggerName
|
||||
);
|
||||
const downPayload = {
|
||||
replace: true,
|
||||
name: oldTrigger.name,
|
||||
table: {
|
||||
name: oldTrigger.table_name,
|
||||
schema: oldTrigger.table_schema,
|
||||
},
|
||||
retry_conf: { ...oldTrigger.configuration.retry_conf },
|
||||
...oldTrigger.configuration.definition,
|
||||
headers: [...oldTrigger.configuration.headers],
|
||||
};
|
||||
if (oldTrigger.configuration.webhook_from_env) {
|
||||
downPayload.webhook_from_env = oldTrigger.configuration.webhook_from_env;
|
||||
} else {
|
||||
downPayload.webhook = oldTrigger.configuration.webhook;
|
||||
}
|
||||
const upPayload = {
|
||||
...downPayload,
|
||||
};
|
||||
if (property === 'webhook') {
|
||||
if (modifyTrigger.webhookUrlType === 'env') {
|
||||
delete upPayload.webhook;
|
||||
upPayload.webhook_from_env = modifyTrigger.webhookURL;
|
||||
} else {
|
||||
delete upPayload.webhook_from_env;
|
||||
upPayload.webhook = modifyTrigger.webhookURL;
|
||||
}
|
||||
} else if (property === 'ops') {
|
||||
delete upPayload.update;
|
||||
delete upPayload.delete;
|
||||
delete upPayload.insert;
|
||||
upPayload.update = modifyTrigger.definition.update;
|
||||
upPayload.insert = modifyTrigger.definition.insert;
|
||||
upPayload.delete = modifyTrigger.definition.delete;
|
||||
// Add only if the value is true
|
||||
if (MANUAL_TRIGGER_VAR in modifyTrigger.definition) {
|
||||
delete upPayload[MANUAL_TRIGGER_VAR];
|
||||
upPayload[MANUAL_TRIGGER_VAR] =
|
||||
modifyTrigger.definition[MANUAL_TRIGGER_VAR];
|
||||
}
|
||||
} else if (property === 'retry') {
|
||||
upPayload.retry_conf = {
|
||||
num_retries: modifyTrigger.retryConf.numRetrys,
|
||||
interval_sec: modifyTrigger.retryConf.retryInterval,
|
||||
timeout_sec: modifyTrigger.retryConf.timeout,
|
||||
};
|
||||
} else if (property === 'headers') {
|
||||
delete upPayload.headers;
|
||||
upPayload.headers = [];
|
||||
modifyTrigger.headers
|
||||
.filter(h => Boolean(h.key.trim()))
|
||||
.forEach(h => {
|
||||
const { key, value, type } = h;
|
||||
if (type === 'env') {
|
||||
upPayload.headers.push({
|
||||
name: key.trim(),
|
||||
value_from_env: value.trim(),
|
||||
});
|
||||
} else {
|
||||
upPayload.headers.push({
|
||||
name: key.trim(),
|
||||
value: value.trim(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const upQuery = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
...upPayload,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const downQuery = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
...downPayload,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const migrationName = `modify_tr_${triggerName}_${property}`;
|
||||
const requestMsg = 'Updating trigger';
|
||||
const successMsg = 'Updated trigger';
|
||||
const errorMsg = 'Updating trigger failed';
|
||||
const customOnSuccess = () => {
|
||||
dispatch({ type: REQUEST_COMPLETE });
|
||||
dispatch(setTrigger(triggerName.trim()));
|
||||
dispatch(loadTriggers([triggerName]));
|
||||
return;
|
||||
};
|
||||
const customOnError = err => {
|
||||
dispatch({ type: REQUEST_COMPLETE });
|
||||
dispatch({ type: UPDATE_MIGRATION_STATUS_ERROR, data: err });
|
||||
return;
|
||||
};
|
||||
dispatch({ type: REQUEST_ONGOING, data: property });
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQuery.args,
|
||||
downQuery.args,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg,
|
||||
true
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case SET_WEBHOOK_URL:
|
||||
return {
|
||||
...state,
|
||||
webhookURL: action.data,
|
||||
};
|
||||
case SET_WEBHOOK_URL_TYPE:
|
||||
return {
|
||||
...state,
|
||||
webhookUrlType: action.data,
|
||||
};
|
||||
case TOGGLE_QUERY_TYPE:
|
||||
const newDefinition = { ...state.definition };
|
||||
if (action.value) {
|
||||
if (action.query === 'update') {
|
||||
newDefinition[action.query] = { columns: action.columns };
|
||||
} else {
|
||||
newDefinition[action.query] = { columns: '*' };
|
||||
}
|
||||
} else {
|
||||
delete newDefinition[action.query];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
definition: newDefinition,
|
||||
};
|
||||
case TOGGLE_MANUAL_QUERY_TYPE:
|
||||
return {
|
||||
...state,
|
||||
definition: {
|
||||
...state.definition,
|
||||
[MANUAL_TRIGGER_VAR]: action.data,
|
||||
},
|
||||
};
|
||||
case TOGGLE_COLUMN:
|
||||
const queryColumns = [...state.definition[action.query].columns];
|
||||
if (queryColumns.find(qc => qc === action.column)) {
|
||||
return {
|
||||
...state,
|
||||
definition: {
|
||||
...state.definition,
|
||||
[action.query]: {
|
||||
columns: queryColumns.filter(qc => qc !== action.column),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
definition: {
|
||||
...state.definition,
|
||||
[action.query]: { columns: [...queryColumns, action.column] },
|
||||
},
|
||||
};
|
||||
case SET_RETRY_NUM:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
numRetrys: action.data,
|
||||
},
|
||||
};
|
||||
case SET_RETRY_INTERVAL:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
retryInterval: action.data,
|
||||
},
|
||||
};
|
||||
case SET_RETRY_TIMEOUT:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
timeout: action.data,
|
||||
},
|
||||
};
|
||||
case ADD_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: [...state.headers, { key: '', type: 'static', value: '' }],
|
||||
};
|
||||
case REMOVE_HEADER:
|
||||
return {
|
||||
...state,
|
||||
headers: state.headers.filter((h, i) => i !== action.data),
|
||||
};
|
||||
case SET_HEADERKEY:
|
||||
const kNewHeaders = [...state.headers];
|
||||
kNewHeaders[action.index].key = action.data;
|
||||
return {
|
||||
...state,
|
||||
headers: kNewHeaders,
|
||||
};
|
||||
case SET_HEADERVALUE:
|
||||
const vNewHeaders = [...state.headers];
|
||||
vNewHeaders[action.index].value = action.data;
|
||||
return {
|
||||
...state,
|
||||
headers: vNewHeaders,
|
||||
};
|
||||
case SET_HEADERTYPE:
|
||||
const tNewHeaders = [...state.headers];
|
||||
tNewHeaders[action.index].type = action.data;
|
||||
return {
|
||||
...state,
|
||||
headers: tNewHeaders,
|
||||
};
|
||||
case REQUEST_ONGOING:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: action.data,
|
||||
};
|
||||
case REQUEST_COMPLETE:
|
||||
return {
|
||||
...state,
|
||||
ongoingRequest: null,
|
||||
};
|
||||
case RESET_MODIFY_STATE:
|
||||
return {
|
||||
...defaultState,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
@ -1,18 +0,0 @@
|
||||
import Modify from './Modify';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
modifyTrigger: state.modifyTrigger,
|
||||
modifyTriggerName: ownProps.params.trigger,
|
||||
triggerList: state.triggers.triggerList,
|
||||
schemaList: state.tables.schemaList,
|
||||
allSchemas: state.tables.allSchemas,
|
||||
serverVersion: state.main.serverVersion,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
readOnlyMode: state.main.readOnlyMode,
|
||||
};
|
||||
};
|
||||
|
||||
const modifyTriggerConnector = connect => connect(mapStateToProps)(Modify);
|
||||
|
||||
export default modifyTriggerConnector;
|
@ -1,153 +0,0 @@
|
||||
import React from 'react';
|
||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||
import AceEditor from 'react-ace';
|
||||
import {
|
||||
addHeader,
|
||||
removeHeader,
|
||||
setHeaderKey,
|
||||
setHeaderType,
|
||||
setHeaderValue,
|
||||
} from './Actions';
|
||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||
|
||||
class HeadersEditor extends React.Component {
|
||||
setValues = () => {
|
||||
const { dispatch, headers, modifyTrigger } = this.props;
|
||||
headers.forEach((h, i) => {
|
||||
const { name, value, value_from_env } = h;
|
||||
dispatch(setHeaderKey(name, i));
|
||||
dispatch(setHeaderType(value_from_env ? 'env' : 'static', i));
|
||||
dispatch(setHeaderValue(value || value_from_env, i));
|
||||
if (!modifyTrigger[i + 1]) {
|
||||
this.addExtraHeader();
|
||||
}
|
||||
});
|
||||
};
|
||||
addExtraHeader = () => {
|
||||
const { dispatch, modifyTrigger } = this.props;
|
||||
const lastHeader = modifyTrigger.headers[modifyTrigger.headers.length - 1];
|
||||
if (lastHeader.key && lastHeader.value && lastHeader.type) {
|
||||
dispatch(addHeader());
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectionChange = (e, i) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(setHeaderType(e.target.getAttribute('value'), i));
|
||||
dispatch(setHeaderValue('', i));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { headers, styles, save, modifyTrigger, dispatch } = this.props;
|
||||
const sanitiseHeaders = rawHeaders => {
|
||||
return rawHeaders.map(h => {
|
||||
return {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
value_from_env: h.value_from_env,
|
||||
};
|
||||
});
|
||||
};
|
||||
const collapsed = () => (
|
||||
<div>
|
||||
{headers.length > 0 ? (
|
||||
<div className={styles.modifyHeaders}>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="headers"
|
||||
value={JSON.stringify(sanitiseHeaders(headers), null, 2)}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.modifyProperty}>No headers</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const expanded = () => (
|
||||
<div className={styles.modifyOpsPadLeft}>
|
||||
{modifyTrigger.headers.map((h, i) => {
|
||||
return (
|
||||
<div className={styles.modifyHeadersCollapsedContent} key={i}>
|
||||
<input
|
||||
type="text"
|
||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyHeadersTextbox}`}
|
||||
value={h.key}
|
||||
onChange={e => {
|
||||
dispatch(setHeaderKey(e.target.value, i));
|
||||
}}
|
||||
placeholder="key"
|
||||
/>
|
||||
<div className={styles.dropDownGroup}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'Value', value: 'static' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={h.type === 'env' ? 'From env var' : 'Value'}
|
||||
dataKey={h.type === 'env' ? 'env' : 'static'}
|
||||
onButtonChange={e => this.handleSelectionChange(e, i)}
|
||||
onInputChange={e => {
|
||||
dispatch(setHeaderValue(e.target.value, i));
|
||||
this.addExtraHeader();
|
||||
}}
|
||||
required
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={h.value}
|
||||
id={`header-value-${i}`}
|
||||
inputPlaceHolder={
|
||||
h.type === 'env' ? 'HEADER_FROM_ENV' : 'value'
|
||||
}
|
||||
testId={`header-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose}
|
||||
${styles.removeHeader}
|
||||
${
|
||||
i !== modifyTrigger.headers.length - 1 &&
|
||||
'fa-lg fa fa-times'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(removeHeader(i));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
||||
<div className={styles.modifySection}>
|
||||
<h4 className={styles.modifySectionHeading}>
|
||||
Headers{' '}
|
||||
<Tooltip message="Edit headers to be sent along with the event to your webhook" />
|
||||
</h4>
|
||||
<Editor
|
||||
editorCollapsed={collapsed}
|
||||
editorExpanded={expanded}
|
||||
expandCallback={this.setValues}
|
||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
||||
property="headers"
|
||||
service="modify-trigger"
|
||||
saveFunc={save}
|
||||
styles={styles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HeadersEditor;
|
@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import styles from './ModifyEvent.scss';
|
||||
|
||||
import { getTableColumns } from '../utils';
|
||||
|
||||
import Info from './Info';
|
||||
import WebhookEditor from './WebhookEditor';
|
||||
import OperationEditor from './OperationEditor';
|
||||
import RetryConfEditor from './RetryConfEditor';
|
||||
import HeadersEditor from './HeadersEditor';
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
import { save, setDefaults, RESET_MODIFY_STATE } from './Actions';
|
||||
|
||||
import { NotFoundError } from '../../../Error/PageNotFound';
|
||||
|
||||
class Modify extends React.Component {
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(setDefaults());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: RESET_MODIFY_STATE,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
modifyTriggerName,
|
||||
modifyTrigger,
|
||||
triggerList,
|
||||
readOnlyMode,
|
||||
dispatch,
|
||||
} = this.props;
|
||||
|
||||
const currentTrigger = triggerList.find(
|
||||
tr => tr.name === modifyTriggerName
|
||||
);
|
||||
|
||||
if (!currentTrigger) {
|
||||
// throw a 404 exception
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const {
|
||||
definition,
|
||||
headers,
|
||||
webhook,
|
||||
webhook_from_env,
|
||||
retry_conf,
|
||||
} = currentTrigger.configuration;
|
||||
|
||||
return (
|
||||
<div className={styles.containerWhole + ' container-fluid'}>
|
||||
<TableHeader
|
||||
dispatch={dispatch}
|
||||
triggerName={modifyTriggerName}
|
||||
tabName="modify"
|
||||
readOnlyMode={readOnlyMode}
|
||||
/>
|
||||
<br />
|
||||
<div className={styles.container}>
|
||||
<Info
|
||||
triggerName={currentTrigger.name}
|
||||
tableName={currentTrigger.table_name}
|
||||
schemaName={currentTrigger.table_schema}
|
||||
styles={styles}
|
||||
/>
|
||||
<WebhookEditor
|
||||
webhook={webhook || webhook_from_env}
|
||||
dispatch={dispatch}
|
||||
modifyTrigger={modifyTrigger}
|
||||
env={Boolean(webhook_from_env)}
|
||||
newWebhook={null}
|
||||
save={() => dispatch(save('webhook', modifyTriggerName))}
|
||||
styles={styles}
|
||||
/>
|
||||
<OperationEditor
|
||||
definition={definition}
|
||||
allTableColumns={getTableColumns(currentTrigger)}
|
||||
dispatch={dispatch}
|
||||
modifyTrigger={modifyTrigger}
|
||||
newDefinition={null}
|
||||
styles={styles}
|
||||
save={() => dispatch(save('ops', modifyTriggerName))}
|
||||
/>
|
||||
<RetryConfEditor
|
||||
retryConf={retry_conf}
|
||||
modifyTrigger={modifyTrigger}
|
||||
styles={styles}
|
||||
save={() => dispatch(save('retry', modifyTriggerName))}
|
||||
serverVersion={this.props.serverVersion}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<HeadersEditor
|
||||
headers={headers}
|
||||
styles={styles}
|
||||
modifyTrigger={modifyTrigger}
|
||||
save={() => dispatch(save('headers', modifyTriggerName))}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<ActionButtons
|
||||
styles={styles}
|
||||
dispatch={dispatch}
|
||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
||||
triggerName={modifyTriggerName}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Modify;
|
@ -1,229 +0,0 @@
|
||||
import React from 'react';
|
||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||
|
||||
import { toggleQueryType, toggleColumn, toggleManualType } from './Actions';
|
||||
|
||||
import {
|
||||
getTriggerOperations,
|
||||
triggerOperationMap,
|
||||
MANUAL_TRIGGER_VAR,
|
||||
} from './utils';
|
||||
|
||||
class OperationEditor extends React.Component {
|
||||
toggleOperation = upObj => {
|
||||
if (upObj.query === MANUAL_TRIGGER_VAR) {
|
||||
return toggleManualType(upObj);
|
||||
}
|
||||
return toggleQueryType(upObj);
|
||||
};
|
||||
|
||||
setValues = () => {
|
||||
const { dispatch, definition } = this.props;
|
||||
/*
|
||||
* Loop through the keys in definition,
|
||||
* this object will have actual internal name.
|
||||
* No need to transform from display to internal name
|
||||
* */
|
||||
for (const queryType in definition) {
|
||||
/* If the definition[queryType] holds true
|
||||
* This will be false or undefined if `queryType` doesn't exist in the object or definition[queryType] is false.
|
||||
* */
|
||||
if (queryType in definition) {
|
||||
if (queryType !== MANUAL_TRIGGER_VAR) {
|
||||
dispatch(
|
||||
this.toggleOperation({
|
||||
query: queryType,
|
||||
columns: definition[queryType].columns,
|
||||
value: true,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
this.toggleOperation({
|
||||
query: queryType,
|
||||
value: definition[queryType],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
definition,
|
||||
allTableColumns,
|
||||
styles,
|
||||
save,
|
||||
modifyTrigger,
|
||||
dispatch,
|
||||
} = this.props;
|
||||
/*
|
||||
* Query types will have `CONSOLE_QUERY` only for version > 45
|
||||
*
|
||||
* */
|
||||
const operationTypes = getTriggerOperations();
|
||||
const renderOperation = (qt, i) => {
|
||||
const isChecked = Boolean(definition[triggerOperationMap[qt]]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles.opsCheckboxWrapper + ' col-md-2 ' + styles.padd_remove
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.opsCheckboxDisabled}
|
||||
checked={isChecked}
|
||||
disabled
|
||||
/>
|
||||
{qt}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const collapsed = () => (
|
||||
<div className={styles.modifyOps}>
|
||||
<div className={styles.modifyOpsCollapsedContent}>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{operationTypes.map((qt, i) => renderOperation(qt, i))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent}>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
Listen columns for update:
|
||||
</div>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{definition.update ? (
|
||||
allTableColumns.map((col, i) => (
|
||||
<div
|
||||
className={`${styles.opsCheckboxWrapper} ${styles.columnListElement} ${styles.padd_remove}`}
|
||||
key={i}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.opsCheckboxDisabled}
|
||||
checked={Boolean(
|
||||
definition.update.columns.find(c => c === col.name)
|
||||
)}
|
||||
disabled
|
||||
/>
|
||||
{col.name}
|
||||
<small className={styles.addPaddSmall}> ({col.type})</small>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
'col-md-12 ' +
|
||||
styles.padd_remove +
|
||||
' ' +
|
||||
styles.modifyOpsCollapsedtitle
|
||||
}
|
||||
>
|
||||
<i>Applicable only if update operation is selected.</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const expanded = () => (
|
||||
<div className={styles.modifyOpsPadLeft}>
|
||||
<div className={styles.modifyOpsCollapsedContent}>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{operationTypes.map((qt, i) => (
|
||||
<div
|
||||
className={`${styles.opsCheckboxWrapper} col-md-2 ${styles.padd_remove} ${styles.cursorPointer}`}
|
||||
key={i}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
this.toggleOperation({
|
||||
query: triggerOperationMap[qt],
|
||||
columns: allTableColumns.map(c => c.name),
|
||||
value: !modifyTrigger.definition[triggerOperationMap[qt]],
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={`${styles.opsCheckbox} ${styles.cursorPointer}`}
|
||||
checked={Boolean(
|
||||
modifyTrigger.definition[triggerOperationMap[qt]]
|
||||
)}
|
||||
/>
|
||||
{qt}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent}>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
Listen columns for update:
|
||||
</div>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{modifyTrigger.definition.update ? (
|
||||
allTableColumns.map((col, i) => (
|
||||
<div
|
||||
className={`${styles.opsCheckboxWrapper} ${styles.columnListElement} ${styles.padd_remove} ${styles.cursorPointer}`}
|
||||
key={i}
|
||||
onClick={() => dispatch(toggleColumn('update', col.name))}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={`${styles.opsCheckbox} ${styles.cursorPointer}`}
|
||||
checked={Boolean(
|
||||
modifyTrigger.definition.update.columns.find(
|
||||
c => c === col.name
|
||||
)
|
||||
)}
|
||||
/>
|
||||
{col.name}
|
||||
<small className={styles.addPaddSmall}> ({col.type})</small>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
'col-md-12 ' +
|
||||
styles.padd_remove +
|
||||
' ' +
|
||||
styles.modifyOpsCollapsedtitle
|
||||
}
|
||||
>
|
||||
<i>Applicable only if update operation is selected.</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
||||
<div className={styles.modifySection}>
|
||||
<h4 className={styles.modifySectionHeading}>
|
||||
Trigger Operations{' '}
|
||||
<Tooltip message="Edit operations and related columns" />
|
||||
</h4>
|
||||
<Editor
|
||||
editorCollapsed={collapsed}
|
||||
editorExpanded={expanded}
|
||||
styles={styles}
|
||||
property="ops"
|
||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
||||
service="modify-trigger"
|
||||
saveFunc={save}
|
||||
expandCallback={this.setValues}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OperationEditor;
|
@ -1,154 +0,0 @@
|
||||
import React from 'react';
|
||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||
import {
|
||||
setRetryNum,
|
||||
setRetryInterval,
|
||||
setRetryTimeout,
|
||||
showValidationError,
|
||||
} from './Actions';
|
||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||
|
||||
class RetryConfEditor extends React.Component {
|
||||
setValues = () => {
|
||||
const { dispatch } = this.props;
|
||||
const retryConf = this.props.retryConf || {};
|
||||
dispatch(setRetryNum(retryConf.num_retries || 0));
|
||||
dispatch(setRetryInterval(retryConf.interval_sec || 10));
|
||||
dispatch(setRetryTimeout(retryConf.timeout_sec || 60));
|
||||
};
|
||||
|
||||
validateAndSave = () => {
|
||||
const {
|
||||
dispatch,
|
||||
modifyTrigger: {
|
||||
retryConf: { numRetrys, retryInterval, timeout },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const iNumRetries = numRetrys === '' ? 0 : parseInt(numRetrys, 10);
|
||||
const iRetryInterval =
|
||||
retryInterval === '' ? 10 : parseInt(retryInterval, 10);
|
||||
const iTimeout = timeout === '' ? 60 : parseInt(timeout, 10);
|
||||
|
||||
if (iNumRetries < 0 || isNaN(iNumRetries)) {
|
||||
dispatch(
|
||||
showValidationError('Number of retries must be a non negative number!')
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(setRetryNum(iNumRetries));
|
||||
|
||||
if (iRetryInterval <= 0 || isNaN(iRetryInterval)) {
|
||||
dispatch(showValidationError('Retry interval must be a postive number!'));
|
||||
return;
|
||||
}
|
||||
dispatch(setRetryInterval(iRetryInterval));
|
||||
|
||||
if (isNaN(iTimeout) || iTimeout <= 0) {
|
||||
dispatch(showValidationError('Timeout must be a positive number!'));
|
||||
return;
|
||||
}
|
||||
dispatch(setRetryTimeout(iTimeout));
|
||||
|
||||
this.props.save();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { styles, dispatch, modifyTrigger } = this.props;
|
||||
const retryConf = this.props.retryConf || {};
|
||||
|
||||
const collapsed = () => (
|
||||
<div className={styles.modifyOps}>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={'col-md-4 ' + styles.padd_remove}>
|
||||
Number of retries:
|
||||
</div>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{retryConf.num_retries || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={'col-md-4 ' + styles.padd_remove}>
|
||||
Retry Interval (sec):
|
||||
</div>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{retryConf.interval_sec || 10}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={'col-md-4 ' + styles.padd_remove}>Timeout (sec):</div>
|
||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
||||
{retryConf.timeout_sec || 60}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const expanded = () => (
|
||||
<div className={styles.modifyOpsPadLeft}>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
||||
Number of retries:
|
||||
</div>
|
||||
<div className="col-md-12">
|
||||
<input
|
||||
type="text"
|
||||
value={modifyTrigger.retryConf.numRetrys}
|
||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
||||
onChange={e => dispatch(setRetryNum(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
||||
Retry interval (sec):
|
||||
</div>
|
||||
<div className="col-md-12">
|
||||
<input
|
||||
type="text"
|
||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
||||
value={modifyTrigger.retryConf.retryInterval}
|
||||
onChange={e => dispatch(setRetryInterval(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modifyOpsCollapsedContent1}>
|
||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
||||
Timeout (sec):
|
||||
</div>
|
||||
<div className="col-md-12">
|
||||
<input
|
||||
type="text"
|
||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
||||
value={modifyTrigger.retryConf.timeout}
|
||||
onChange={e => dispatch(setRetryTimeout(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
||||
<div className={styles.modifySection}>
|
||||
<h4 className={styles.modifySectionHeading}>
|
||||
Retry configuration{' '}
|
||||
<Tooltip message="Edit your retry settings for event failures" />
|
||||
</h4>
|
||||
<Editor
|
||||
editorCollapsed={collapsed}
|
||||
editorExpanded={expanded}
|
||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
||||
property={'retry'}
|
||||
saveFunc={this.validateAndSave}
|
||||
service="modify-trigger"
|
||||
expandCallback={this.setValues}
|
||||
styles={styles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RetryConfEditor;
|
@ -1,17 +0,0 @@
|
||||
const defaultState = {
|
||||
definition: {},
|
||||
webhookURL: '',
|
||||
webhookUrlType: 'url',
|
||||
retryConf: {
|
||||
numRetrys: 0,
|
||||
retryInterval: 10,
|
||||
timeout: 60,
|
||||
},
|
||||
ongoingRequest: false,
|
||||
lastError: null,
|
||||
internalError: null,
|
||||
lastSuccess: null,
|
||||
headers: [{ key: '', type: 'static', value: '' }],
|
||||
};
|
||||
|
||||
export default defaultState;
|
@ -1,114 +0,0 @@
|
||||
import React from 'react';
|
||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
||||
import {
|
||||
setWebhookUrl,
|
||||
setWebhookUrlType,
|
||||
showValidationError,
|
||||
} from './Actions';
|
||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
||||
|
||||
class WebhookEditor extends React.Component {
|
||||
setValues = () => {
|
||||
const { webhook, env, dispatch } = this.props;
|
||||
dispatch(setWebhookUrl(webhook));
|
||||
dispatch(setWebhookUrlType(env ? 'env' : 'url'));
|
||||
};
|
||||
|
||||
handleSelectionChange = e => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(setWebhookUrlType(e.target.getAttribute('value')));
|
||||
dispatch(setWebhookUrl(''));
|
||||
};
|
||||
|
||||
validateAndSave = () => {
|
||||
const { modifyTrigger, dispatch } = this.props;
|
||||
if (modifyTrigger.webhookUrlType === 'url') {
|
||||
let tempUrl = false;
|
||||
try {
|
||||
tempUrl = new URL(modifyTrigger.webhookURL);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (!tempUrl) {
|
||||
dispatch(showValidationError('Invalid URL'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.props.save();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
webhook,
|
||||
modifyTrigger,
|
||||
env,
|
||||
dispatch,
|
||||
styles,
|
||||
save: saveWebhook,
|
||||
} = this.props;
|
||||
const collapsed = () => (
|
||||
<div className={styles.modifyProperty}>
|
||||
<p>
|
||||
{webhook}
|
||||
|
||||
</p>
|
||||
<i>{env && '- from env'}</i>
|
||||
</div>
|
||||
);
|
||||
|
||||
const expanded = () => (
|
||||
<div className={styles.modifyWhDropdownWrapper}>
|
||||
<DropdownButton
|
||||
dropdownOptions={[
|
||||
{ display_text: 'URL', value: 'url' },
|
||||
{ display_text: 'From env var', value: 'env' },
|
||||
]}
|
||||
title={
|
||||
modifyTrigger.webhookUrlType === 'env' ? 'From env var' : 'URL'
|
||||
}
|
||||
dataKey={modifyTrigger.webhookUrlType === 'env' ? 'env' : 'url'}
|
||||
onButtonChange={this.handleSelectionChange}
|
||||
onInputChange={e => dispatch(setWebhookUrl(e.target.value))}
|
||||
required
|
||||
bsClass={styles.dropdown_button}
|
||||
inputVal={modifyTrigger.webhookURL}
|
||||
id="webhook-url"
|
||||
inputPlaceHolder={
|
||||
modifyTrigger.webhookUrlType === 'env'
|
||||
? 'MY_WEBHOOK_URL'
|
||||
: 'http://httpbin.org/post'
|
||||
}
|
||||
testId="webhook"
|
||||
/>
|
||||
<br />
|
||||
<small>
|
||||
Note: Specifying the webhook URL via an environmental variable is
|
||||
recommended if you have different URLs for multiple environments.
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
||||
<div className={styles.modifySection}>
|
||||
<h4 className={styles.modifySectionHeading}>
|
||||
Webhook URL <Tooltip message="Edit your webhook URL" />
|
||||
</h4>
|
||||
<Editor
|
||||
editorCollapsed={collapsed}
|
||||
editorExpanded={expanded}
|
||||
expandCallback={this.setValues}
|
||||
property="webhook"
|
||||
service="modify-trigger"
|
||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
||||
styles={styles}
|
||||
saveFunc={saveWebhook}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebhookEditor;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user