console: add scheduled triggers support (#4732)

This commit is contained in:
Rishichandra Wawhal 2020-06-05 13:40:08 +05:30 committed by GitHub
parent ae75c6c06e
commit aaab6d3eb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
217 changed files with 9704 additions and 11784 deletions

View File

@ -82,6 +82,7 @@
"jsx-a11y/no-autofocus": 0, "jsx-a11y/no-autofocus": 0,
"max-len": 0, "max-len": 0,
"no-continue": 0, "no-continue": 0,
"no-new": 0,
"eqeqeq": 0, "eqeqeq": 0,
"no-nested-ternary": 0 "no-nested-ternary": 0
}, },
@ -160,8 +161,12 @@
"no-unused-expressions": "off", "no-unused-expressions": "off",
"no-console": "off", "no-console": "off",
"prefer-destructuring": "off", "prefer-destructuring": "off",
"jsx-a11y/click-events-have-key-events": "off", "no-plusplus": "off",
"jsx-a11y/anchor-is-valid": "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", "jsx-a11y/interactive-supports-focus": "off",
"no-restricted-properties": "off", "no-restricted-properties": "off",
"react/no-danger": "off", "react/no-danger": "off",

View File

@ -21,10 +21,12 @@ import {
} from '../../validators/validators'; } from '../../validators/validators';
import { setPromptValue } from '../../../helpers/common'; import { setPromptValue } from '../../../helpers/common';
const EVENT_TRIGGER_INDEX_ROUTE = '/events/data';
const testName = 'ctr'; // create trigger const testName = 'ctr'; // create trigger
export const visitEventsManagePage = () => { export const visitEventsManagePage = () => {
cy.visit('/events/manage'); cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/manage`);
}; };
export const passPTCreateTable = () => { export const passPTCreateTable = () => {
@ -69,11 +71,11 @@ export const passPTCreateTable = () => {
export const checkCreateTriggerRoute = () => { export const checkCreateTriggerRoute = () => {
// Click on the create trigger button // Click on the create trigger button
cy.visit('/events/manage'); cy.visit(EVENT_TRIGGER_INDEX_ROUTE);
cy.wait(15000); cy.wait(15000);
cy.get(getElementFromAlias('data-create-trigger')).click(); cy.get(getElementFromAlias('data-sidebar-add')).click();
// Match the URL // 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 = () => { export const failCTWithoutData = () => {
@ -82,7 +84,7 @@ export const failCTWithoutData = () => {
// Click on create // Click on create
cy.get(getElementFromAlias('trigger-create')).click(); cy.get(getElementFromAlias('trigger-create')).click();
// Check if the route didn't change // 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 // Validate
validateCT(getTriggerName(0, testName), ResultType.FAILURE); validateCT(getTriggerName(0, testName), ResultType.FAILURE);
}; };
@ -108,9 +110,15 @@ export const passCT = () => {
cy.get(getElementFromAlias('advanced-settings')).click(); cy.get(getElementFromAlias('advanced-settings')).click();
// retry configuration // retry configuration
cy.get(getElementFromAlias('no-of-retries')).type(getNoOfRetries()); cy.get(getElementFromAlias('no-of-retries'))
cy.get(getElementFromAlias('interval-seconds')).type(getIntervalSeconds()); .clear()
cy.get(getElementFromAlias('timeout-seconds')).type(getTimeoutSeconds()); .type(getNoOfRetries());
cy.get(getElementFromAlias('interval-seconds'))
.clear()
.type(getIntervalSeconds());
cy.get(getElementFromAlias('timeout-seconds'))
.clear()
.type(getTimeoutSeconds());
// Click on create // Click on create
cy.get(getElementFromAlias('trigger-create')).click(); 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 // Check if the trigger got created and navigated to processed events page
cy.url().should( cy.url().should(
'eq', 'eq',
`${baseUrl}/events/manage/triggers/${getTriggerName(0, testName)}/processed` `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(
0,
testName
)}/modify`
); );
cy.get(getElementFromAlias(getTriggerName(0, testName))); cy.get(getElementFromAlias(getTriggerName(0, testName)));
// Validate // Validate
@ -127,7 +138,7 @@ export const passCT = () => {
export const failCTDuplicateTrigger = () => { export const failCTDuplicateTrigger = () => {
// Visit create trigger page // Visit create trigger page
cy.visit('/events/manage/triggers/add'); cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/add`);
// trigger and table name // trigger and table name
cy.get(getElementFromAlias('trigger-name')) cy.get(getElementFromAlias('trigger-name'))
.clear() .clear()
@ -148,7 +159,7 @@ export const failCTDuplicateTrigger = () => {
cy.get(getElementFromAlias('trigger-create')).click(); cy.get(getElementFromAlias('trigger-create')).click();
cy.wait(5000); cy.wait(5000);
// should be on the same URL // 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 = () => { export const insertTableRow = () => {
@ -163,13 +174,17 @@ export const insertTableRow = () => {
// now it should invoke the trigger to webhook // now it should invoke the trigger to webhook
cy.wait(10000); cy.wait(10000);
// check if processed events has a row and it is a successful response // check if processed events has a row and it is a successful response
cy.visit(`/events/manage/triggers/${getTriggerName(0, testName)}/processed`); cy.visit(
cy.get(getElementFromAlias('trigger-processed-events')).contains('1'); `${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(0, testName)}/processed`
);
cy.get('.rt-tr-group').should('have.length', 1);
}; };
export const deleteCTTestTrigger = () => { export const deleteCTTestTrigger = () => {
// Go to the settings section of the trigger // 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 // click on settings tab
cy.get(getElementFromAlias('trigger-modify')).click(); cy.get(getElementFromAlias('trigger-modify')).click();
setPromptValue(getTriggerName(0, testName)); setPromptValue(getTriggerName(0, testName));
@ -181,7 +196,7 @@ export const deleteCTTestTrigger = () => {
.should('be.called'); .should('be.called');
cy.wait(7000); cy.wait(7000);
// Match the URL // Match the URL
cy.url().should('eq', `${baseUrl}/events/manage/triggers`); cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/manage`);
// Validate // Validate
validateCTrigger(getTriggerName(0, testName), ResultType.FAILURE); validateCTrigger(getTriggerName(0, testName), ResultType.FAILURE);
}; };

View File

@ -3259,6 +3259,40 @@
"@types/react": "*" "@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": { "@types/react-redux": {
"version": "7.1.7", "version": "7.1.7",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.7.tgz",
@ -3870,9 +3904,9 @@
} }
}, },
"ace-builds": { "ace-builds": {
"version": "1.4.8", "version": "1.4.11",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.8.tgz", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.11.tgz",
"integrity": "sha512-8ZVAxwyCGAxQX8mOp9imSXH0hoSPkGfy8igJy+WO/7axL30saRhKgg1XPACSmxxPA7nfHVwM+ShWXT+vKsNuFg==" "integrity": "sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw=="
}, },
"acorn": { "acorn": {
"version": "7.1.1", "version": "7.1.1",
@ -6611,6 +6645,12 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "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": { "supports-color": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
@ -12843,10 +12883,9 @@
} }
}, },
"moment": { "moment": {
"version": "2.24.0", "version": "2.26.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", "integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
"dev": true
}, },
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
@ -13688,7 +13727,7 @@
}, },
"onetime": { "onetime": {
"version": "1.1.0", "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=", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true "dev": true
}, },
@ -15232,6 +15271,24 @@
"prop-types": "^15.5.8" "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": { "react-dom": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@ -15358,6 +15415,11 @@
"react-notification-system": "^0.2.x" "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": { "react-overlays": {
"version": "0.8.3", "version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz",

View File

@ -50,6 +50,7 @@
"dependencies": { "dependencies": {
"@graphql-codegen/core": "1.13.5", "@graphql-codegen/core": "1.13.5",
"@graphql-codegen/typescript": "1.13.5", "@graphql-codegen/typescript": "1.13.5",
"ace-builds": "^1.4.11",
"apollo-link": "1.2.14", "apollo-link": "1.2.14",
"apollo-link-ws": "1.0.20", "apollo-link-ws": "1.0.20",
"brace": "0.11.1", "brace": "0.11.1",
@ -62,6 +63,7 @@
"isomorphic-fetch": "2.2.1", "isomorphic-fetch": "2.2.1",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"less": "3.11.1", "less": "3.11.1",
"moment": "^2.26.0",
"piping": "0.3.2", "piping": "0.3.2",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react": "16.13.1", "react": "16.13.1",
@ -69,6 +71,7 @@
"react-autosuggest": "10.0.2", "react-autosuggest": "10.0.2",
"react-bootstrap": "0.32.4", "react-bootstrap": "0.32.4",
"react-copy-to-clipboard": "5.0.2", "react-copy-to-clipboard": "5.0.2",
"react-datetime": "^2.16.3",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-helmet": "5.2.1", "react-helmet": "5.2.1",
"react-icons": "3.9.0", "react-icons": "3.9.0",
@ -120,6 +123,7 @@
"@types/react-dom": "16.9.5", "@types/react-dom": "16.9.5",
"@types/react-helmet": "5.0.15", "@types/react-helmet": "5.0.15",
"@types/react-hot-loader": "4.1.1", "@types/react-hot-loader": "4.1.1",
"@types/react-notification-system-redux": "1.1.6",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/react-router": "^3.0.8", "@types/react-router": "^3.0.8",
"@types/react-router-redux": "4.0.44", "@types/react-router-redux": "4.0.44",

View File

@ -4,7 +4,7 @@ const baseUrl = globals.dataApiUrl;
const hasuractlApiHost = globals.apiHost; const hasuractlApiHost = globals.apiHost;
const hasuractlApiPort = globals.apiPort; const hasuractlApiPort = globals.apiPort;
const hasuractlUrl = hasuractlApiHost + ':' + hasuractlApiPort; const hasuractlUrl = `${hasuractlApiHost}:${hasuractlApiPort}`;
const Endpoints = { const Endpoints = {
getSchema: `${baseUrl}/v1/query`, getSchema: `${baseUrl}/v1/query`,

View File

@ -5,7 +5,6 @@ import { isEmpty } from './components/Common/utils/jsUtils';
// TODO: move this section to a more appropriate location // TODO: move this section to a more appropriate location
/* set helper tools into window */ /* set helper tools into window */
import sqlFormatter from './helpers/sql-formatter.min'; import sqlFormatter from './helpers/sql-formatter.min';
import hljs from './helpers/highlight.min'; import hljs from './helpers/highlight.min';
@ -47,7 +46,6 @@ const globals = {
telemetryNotificationShown: '', telemetryNotificationShown: '',
isProduction, isProduction,
}; };
if (globals.consoleMode === SERVER_CONSOLE_MODE) { if (globals.consoleMode === SERVER_CONSOLE_MODE) {
if (isProduction) { if (isProduction) {
const consolePath = window.__env.consolePath; const consolePath = window.__env.consolePath;

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,5 +1,4 @@
import defaultState from './State'; import defaultState from './State';
import Notifications from 'react-notification-system-redux';
import { loadConsoleOpts } from '../../telemetry/Actions'; import { loadConsoleOpts } from '../../telemetry/Actions';
import { fetchServerConfig } from '../Main/Actions'; import { fetchServerConfig } from '../Main/Actions';
@ -24,29 +23,6 @@ const CONNECTION_FAILED = 'App/CONNECTION_FAILED';
* onRemove: function, null, same as onAdd * onRemove: function, null, same as onAdd
* uid: integer/string, null, unique identifier to the notification, same uid will not be shown again * 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 }) => { export const requireAsyncGlobals = ({ dispatch }) => {
return (nextState, finalState, callback) => { return (nextState, finalState, callback) => {
@ -119,5 +95,4 @@ export {
FAILED_REQUEST, FAILED_REQUEST,
ERROR_REQUEST, ERROR_REQUEST,
CONNECTION_FAILED, CONNECTION_FAILED,
showNotification,
}; };

View File

@ -5,7 +5,6 @@ import ProgressBar from 'react-progress-bar-plus';
import Notifications from 'react-notification-system-redux'; import Notifications from 'react-notification-system-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { ThemeProvider } from 'styled-components'; import { ThemeProvider } from 'styled-components';
import ErrorBoundary from '../Error/ErrorBoundary'; import ErrorBoundary from '../Error/ErrorBoundary';
import { telemetryNotificationShown } from '../../telemetry/Actions'; import { telemetryNotificationShown } from '../../telemetry/Actions';
import { showTelemetryNotification } from '../../telemetry/Notifications'; import { showTelemetryNotification } from '../../telemetry/Notifications';

View File

@ -1,6 +1,7 @@
import globals from 'Globals'; import globals from 'Globals';
const stateKey = 'CONSOLE_LOCAL_INFO:' + globals.dataApiUrl; const stateKey = 'CONSOLE_LOCAL_INFO:' + globals.dataApiUrl;
const CONSOLE_ADMIN_SECRET = 'CONSOLE_ADMIN_SECRET'; const CONSOLE_ADMIN_SECRET = 'CONSOLE_ADMIN_SECRET';
const loadAppState = () => JSON.parse(window.localStorage.getItem(stateKey)); const loadAppState = () => JSON.parse(window.localStorage.getItem(stateKey));

View File

@ -1,18 +1,17 @@
import React from 'react'; import React from 'react';
import AceEditor from 'react-ace'; import AceEditor, { IAceEditorProps } from 'react-ace';
import { ACE_EDITOR_THEME, ACE_EDITOR_FONT_SIZE } from './utils';
import 'ace-builds/src-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools'; import 'ace-builds/src-noconflict/ext-language_tools';
import 'ace-builds/src-noconflict/ext-error_marker'; import 'ace-builds/src-noconflict/ext-error_marker';
import 'ace-builds/src-noconflict/ext-beautify'; 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 ( return (
<AceEditor <AceEditor
mode={mode} mode={mode}
theme={ACE_EDITOR_THEME} theme={ACE_EDITOR_THEME}
fontSize={ACE_EDITOR_FONT_SIZE} fontSize={ACE_EDITOR_FONT_SIZE}
showPrintMargine
showGutter showGutter
tabSize={2} tabSize={2}
setOptions={{ setOptions={{

View File

@ -1,3 +1,5 @@
// eslint-disable-file import/no-extraneous-dependencies
import 'ace-builds/src-noconflict/theme-eclipse'; import 'ace-builds/src-noconflict/theme-eclipse';
import 'ace-builds/src-noconflict/mode-graphqlschema'; import 'ace-builds/src-noconflict/mode-graphqlschema';
import 'ace-builds/src-noconflict/mode-sql'; 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_THEME = 'eclipse';
export const ACE_EDITOR_FONT_SIZE = 14; export const ACE_EDITOR_FONT_SIZE = 14;
export const getLanguageModeFromExtension = extension => { export const getLanguageModeFromExtension = (extension: string) => {
switch (extension) { switch (extension) {
case 'ts': case 'ts':
return 'typescript'; return 'typescript';

View File

@ -10,8 +10,8 @@ import styles from './CollapsibleToggle.scss';
*/ */
interface CollapsibleToggleProps { interface CollapsibleToggleProps {
title: string; title: React.ReactNode;
isOpen: boolean; isOpen?: boolean;
toggleHandler?: () => void; toggleHandler?: () => void;
testId: string; testId: string;
useDefaultTitleStyle?: boolean; useDefaultTitleStyle?: boolean;
@ -40,7 +40,7 @@ class CollapsibleToggle extends React.Component<
const { isOpen, toggleHandler } = nextProps; const { isOpen, toggleHandler } = nextProps;
if (toggleHandler) { if (toggleHandler) {
this.setState({ isOpen, toggleHandler }); this.setState({ isOpen: !!isOpen, toggleHandler });
} }
} }

View File

@ -478,6 +478,10 @@ input {
padding-right: 15px; padding-right: 15px;
} }
.addPadding20Px {
padding: 20px;
}
.width_auto { .width_auto {
width: auto; width: auto;
} }

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View 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,
};
};

View 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;

View 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;
}
};

View File

@ -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;

View 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;

View File

@ -1,13 +1,11 @@
const emptyHeader = { import { Header as HeaderClient, defaultHeader } from './Headers';
name: '', import { Header as HeaderServer } from '../utils/v1QueryUtils';
value: '',
type: 'static',
};
export const transformHeaders = (headers = []) => { export const transformHeaders = (headers_?: HeaderClient[]) => {
const headers = headers_ || [];
return headers return headers
.map(h => { .map(h => {
const transformedHeader = { const transformedHeader: HeaderServer = {
name: h.name, name: h.name,
}; };
if (h.type === 'static') { if (h.type === 'static') {
@ -20,24 +18,24 @@ export const transformHeaders = (headers = []) => {
.filter(h => !!h.name && (!!h.value || !!h.value_from_env)); .filter(h => !!h.name && (!!h.value || !!h.value_from_env));
}; };
export const addPlaceholderHeader = newHeaders => { export const addPlaceholderHeader = (newHeaders: HeaderClient[]) => {
if (newHeaders.length) { if (newHeaders.length) {
const lastHeader = newHeaders[newHeaders.length - 1]; const lastHeader = newHeaders[newHeaders.length - 1];
if (lastHeader.name && lastHeader.value) { if (lastHeader.name && lastHeader.value) {
newHeaders.push(emptyHeader); newHeaders.push(defaultHeader);
} }
} else { } else {
newHeaders.push(emptyHeader); newHeaders.push(defaultHeader);
} }
return newHeaders; return newHeaders;
}; };
export const parseServerHeaders = (headers = []) => { export const parseServerHeaders = (headers: HeaderServer[] = []) => {
return addPlaceholderHeader( return addPlaceholderHeader(
headers.map(h => { headers.map(h => {
const parsedHeader = { const parsedHeader: HeaderClient = {
name: h.name, name: h.name,
value: h.value, value: h.value || '',
type: 'static', type: 'static',
}; };
if (h.value_from_env) { if (h.value_from_env) {

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import styles from '../Common.scss'; import styles from '../Common.scss';
const Check = ({ className }) => { const Check = ({ className, title = '' }) => {
return ( return (
<i <i
className={`fa fa-check ${styles.iconCheck} ${className}`} className={`fa fa-check ${styles.iconCheck} ${className}`}
aria-hidden="true" aria-hidden="true"
title={title}
/> />
); );
}; };

View 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;

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import styles from '../Common.scss'; import styles from '../Common.scss';
const Cross = ({ className }) => { const Cross = ({ className, title = '' }) => {
return ( return (
<i <i
className={`fa fa-times ${styles.iconCross} ${className}`} className={`fa fa-times ${styles.iconCross} ${className}`}
aria-hidden="true" aria-hidden="true"
title={title}
/> />
); );
}; };

View 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;

View 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;

View File

@ -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;

View File

@ -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>
&nbsp;
<i
key={`${b.title}-arrow`}
className="fa fa-angle-right"
aria-hidden="true"
/>
&nbsp;
</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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;

View File

@ -150,3 +150,22 @@
.ReactTable .rt-thead [role='columnheader'] { .ReactTable .rt-thead [role='columnheader'] {
outline: 0; 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;
}

View File

@ -3,11 +3,17 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip'; import Tooltip from 'react-bootstrap/lib/Tooltip';
import styles from './Tooltip.scss'; import styles from './Tooltip.scss';
const tooltipGen = message => { const tooltipGen = (message: string) => {
return <Tooltip id={message}>{message}</Tooltip>; 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)}> <OverlayTrigger placement={placement} overlay={tooltipGen(message)}>
<i <i
className={`fa fa-question-circle + ${styles.tooltipIcon}`} className={`fa fa-question-circle + ${styles.tooltipIcon}`}

View File

@ -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 */ /* TYPE utils */
export const isNotDefined = value => { export const isNotDefined = (value: unknown) => {
return value === null || value === undefined; return value === null || value === undefined;
}; };
export const exists = value => { /*
* Deprecated: Use "isNull" instead
*/
export const exists = (value: unknown) => {
return value !== null && value !== undefined; return value !== null && value !== undefined;
}; };
export const isArray = value => { export const isArray = (value: unknown) => {
return Array.isArray(value); return Array.isArray(value);
}; };
export const isObject = value => { export const isObject = (value: unknown) => {
return typeof value === 'object' && value !== null; return typeof value === 'object' && value !== null;
}; };
export const isString = value => { export const isString = (value: unknown) => {
return typeof value === 'string'; return typeof value === 'string';
}; };
export const isNumber = value => { export const isNumber = (value: unknown) => {
return typeof value === 'number'; return typeof value === 'number';
}; };
export const isFloat = n => { export const isFloat = (value: unknown) => {
return typeof value === 'number' && n % 1 !== 0; return typeof value === 'number' && value % 1 !== 0;
}; };
export const isBoolean = value => { export const isBoolean = (value: unknown) => {
return typeof value === 'boolean'; return typeof value === 'boolean';
}; };
export const isPromise = value => { export const isPromise = (value: any) => {
if (!value) return false; if (!value) return false;
return value.constructor.name === 'Promise'; 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(); const literal = literal_.trim();
if (!literal) return false; if (!literal) return false;
const templateStartIndex = literal.indexOf('{{'); const templateStartIndex = literal.indexOf('{{');
const templateEndEdex = literal.indexOf('}}'); const templateEndEdex = literal.indexOf('}}');
return ( return templateStartIndex !== -1 && templateEndEdex > templateStartIndex + 2;
templateStartIndex !== '-1' && templateEndEdex > templateStartIndex + 2
);
}; };
export const isJsonString = str => { export const isValidDate = (date: Date) => {
try { try {
JSON.parse(str); date.toISOString();
} catch (e) { } catch {
return false; return false;
} }
return true; return true;
}; };
export const isEmpty = value => { export const isEmpty = (value: any) => {
let _isEmpty = false; let empty = false;
if (!exists(value)) { if (!exists(value)) {
_isEmpty = true; empty = true;
} else if (isArray(value)) { } else if (isArray(value)) {
_isEmpty = value.length === 0; empty = value.length === 0;
} else if (isObject(value)) { } else if (isObject(value)) {
_isEmpty = JSON.stringify(value) === JSON.stringify({}); empty = JSON.stringify(value) === JSON.stringify({});
} else if (isString(value)) { } else if (isString(value)) {
_isEmpty = value === ''; empty = value === '';
} }
return _isEmpty; return empty;
}; };
export const isEqual = (value1, value2) => { export const isEqual = (value1: any, value2: any) => {
let _isEqual = false; let equal = false;
if (typeof value1 === typeof value2) { if (typeof value1 === typeof value2) {
if (isArray(value1)) { if (isArray(value1)) {
_isEqual = JSON.stringify(value1) === JSON.stringify(value2); equal = JSON.stringify(value1) === JSON.stringify(value2);
} else if (isObject(value2)) { } else if (isObject(value2)) {
const value1Keys = Object.keys(value1); const value1Keys = Object.keys(value1);
const value2Keys = Object.keys(value2); const value2Keys = Object.keys(value2);
if (value1Keys.length === value2Keys.length) { if (value1Keys.length === value2Keys.length) {
_isEqual = true; equal = true;
for (let i = 0; i < value1Keys.length; i++) { for (let i = 0; i < value1Keys.length; i++) {
const key = value1Keys[i]; const key = value1Keys[i];
if (!isEqual(value1[key], value2[key])) { if (!isEqual(value1[key], value2[key])) {
_isEqual = false; equal = false;
break; break;
} }
} }
} }
} else { } 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 */ /* ARRAY utils */
export const deleteArrayElementAtIndex = (array: unknown[], index: number) => {
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) => {
return array.splice(index, 1); return array.splice(index, 1);
}; };
export const arrayDiff = (arr1, arr2) => { export const arrayDiff = (arr1: unknown[], arr2: unknown[]) => {
return arr1.filter(v => !arr2.includes(v)); return arr1.filter(v => !arr2.includes(v));
}; };
/* JSON utils */ /* JSON utils */
export const getAllJsonPaths = (json, leafKeys = [], prefix = '') => { export function getAllJsonPaths(json: any, leafKeys: any[], prefix = '') {
const _paths = []; const paths = [];
const addPrefix = subPath => { const addPrefix = (subPath: string) => {
return prefix + (prefix && subPath ? '.' : '') + subPath; return prefix + (prefix && subPath ? '.' : '') + subPath;
}; };
const handleSubJson = (subJson, newPrefix) => { const handleSubJson = (subJson: any, newPrefix: string) => {
const subPaths = getAllJsonPaths(subJson, leafKeys, newPrefix); const subPaths = getAllJsonPaths(subJson, leafKeys, newPrefix);
subPaths.forEach(subPath => { subPaths.forEach(subPath => {
_paths.push(subPath); paths.push(subPath);
}); });
if (!subPaths.length) { if (!subPaths.length) {
_paths.push(newPrefix); paths.push(newPrefix);
} }
}; };
if (isArray(json)) { if (isArray(json)) {
json.forEach((subJson, i) => { json.forEach((subJson: any, i: number) => {
handleSubJson(subJson, addPrefix(i.toString())); handleSubJson(subJson, addPrefix(i.toString()));
}); });
} else if (isObject(json)) { } else if (isObject(json)) {
Object.keys(json).forEach(key => { Object.keys(json).forEach(key => {
if (leafKeys.includes(key)) { if (leafKeys.includes(key)) {
_paths.push({ [addPrefix(key)]: json[key] }); paths.push({ [addPrefix(key)]: json[key] });
} else { } else {
handleSubJson(json[key], addPrefix(key)); handleSubJson(json[key], addPrefix(key));
} }
}); });
} else { } 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 s.charAt(0).toUpperCase() + s.slice(1);
}; };
// return number with commas for readability // return number with commas for readability
export const getReadableNumber = number => { export const getReadableNumber = (number: number) => {
if (!isNumber(number)) return number;
return number.toLocaleString(); return number.toLocaleString();
}; };
/* URL utils */ /* URL utils */
export const getUrlSearchParamValue = param => { export const getUrlSearchParamValue = (param: string) => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
return urlSearchParams.get(param); return urlSearchParams.get(param);
}; };
/* ALERT utils */ /* ALERT utils */
// use browser confirm and prompt to get user confirmation for actions // use browser confirm and prompt to get user confirmation for actions
@ -205,14 +208,14 @@ export const getConfirmation = (
} }
if (!hardConfirmation) { if (!hardConfirmation) {
isConfirmed = confirm(modalContent); isConfirmed = window.confirm(modalContent);
} else { } else {
modalContent += '\n\n'; modalContent += '\n\n';
modalContent += `Type "${confirmationText}" to confirm:`; modalContent += `Type "${confirmationText}" to confirm:`;
// retry prompt until user cancels or confirmation text matches // retry prompt until user cancels or confirmation text matches
// prompt returns null on cancel or a string otherwise // prompt returns null on cancel or a string otherwise
let promptResponse = ''; let promptResponse: string | null = '';
while (!isConfirmed && promptResponse !== null) { while (!isConfirmed && promptResponse !== null) {
promptResponse = prompt(modalContent); promptResponse = prompt(modalContent);
@ -226,14 +229,14 @@ export const getConfirmation = (
/* FILE utils */ /* FILE utils */
export const uploadFile = ( export const uploadFile = (
fileHandler, fileHandler: (s: string | ArrayBufferLike | null) => void,
fileFormat = null, fileFormat: string | null,
invalidFileHandler = null, invalidFileHandler: any,
errorCallback = null errorCallback?: (title: string, subTitle: string, details?: any) => void
) => { ) => {
const fileInputElement = document.createElement('div'); const fileInputElement = document.createElement('div');
fileInputElement.innerHTML = '<input style="display:none" type="file">'; fileInputElement.innerHTML = '<input style="display:none" type="file">';
const fileInput = fileInputElement.firstChild; const fileInput: any = fileInputElement.firstChild;
document.body.appendChild(fileInputElement); document.body.appendChild(fileInputElement);
const onFileUpload = () => { const onFileUpload = () => {
@ -242,21 +245,19 @@ export const uploadFile = (
let isValidFile = true; let isValidFile = true;
if (fileFormat) { if (fileFormat) {
const expectedFileSuffix = '.' + fileFormat; const expectedFileSuffix = `.${fileFormat}`;
if (!fileName.endsWith(expectedFileSuffix)) { if (!fileName.endsWith(expectedFileSuffix)) {
isValidFile = false; isValidFile = false;
if (invalidFileHandler) { if (invalidFileHandler) {
invalidFileHandler(fileName); invalidFileHandler(fileName);
} else { } else if (errorCallback) {
if (errorCallback) {
errorCallback( errorCallback(
'Invalid file format', 'Invalid file format',
`Expected a ${expectedFileSuffix} file` `Expected a ${expectedFileSuffix} file`
); );
} }
}
fileInputElement.remove(); fileInputElement.remove();
} }
@ -283,7 +284,7 @@ export const uploadFile = (
fileInput.click(); fileInput.click();
}; };
export const downloadFile = (fileName, dataString) => { export const downloadFile = (fileName: string, dataString: string) => {
const downloadLinkElem = document.createElement('a'); const downloadLinkElem = document.createElement('a');
downloadLinkElem.setAttribute('href', dataString); downloadLinkElem.setAttribute('href', dataString);
downloadLinkElem.setAttribute('download', fileName); downloadLinkElem.setAttribute('download', fileName);
@ -295,7 +296,7 @@ export const downloadFile = (fileName, dataString) => {
downloadLinkElem.remove(); downloadLinkElem.remove();
}; };
export const downloadObjectAsJsonFile = (fileName, object) => { export const downloadObjectAsJsonFile = (fileName: string, object: any) => {
const contentType = 'application/json;charset=utf-8;'; const contentType = 'application/json;charset=utf-8;';
const jsonSuffix = '.json'; const jsonSuffix = '.json';
@ -303,17 +304,16 @@ export const downloadObjectAsJsonFile = (fileName, object) => {
? fileName ? fileName
: fileName + jsonSuffix; : fileName + jsonSuffix;
const dataString = const dataString = `data:${contentType},${encodeURIComponent(
'data:' + JSON.stringify(object, null, 2)
contentType + )}`;
',' +
encodeURIComponent(JSON.stringify(object, null, 2));
downloadFile(fileNameWithSuffix, dataString); downloadFile(fileNameWithSuffix, dataString);
}; };
export const getFileExtensionFromFilename = filename => { export const getFileExtensionFromFilename = (filename: string) => {
return filename.match(/\.[0-9a-z]+$/i)[0]; const matches = filename.match(/\.[0-9a-z]+$/i);
return matches ? matches[0] : null;
}; };
// return time in format YYYY_MM_DD_hh_mm_ss_s // 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('_'); 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');
};

View File

@ -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,
// }))
// ]
// };

View 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,
// }))
// ]
// };

View 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 });

View File

@ -68,3 +68,91 @@ export const getActionsBaseRoute = () => {
export const getActionsCreateRoute = () => { export const getActionsCreateRoute = () => {
return `${getActionsBaseRoute()}/add`; 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');
};

View File

@ -1,31 +1,23 @@
interface SqlUtilsOptions { export const sqlEscapeText = (rawText: string) => {
tableName: string; let text = rawText;
schemaName: string;
constraintName: string; if (text) {
check?: string; text = text.replace(/'/g, "\\'");
selectedPkColumns?: string[];
} }
export const sqlEscapeText = (text: string) => { return `E'${text}'`;
let escapedText = text;
if (escapedText) {
escapedText = escapedText.replace(/'/g, "\\'");
}
return `E'${escapedText}'`;
}; };
// detect DDL statements in SQL // detect DDL statements in SQL
export const checkSchemaModification = (_sql: string) => { export const checkSchemaModification = (sql: string) => {
let isSchemaModification = false; let isSchemaModification = false;
const sqlStatements = _sql const sqlStatements = sql
.toLowerCase() .toLowerCase()
.split(';') .split(';')
.map(s => s.trim()); .map(s => s.trim());
sqlStatements.forEach((statement: string) => { sqlStatements.forEach(statement => {
if ( if (
statement.startsWith('create ') || statement.startsWith('create ') ||
statement.startsWith('alter ') || statement.startsWith('alter ') ||
@ -70,12 +62,12 @@ export const getCreatePkSql = ({
tableName, tableName,
selectedPkColumns, selectedPkColumns,
constraintName, constraintName,
}: SqlUtilsOptions) => { }: {
// if no primary key columns provided, return empty query schemaName: string;
if (!selectedPkColumns || selectedPkColumns.length === 0) { tableName: string;
return ''; selectedPkColumns: string[];
} constraintName: string;
}) => {
return `alter table "${schemaName}"."${tableName}" return `alter table "${schemaName}"."${tableName}"
add constraint "${constraintName}" add constraint "${constraintName}"
primary key ( ${selectedPkColumns.map(pkc => `"${pkc}"`).join(', ')} );`; primary key ( ${selectedPkColumns.map(pkc => `"${pkc}"`).join(', ')} );`;
@ -85,14 +77,17 @@ export const getDropPkSql = ({
schemaName, schemaName,
tableName, tableName,
constraintName, constraintName,
}: SqlUtilsOptions) => { }: {
schemaName: string;
tableName: string;
constraintName: string;
}) => {
return `alter table "${schemaName}"."${tableName}" drop constraint "${constraintName}";`; return `alter table "${schemaName}"."${tableName}" drop constraint "${constraintName}";`;
}; };
export const terminateSql = (sql: string) => { export const terminateSql = (sql: string) => {
const sqlTerminated = sql.trim(); const sqlSanitised = sql.trim();
return sqlSanitised[sqlSanitised.length - 1] !== ';'
return sqlTerminated[sqlTerminated.length - 1] !== ';' ? `${sqlSanitised};`
? `${sqlTerminated};` : sqlSanitised;
: sqlTerminated;
}; };

View File

@ -1,2 +1,12 @@
export const UNSAFE_keys = <T extends object>(source: T) => export const UNSAFE_keys = <T extends object>(source: T) =>
Object.keys(source) as Array<keyof 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;

View File

@ -1,7 +1,6 @@
export const getPathRoot = path => { export const getPathRoot = path => {
return path.split('/')[1]; return path.split('/')[1];
}; };
export const stripTrailingSlash = url => { export const stripTrailingSlash = url => {
if (url && url.endsWith('/')) { if (url && url.endsWith('/')) {
return url.slice(0, -1); return url.slice(0, -1);

View File

@ -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,
},
});

View 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
);

View File

@ -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);

View 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);

View File

@ -739,12 +739,20 @@ class Main extends React.Component {
tooltips.remoteSchema, tooltips.remoteSchema,
'/remote-schemas/manage/schemas' '/remote-schemas/manage/schemas'
)} )}
{getSidebarItem( {/* {getSidebarItem(
'Events', 'Events',
'fa-cloud', 'fa-cloud',
tooltips.events, tooltips.events,
'/events/manage/triggers' '/events/manage/triggers'
)} )}
{' '}
*/}{' '}
{getSidebarItem(
'Events',
'fa-cloud',
tooltips.events,
'/events/data/manage'
)}
</ul> </ul>
</div> </div>
<div id="dropdown_wrapper" className={styles.clusterInfoWrapper}> <div id="dropdown_wrapper" className={styles.clusterInfoWrapper}>

View File

@ -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;

View File

@ -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;

View File

@ -1,16 +1,63 @@
import React from 'react'; import React from 'react';
import AceEditor from 'react-ace'; 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 Button from '../../Common/Button/Button';
import { Thunk } from '../../../types';
import { Json } from '../../Common/utils/tsUtils';
import './Notification/NotificationOverrides.css'; import './Notification/NotificationOverrides.css';
import { isObject, isString } from '../../Common/utils/jsUtils'; import { isObject, isString } from '../../Common/utils/jsUtils';
const styles = require('./Notification/Notification.scss'); 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 ( return (
<div className={'notification-details'}> <div className="notification-details">
<AceEditor <AceEditor
readOnly readOnly
showPrintMargin={false} 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 = () => { const getErrorMessage = () => {
let notificationMessage; let notificationMessage;
@ -41,10 +92,9 @@ const showErrorNotification = (title, message, error) => {
error.message.error === 'query execution failed') error.message.error === 'query execution failed')
) { ) {
if (error.message.internal) { if (error.message.internal) {
notificationMessage = notificationMessage = `${error.message.code}: ${error.message.internal.error.message}`;
error.message.code + ': ' + error.message.internal.error.message;
} else { } else {
notificationMessage = error.code + ': ' + error.message.error; notificationMessage = `${error.code}: ${error.message.error}`;
} }
} else if ('info' in error) { } else if ('info' in error) {
notificationMessage = error.info; notificationMessage = error.info;
@ -58,12 +108,12 @@ const showErrorNotification = (title, message, error) => {
} else if (error.message && isString(error.message)) { } else if (error.message && isString(error.message)) {
notificationMessage = error.message; notificationMessage = error.message;
} else if (error.message && 'code' in error.message) { } else if (error.message && 'code' in error.message) {
notificationMessage = error.message.code + ' : ' + message; notificationMessage = `${error.message.code} : ${message}`;
} else { } else {
notificationMessage = error.code; notificationMessage = error.code;
} }
} else if ('internal' in error && 'error' in error.internal) { } 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) { } else if ('custom' in error) {
notificationMessage = error.custom; notificationMessage = error.custom;
} else if ('code' in error && 'error' in error && 'path' in error) { } else if ('code' in error && 'error' in error && 'path' in error) {
@ -78,9 +128,8 @@ const showErrorNotification = (title, message, error) => {
}; };
const getRefreshBtn = () => { const getRefreshBtn = () => {
let refreshBtn;
if (error && 'action' in error) { if (error && 'action' in error) {
refreshBtn = ( return (
<Button <Button
className={styles.add_mar_top_small} className={styles.add_mar_top_small}
color="yellow" color="yellow"
@ -94,8 +143,7 @@ const showErrorNotification = (title, message, error) => {
</Button> </Button>
); );
} }
return null;
return refreshBtn;
}; };
const getErrorJson = () => { const getErrorJson = () => {
@ -119,7 +167,7 @@ const showErrorNotification = (title, message, error) => {
return dispatch => { return dispatch => {
const getNotificationAction = () => { const getNotificationAction = () => {
let action = null; let action;
if (errorJson) { if (errorJson) {
const errorDetails = [ const errorDetails = [
@ -130,13 +178,15 @@ const showErrorNotification = (title, message, error) => {
label: 'Details', label: 'Details',
callback: () => { callback: () => {
dispatch( dispatch(
showNotification({ showNotification(
level: 'error', {
position: 'br', // HACK: to avoid expansion of existing notifications position: 'br',
title, title,
message: errorMessage, message: errorMessage,
children: errorDetails, children: errorDetails,
}) },
'error'
)
); );
}, },
}; };
@ -146,53 +196,68 @@ const showErrorNotification = (title, message, error) => {
}; };
dispatch( dispatch(
showNotification({ showNotification(
level: 'error', {
title, title,
message: errorMessage, message: errorMessage,
action: getNotificationAction(), action: getNotificationAction(),
}) },
'error'
)
); );
}; };
}; };
const showSuccessNotification = (title, message) => { const showSuccessNotification = (title: string, message?: string): Thunk => {
return dispatch => { return dispatch => {
dispatch( dispatch(
showNotification({ showNotification(
{
level: 'success', level: 'success',
title, title,
message: message ? message : null, message,
}) },
'success'
)
); );
}; };
}; };
const showInfoNotification = title => { const showInfoNotification = (title: string): Thunk => {
return dispatch => { return dispatch => {
dispatch( dispatch(
showNotification({ showNotification(
{
title, title,
autoDismiss: 0, autoDismiss: 0,
}) },
'info'
)
); );
}; };
}; };
const showWarningNotification = (title, message, dataObj) => { const showWarningNotification = (
const children = []; title: string,
message: string,
dataObj: Json
): Thunk => {
const children: JSX.Element[] = [];
if (dataObj) { if (dataObj) {
children.push(getNotificationDetails(dataObj)); children.push(getNotificationDetails(dataObj, null));
} }
return dispatch => { return dispatch => {
dispatch( dispatch(
showNotification({ showNotification(
{
level: 'warning', level: 'warning',
title, title,
message, message,
children, children,
}) },
'warning'
)
); );
}; };
}; };

View File

@ -463,7 +463,7 @@ class AddTable extends Component {
<div <div
className={`${styles.addTablesBody} ${styles.clear_fix} ${styles.padd_left}`} 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}> <div className={styles.subHeader}>
<h2 className={styles.heading_text}>Add a new table</h2> <h2 className={styles.heading_text}>Add a new table</h2>
<div className="clearfix" /> <div className="clearfix" />

View File

@ -0,0 +1,7 @@
import { GetReduxState } from '../../../../types';
const dataHeaders = (getState: GetReduxState) => {
return getState().tables.dataHeaders;
};
export default dataHeaders;

View File

@ -12,7 +12,7 @@ import globals from '../../../../Globals';
import returnMigrateUrl from '../Common/getMigrateUrl'; import returnMigrateUrl from '../Common/getMigrateUrl';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants'; import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants';
import { loadMigrationStatus } from '../../../Main/Actions'; import { loadMigrationStatus } from '../../../Main/Actions';
import { handleMigrationErrors } from '../../EventTrigger/EventActions'; import { handleMigrationErrors } from '../../../../utils/migration';
import { showSuccessNotification } from '../../Common/Notification'; import { showSuccessNotification } from '../../Common/Notification';

View File

@ -11,7 +11,7 @@ import dataHeaders from '../Common/Headers';
import { getConfirmation } from '../../../Common/utils/jsUtils'; import { getConfirmation } from '../../../Common/utils/jsUtils';
import { import {
getBulkDeleteQuery, getBulkDeleteQuery,
generateSelectQuery, getSelectQuery,
getFetchManualTriggersQuery, getFetchManualTriggersQuery,
getDeleteQuery, getDeleteQuery,
getRunSqlQuery, getRunSqlQuery,
@ -73,10 +73,14 @@ const vMakeRowsRequest = () => {
const requestBody = { const requestBody = {
type: 'bulk', type: 'bulk',
args: [ args: [
generateSelectQuery( getSelectQuery(
'select', 'select',
generateTableDef(originalTable, currentSchema), 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)), getRunSqlQuery(getEstimateCountQuery(currentSchema, originalTable)),
], ],
@ -124,10 +128,14 @@ const vMakeCountRequest = () => {
} = getState().tables; } = getState().tables;
const url = Endpoints.query; const url = Endpoints.query;
const requestBody = generateSelectQuery( const requestBody = getSelectQuery(
'count', 'count',
generateTableDef(originalTable, currentSchema), generateTableDef(originalTable, currentSchema),
view.query view.query.columns,
view.query.where,
view.query.offset,
view.query.limit,
view.query.order_by
); );
const options = { const options = {
@ -176,7 +184,10 @@ const vMakeTableRequests = () => (dispatch, getState) => {
const fetchManualTriggers = tableName => { const fetchManualTriggers = tableName => {
return (dispatch, getState) => { return (dispatch, getState) => {
const url = Endpoints.getSchema; const url = Endpoints.getSchema;
const body = getFetchManualTriggersQuery(tableName); const { currentSchema } = getState().tables;
const body = getFetchManualTriggersQuery(
generateTableDef(tableName, currentSchema)
);
const options = { const options = {
credentials: globalCookiePolicy, credentials: globalCookiePolicy,

View File

@ -7,7 +7,7 @@ import DragFoldTable, {
import Dropdown from '../../../Common/Dropdown/Dropdown'; import Dropdown from '../../../Common/Dropdown/Dropdown';
import InvokeManualTrigger from '../../EventTrigger/Common/InvokeManualTrigger/InvokeManualTrigger'; import InvokeManualTrigger from '../../Events/EventTriggers/InvokeManualTrigger/InvokeManualTrigger';
import { import {
vExpandRel, vExpandRel,

View File

@ -54,14 +54,12 @@ import {
getUntrackTableQuery, getUntrackTableQuery,
getTrackTableQuery, getTrackTableQuery,
} from '../../../Common/utils/v1QueryUtils'; } from '../../../Common/utils/v1QueryUtils';
import { import {
fetchColumnCastsQuery, fetchColumnCastsQuery,
convertArrayToJson, convertArrayToJson,
sanitiseRootFields, sanitiseRootFields,
sanitiseColumnNames, sanitiseColumnNames,
} from './utils'; } from './utils';
import { import {
getSchemaBaseRoute, getSchemaBaseRoute,
getTableModifyRoute, getTableModifyRoute,

View File

@ -1,18 +1,17 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import Button from '../../../Common/Button'; import Button from '../../../Common/Button';
import { isJsonString, getConfirmation } from '../../../Common/utils/jsUtils'; import { isJsonString, getConfirmation } from '../../../Common/utils/jsUtils';
import { FilterState } from './utils'; import { FilterState } from './utils';
import { showErrorNotification } from '../../Common/Notification'; import { showErrorNotification } from '../../Common/Notification';
import { permChangePermissions, permChangeTypes } from './Actions'; import { permChangePermissions, permChangeTypes } from './Actions';
import styles from '../../../Common/Permissions/PermissionStyles.scss'; import styles from '../../../Common/Permissions/PermissionStyles.scss';
import { Dispatch } from '../../../../types';
interface PermButtonSectionProps { interface PermButtonSectionProps {
readOnlyMode: string; readOnlyMode: string;
query: string; query: string;
localFilterString: FilterState; localFilterString: FilterState;
dispatch: (d: ThunkDispatch<{}, {}, AnyAction>) => void; dispatch: Dispatch;
permissionsState: FilterState; permissionsState: FilterState;
permsChanged: string; permsChanged: string;
currQueryPermissions: string; currQueryPermissions: string;

View File

@ -3,11 +3,13 @@ import styles from '../../TableModify/ModifyTable.scss';
import { RemoteRelationshipServer } from './utils'; import { RemoteRelationshipServer } from './utils';
import RemoteRelationshipList from './components/RemoteRelationshipList'; import RemoteRelationshipList from './components/RemoteRelationshipList';
import { fetchRemoteSchemas } from '../../../RemoteSchema/Actions'; import { fetchRemoteSchemas } from '../../../RemoteSchema/Actions';
import { Table } from '../../../../Common/utils/pgUtils';
import { Dispatch } from '../../../../../types';
type Props = { type Props = {
relationships: RemoteRelationshipServer[]; relationships: RemoteRelationshipServer[];
reduxDispatch: any; reduxDispatch: Dispatch;
table: any; table: Table;
remoteSchemas: string[]; remoteSchemas: string[];
}; };

View File

@ -22,9 +22,10 @@ import {
Configuration as ConfigTooltip, Configuration as ConfigTooltip,
} from '../Tooltips'; } from '../Tooltips';
import Explorer from './Explorer'; import Explorer from './Explorer';
import { Table } from '../../../../../Common/utils/pgUtils';
type Props = { type Props = {
table: any; // TODO use "Table" type after ST is merged table: Table;
remoteSchemas: string[]; remoteSchemas: string[];
isLast: boolean; isLast: boolean;
state: RemoteRelationship; state: RemoteRelationship;

View File

@ -5,13 +5,15 @@ import RemoteRelEditor from './RemoteRelEditor';
import RemoteRelCollapsedLabel from './EditorCollapsed'; import RemoteRelCollapsedLabel from './EditorCollapsed';
import { useRemoteRelationship } from '../state'; import { useRemoteRelationship } from '../state';
import { saveRemoteRelationship, dropRemoteRelationship } from '../../Actions'; import { saveRemoteRelationship, dropRemoteRelationship } from '../../Actions';
import { Table } from '../../../../../Common/utils/pgUtils';
import { Dispatch } from '../../../../../../types';
type Props = { type Props = {
relationship?: RemoteRelationshipServer; relationship?: RemoteRelationshipServer;
table: any; table: Table;
isLast: boolean; isLast: boolean;
remoteSchemas: string[]; remoteSchemas: string[];
reduxDispatch: any; // TODO use Dispatch after ST is merged reduxDispatch: Dispatch;
}; };
const EditorWrapper: React.FC<Props> = ({ const EditorWrapper: React.FC<Props> = ({

View File

@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { RemoteRelationshipServer } from '../utils'; import { RemoteRelationshipServer } from '../utils';
import RemoteRelationshipEditor from './RemoteRelEditorWrapper'; import RemoteRelationshipEditor from './RemoteRelEditorWrapper';
import { Table } from '../../../../../Common/utils/pgUtils';
import { Dispatch } from '../../../../../../types';
type Props = { type Props = {
relationships: RemoteRelationshipServer[]; relationships: RemoteRelationshipServer[];
table: any; table: Table;
remoteSchemas: string[]; remoteSchemas: string[];
reduxDispatch: any; // TODO use Dispatch after ST is merged reduxDispatch: Dispatch;
}; };
const RemoteRelationshipList: React.FC<Props> = ({ const RemoteRelationshipList: React.FC<Props> = ({

View File

@ -13,8 +13,9 @@ import {
TreeArgElement, TreeArgElement,
ArgValueKind, ArgValueKind,
} from './utils'; } from './utils';
import { Table } from '../../../../Common/utils/pgUtils';
const getDefaultState = (table: any): RemoteRelationship => ({ const getDefaultState = (table: Table): RemoteRelationship => ({
name: '', name: '',
remoteSchema: '', remoteSchema: '',
remoteFields: [], remoteFields: [],
@ -224,7 +225,7 @@ const reducer = (
// type "table" once ST PR is merged // type "table" once ST PR is merged
export const useRemoteRelationship = ( export const useRemoteRelationship = (
table: any, table: Table,
relationship?: RemoteRelationshipServer relationship?: RemoteRelationshipServer
) => { ) => {
const [state, dispatch] = React.useReducer( const [state, dispatch] = React.useReducer(

View File

@ -17,6 +17,7 @@ import {
isNumber, isNumber,
} from '../../../../Common/utils/jsUtils'; } from '../../../../Common/utils/jsUtils';
import { getUnderlyingType } from '../../../../../shared/utils/graphqlSchemaUtils'; import { getUnderlyingType } from '../../../../../shared/utils/graphqlSchemaUtils';
import { TableDefinition } from '../../../../Common/utils/v1QueryUtils';
export type ArgValueKind = 'column' | 'static'; export type ArgValueKind = 'column' | 'static';
export type ArgValue = { 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 hasuraFields: string[] = [];
const getRemoteFieldArguments = (field: RemoteField) => { const getRemoteFieldArguments = (field: RemoteField) => {
const getArgumentObject = (depth: number, parent?: string) => { const getArgumentObject = (depth: number, parent?: string) => {
@ -325,7 +336,7 @@ export const getRemoteRelPayload = (relationship: RemoteRelationship) => {
return { return {
name: relationship.name, name: relationship.name,
remote_schema: relationship.remoteSchema, remote_schema: relationship.remoteSchema,
remote_field: getRemoteFieldObject(0), remote_field: getRemoteFieldObject(0) || {},
hasura_fields: hasuraFields hasura_fields: hasuraFields
.map(f => f.substr(1)) .map(f => f.substr(1))
.filter((v, i, s) => s.indexOf(v) === i), .filter((v, i, s) => s.indexOf(v) === i),

View File

@ -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 };

View File

@ -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;

View File

@ -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 &nbsp; &nbsp;
<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 &nbsp; &nbsp;
<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 &nbsp; &nbsp;
<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 &nbsp; &nbsp;
<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;

View File

@ -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 &nbsp;&nbsp;
<OverlayTrigger
placement="right"
overlay={tooltip.manualOperationsDescription}
>
<i className="fa fa-question-circle" aria-hidden="true" />
</OverlayTrigger>
&nbsp;&nbsp;
<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 &nbsp; &nbsp;
<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;

View File

@ -1,4 +0,0 @@
const dataHeaders = currentState => {
return currentState().tables.dataHeaders;
};
export default dataHeaders;

View File

@ -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>
);

View File

@ -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,
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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:&nbsp;
</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:&nbsp;
</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;

View File

@ -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: &nbsp;
</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):&nbsp;
</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):&nbsp;
</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;

View File

@ -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;

View File

@ -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}
&nbsp;
</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