mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 15:09:02 +03:00
console: add scheduled triggers support (#4732)
This commit is contained in:
parent
ae75c6c06e
commit
aaab6d3eb6
@ -82,6 +82,7 @@
|
|||||||
"jsx-a11y/no-autofocus": 0,
|
"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",
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
78
console/package-lock.json
generated
78
console/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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`,
|
@ -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;
|
||||||
|
BIN
console/src/action-diagram.png
Normal file
BIN
console/src/action-diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -1,5 +1,4 @@
|
|||||||
import defaultState from './State';
|
import 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,
|
|
||||||
};
|
};
|
||||||
|
@ -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';
|
||||||
|
@ -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));
|
||||||
|
@ -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={{
|
@ -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';
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,6 +478,10 @@ input {
|
|||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addPadding20Px {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.width_auto {
|
.width_auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import InputGroup from 'react-bootstrap/lib/InputGroup';
|
|
||||||
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
|
||||||
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
|
||||||
|
|
||||||
class DropButton extends React.Component {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
dropdownOptions,
|
|
||||||
value,
|
|
||||||
required,
|
|
||||||
onInputChange,
|
|
||||||
onButtonChange,
|
|
||||||
dataKey,
|
|
||||||
dataIndex,
|
|
||||||
bsClass,
|
|
||||||
disabled,
|
|
||||||
inputVal,
|
|
||||||
inputPlaceHolder,
|
|
||||||
id,
|
|
||||||
testId,
|
|
||||||
} = this.props;
|
|
||||||
return (
|
|
||||||
<InputGroup className={bsClass}>
|
|
||||||
<DropdownButton
|
|
||||||
title={value || title}
|
|
||||||
componentClass={InputGroup.Button}
|
|
||||||
disabled={disabled}
|
|
||||||
id={id}
|
|
||||||
data-test={testId + '-' + 'dropdown-button'}
|
|
||||||
>
|
|
||||||
{dropdownOptions.map((d, i) => (
|
|
||||||
<MenuItem
|
|
||||||
data-index-id={dataIndex}
|
|
||||||
value={d.value}
|
|
||||||
onClick={onButtonChange}
|
|
||||||
eventKey={i + 1}
|
|
||||||
key={i}
|
|
||||||
data-test={testId + '-' + 'dropdown-item' + '-' + (i + 1)}
|
|
||||||
>
|
|
||||||
{d.display_text}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownButton>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
data-key={dataKey}
|
|
||||||
data-index-id={dataIndex}
|
|
||||||
className={'form-control'}
|
|
||||||
required={required}
|
|
||||||
onChange={onInputChange}
|
|
||||||
disabled={disabled}
|
|
||||||
value={inputVal || ''}
|
|
||||||
placeholder={inputPlaceHolder}
|
|
||||||
data-test={testId + '-' + 'input'}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DropButton.propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
dropdownOptions: PropTypes.array.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
dataKey: PropTypes.string.isRequired,
|
|
||||||
dataIndex: PropTypes.string.isRequired,
|
|
||||||
inputVal: PropTypes.string.isRequired,
|
|
||||||
inputPlaceHolder: PropTypes.string,
|
|
||||||
required: PropTypes.bool.isRequired,
|
|
||||||
onButtonChange: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
bsClass: PropTypes.string,
|
|
||||||
id: PropTypes.string,
|
|
||||||
testId: PropTypes.string,
|
|
||||||
disabled: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropButton;
|
|
@ -0,0 +1,81 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import InputGroup from 'react-bootstrap/lib/InputGroup';
|
||||||
|
import DropdownButton from 'react-bootstrap/lib/DropdownButton';
|
||||||
|
import MenuItem from 'react-bootstrap/lib/MenuItem';
|
||||||
|
|
||||||
|
type DropDownButtonProps = {
|
||||||
|
title: string;
|
||||||
|
dropdownOptions: {
|
||||||
|
display_text: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
dataKey: string;
|
||||||
|
dataIndex?: string;
|
||||||
|
onButtonChange: (e: React.MouseEvent) => void;
|
||||||
|
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
value?: string;
|
||||||
|
inputVal: string;
|
||||||
|
required: boolean;
|
||||||
|
id: string;
|
||||||
|
testId: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
bsClass: string;
|
||||||
|
inputPlaceHolder: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DDButton: React.FC<DropDownButtonProps> = props => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
dropdownOptions,
|
||||||
|
value,
|
||||||
|
required,
|
||||||
|
onInputChange,
|
||||||
|
onButtonChange,
|
||||||
|
dataKey,
|
||||||
|
dataIndex,
|
||||||
|
bsClass,
|
||||||
|
disabled,
|
||||||
|
inputVal,
|
||||||
|
inputPlaceHolder,
|
||||||
|
id,
|
||||||
|
testId,
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<InputGroup className={bsClass}>
|
||||||
|
<DropdownButton
|
||||||
|
title={value || title}
|
||||||
|
componentClass={InputGroup.Button}
|
||||||
|
disabled={disabled}
|
||||||
|
id={id}
|
||||||
|
data-test={`${testId}-dropdown-button`}
|
||||||
|
>
|
||||||
|
{dropdownOptions.map((d, i) => (
|
||||||
|
<MenuItem
|
||||||
|
data-index-id={dataIndex}
|
||||||
|
value={d.value}
|
||||||
|
onClick={onButtonChange}
|
||||||
|
eventKey={i + 1}
|
||||||
|
key={i}
|
||||||
|
data-test={`${testId}-dropdown-item-${i + 1}`}
|
||||||
|
>
|
||||||
|
{d.display_text}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownButton>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-key={dataKey}
|
||||||
|
data-index-id={dataIndex}
|
||||||
|
className="form-control"
|
||||||
|
required={required}
|
||||||
|
onChange={onInputChange}
|
||||||
|
disabled={disabled}
|
||||||
|
value={inputVal || ''}
|
||||||
|
placeholder={inputPlaceHolder}
|
||||||
|
data-test={`${testId}-input`}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DDButton;
|
95
console/src/components/Common/FilterQuery/FilterQuery.tsx
Normal file
95
console/src/components/Common/FilterQuery/FilterQuery.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { OrderBy } from '../utils/v1QueryUtils';
|
||||||
|
import { BaseTable, generateTableDef } from '../utils/pgUtils';
|
||||||
|
import Where from './Where';
|
||||||
|
import Sorts from './Sorts';
|
||||||
|
import { useFilterQuery } from './state';
|
||||||
|
import { Filter, FilterRenderProp } from './types';
|
||||||
|
import { Dispatch } from '../../../types';
|
||||||
|
import ReloadEnumValuesButton from '../../Services/Data/Common/Components/ReloadEnumValuesButton';
|
||||||
|
import Button from '../Button/Button';
|
||||||
|
import { Nullable } from '../utils/tsUtils';
|
||||||
|
import styles from './FilterQuery.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
table: BaseTable;
|
||||||
|
relationships: Nullable<string[]>; // TODO better
|
||||||
|
render: FilterRenderProp;
|
||||||
|
dispatch: Dispatch;
|
||||||
|
presets: {
|
||||||
|
filters: Filter[];
|
||||||
|
sorts: OrderBy[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Where clause and sorts builder
|
||||||
|
* Accepts a render prop to render the results of filter/sort query
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FilterQuery: React.FC<Props> = props => {
|
||||||
|
const { table, dispatch, presets, render, relationships } = props;
|
||||||
|
|
||||||
|
const { rows, count, runQuery, state, setState } = useFilterQuery(
|
||||||
|
generateTableDef(table.table_name, table.table_schema),
|
||||||
|
dispatch,
|
||||||
|
presets,
|
||||||
|
relationships
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.add_mar_top}>
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
runQuery();
|
||||||
|
}}
|
||||||
|
className={styles.add_mar_bottom}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`${styles.queryBox} col-xs-6 ${styles.padd_left_remove}`}
|
||||||
|
>
|
||||||
|
<span className={styles.subheading_text}>Filter</span>
|
||||||
|
<Where
|
||||||
|
filters={state.filters}
|
||||||
|
setFilters={setState.filters}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.queryBox} col-xs-6 ${styles.padd_left_remove}`}
|
||||||
|
>
|
||||||
|
<b className={styles.subheading_text}>Sort</b>
|
||||||
|
<Sorts
|
||||||
|
sorts={state.sorts}
|
||||||
|
setSorts={setState.sorts}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.padd_right} ${styles.clear_fix}`}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="yellow"
|
||||||
|
size="sm"
|
||||||
|
data-test="run-query"
|
||||||
|
className={styles.add_mar_right}
|
||||||
|
>
|
||||||
|
Run query
|
||||||
|
</Button>
|
||||||
|
<ReloadEnumValuesButton
|
||||||
|
dispatch={dispatch}
|
||||||
|
isEnum={table.is_enum}
|
||||||
|
tooltipStyle={styles.add_mar_left_mid}
|
||||||
|
/>
|
||||||
|
{/* <div className={styles.count + ' alert alert-info'}><i>Total <b>{tableName}</b> rows in the database for current query: {count} </i></div> */}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/* TODO: Handle loading state */}
|
||||||
|
{render(rows, count, state, setState, runQuery)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterQuery;
|
96
console/src/components/Common/FilterQuery/Sorts.tsx
Normal file
96
console/src/components/Common/FilterQuery/Sorts.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { OrderBy } from '../utils/v1QueryUtils';
|
||||||
|
|
||||||
|
import { BaseTable } from '../utils/pgUtils';
|
||||||
|
import styles from './FilterQuery.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sorts: OrderBy[];
|
||||||
|
setSorts: (o: OrderBy[]) => void;
|
||||||
|
table: BaseTable;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Sorts: React.FC<Props> = props => {
|
||||||
|
const { sorts, setSorts, table } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{sorts.map((sort, i) => {
|
||||||
|
const removeSort = () => {
|
||||||
|
setSorts([...sorts.slice(0, i), ...sorts.slice(i + 1)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setColumn = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const col = e.target.value;
|
||||||
|
setSorts([
|
||||||
|
...sorts.slice(0, i),
|
||||||
|
{ ...sorts[i], column: col },
|
||||||
|
...sorts.slice(i + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setType = (e: React.BaseSyntheticEvent) => {
|
||||||
|
const type = e.target.value;
|
||||||
|
setSorts([
|
||||||
|
...sorts.slice(0, i),
|
||||||
|
{ ...sorts[i], type },
|
||||||
|
...sorts.slice(i + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i} // eslint-disable-line react/no-array-index-key
|
||||||
|
className={`${styles.inputRow} row`}
|
||||||
|
>
|
||||||
|
<div className="col-xs-4">
|
||||||
|
<select
|
||||||
|
className="form-control"
|
||||||
|
onChange={setColumn}
|
||||||
|
value={sort.column}
|
||||||
|
data-test={`filter-column-${i}`}
|
||||||
|
>
|
||||||
|
{sort.column === '' ? (
|
||||||
|
<option disabled value="">
|
||||||
|
-- column --
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
{table.columns.map(c => (
|
||||||
|
<option key={c.column_name} value={c.column_name}>
|
||||||
|
{c.column_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-xs-3">
|
||||||
|
<select
|
||||||
|
className="form-control"
|
||||||
|
onChange={setType}
|
||||||
|
value={sort.type}
|
||||||
|
data-test={`filter-op-${i}`}
|
||||||
|
>
|
||||||
|
<option key="asc" value="asc">
|
||||||
|
asc
|
||||||
|
</option>
|
||||||
|
<option key="desc" value="desc">
|
||||||
|
desc
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="text-center col-xs-1">
|
||||||
|
{sorts.length === i + 1 ? null : (
|
||||||
|
<i
|
||||||
|
className="fa fa-times"
|
||||||
|
onClick={removeSort}
|
||||||
|
data-test={`clear-filter-${i}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sorts;
|
121
console/src/components/Common/FilterQuery/Where.tsx
Normal file
121
console/src/components/Common/FilterQuery/Where.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ValueFilter, Operator } from './types';
|
||||||
|
import { allOperators } from './utils';
|
||||||
|
|
||||||
|
import { BaseTable } from '../utils/pgUtils';
|
||||||
|
import { isNotDefined } from '../utils/jsUtils';
|
||||||
|
import styles from './FilterQuery.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: ValueFilter[];
|
||||||
|
setFilters: (f: ValueFilter[]) => void;
|
||||||
|
table: BaseTable;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Where: React.FC<Props> = props => {
|
||||||
|
const { filters, setFilters, table } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{filters.map((filter, i) => {
|
||||||
|
const removeFilter = () => {
|
||||||
|
setFilters([...filters.slice(0, i), ...filters.slice(i + 1)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setKey = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const col = e.target.value;
|
||||||
|
setFilters([
|
||||||
|
...filters.slice(0, i),
|
||||||
|
{ ...filters[i], key: col },
|
||||||
|
...filters.slice(i + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOperator = (e: React.BaseSyntheticEvent) => {
|
||||||
|
// TODO synthetic event with enums
|
||||||
|
const op: Operator = e.target.value;
|
||||||
|
setFilters([
|
||||||
|
...filters.slice(0, i),
|
||||||
|
{ ...filters[i], operator: op, value: '' },
|
||||||
|
...filters.slice(i + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setFilters([
|
||||||
|
...filters.slice(0, i),
|
||||||
|
{ ...filters[i], value },
|
||||||
|
...filters.slice(i + 1),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i} // eslint-disable-line react/no-array-index-key
|
||||||
|
className={`${styles.inputRow} row`}
|
||||||
|
>
|
||||||
|
<div className="col-xs-4">
|
||||||
|
<select
|
||||||
|
className="form-control"
|
||||||
|
onChange={setKey}
|
||||||
|
value={filter.key}
|
||||||
|
data-test={`filter-column-${i}`}
|
||||||
|
>
|
||||||
|
{filter.key === '' ? (
|
||||||
|
<option disabled value="">
|
||||||
|
-- column --
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
{table.columns.map(c => (
|
||||||
|
<option key={c.column_name} value={c.column_name}>
|
||||||
|
{c.column_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-xs-3">
|
||||||
|
<select
|
||||||
|
className="form-control"
|
||||||
|
onChange={setOperator}
|
||||||
|
value={filter.operator || ''}
|
||||||
|
data-test={`filter-op-${i}`}
|
||||||
|
>
|
||||||
|
{isNotDefined(filter.operator) ? (
|
||||||
|
<option disabled value="">
|
||||||
|
-- op --
|
||||||
|
</option>
|
||||||
|
) : null}
|
||||||
|
{allOperators.map(o => (
|
||||||
|
<option key={o.operator} value={o.operator}>
|
||||||
|
{`[${o.alias}] ${o.name}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-xs-4">
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
placeholder="-- value --"
|
||||||
|
value={filter.value}
|
||||||
|
onChange={setValue}
|
||||||
|
data-test={`filter-value-${i}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center col-xs-1">
|
||||||
|
{filters.length === i + 1 ? null : (
|
||||||
|
<i
|
||||||
|
className="fa fa-times"
|
||||||
|
onClick={removeFilter}
|
||||||
|
data-test={`clear-filter-${i}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Where;
|
178
console/src/components/Common/FilterQuery/state.ts
Normal file
178
console/src/components/Common/FilterQuery/state.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
TableDefinition,
|
||||||
|
getSelectQuery,
|
||||||
|
OrderBy,
|
||||||
|
makeOrderBy,
|
||||||
|
} from '../utils/v1QueryUtils';
|
||||||
|
import requestAction from '../../../utils/requestAction';
|
||||||
|
import { Dispatch } from '../../../types';
|
||||||
|
import endpoints from '../../../Endpoints';
|
||||||
|
import {
|
||||||
|
makeFilterState,
|
||||||
|
SetFilterState,
|
||||||
|
ValueFilter,
|
||||||
|
makeValueFilter,
|
||||||
|
Filter,
|
||||||
|
RunQuery,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import { Nullable } from '../utils/tsUtils';
|
||||||
|
import { isNotDefined } from '../utils/jsUtils';
|
||||||
|
import { parseFilter } from './utils';
|
||||||
|
|
||||||
|
const defaultFilter = makeValueFilter('', null, '');
|
||||||
|
const defaultSort = makeOrderBy('', 'asc');
|
||||||
|
|
||||||
|
const defaultState = makeFilterState([defaultFilter], [defaultSort], 10, 0);
|
||||||
|
|
||||||
|
export const useFilterQuery = (
|
||||||
|
table: TableDefinition,
|
||||||
|
dispatch: Dispatch,
|
||||||
|
presets: {
|
||||||
|
filters: Filter[];
|
||||||
|
sorts: OrderBy[];
|
||||||
|
},
|
||||||
|
relationships: Nullable<string[]>
|
||||||
|
) => {
|
||||||
|
const [state, setState] = React.useState(defaultState);
|
||||||
|
const [rows, setRows] = React.useState<any[]>([]);
|
||||||
|
const [count, setCount] = React.useState<number>();
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState(false);
|
||||||
|
|
||||||
|
const runQuery: RunQuery = (runQueryOpts = {}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
|
||||||
|
const { offset, limit, sorts: newSorts } = runQueryOpts;
|
||||||
|
|
||||||
|
const where = {
|
||||||
|
$and: [...state.filters, ...presets.filters]
|
||||||
|
.filter(f => !!f.key && !!f.value)
|
||||||
|
.map(f => parseFilter(f)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderBy = newSorts || [
|
||||||
|
...state.sorts.filter(f => !!f.column),
|
||||||
|
...presets.sorts,
|
||||||
|
];
|
||||||
|
|
||||||
|
const query = getSelectQuery(
|
||||||
|
'select',
|
||||||
|
table,
|
||||||
|
['*', ...(relationships || []).map(r => ({ name: r, columns: ['*'] }))],
|
||||||
|
where,
|
||||||
|
isNotDefined(offset) ? state.offset : offset,
|
||||||
|
isNotDefined(limit) ? state.limit : limit,
|
||||||
|
orderBy
|
||||||
|
);
|
||||||
|
const countQuery = getSelectQuery(
|
||||||
|
'count',
|
||||||
|
table,
|
||||||
|
['*', ...(relationships || []).map(r => ({ name: r, columns: ['*'] }))],
|
||||||
|
where,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
orderBy
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(query),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
requestAction(endpoints.query, options, undefined, undefined, true, true)
|
||||||
|
).then(
|
||||||
|
(data: any[]) => {
|
||||||
|
setRows(data);
|
||||||
|
setLoading(false);
|
||||||
|
if (offset !== undefined) {
|
||||||
|
setState(s => ({ ...s, offset }));
|
||||||
|
}
|
||||||
|
if (limit !== undefined) {
|
||||||
|
setState(s => ({ ...s, limit }));
|
||||||
|
}
|
||||||
|
if (newSorts) {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
sorts: newSorts,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
requestAction(
|
||||||
|
endpoints.query,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(countQuery),
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
).then((countData: { count: number }) => {
|
||||||
|
setCount(countData.count);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
setError(true);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
runQuery();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setter: SetFilterState = {
|
||||||
|
sorts: (sorts: OrderBy[]) => {
|
||||||
|
const newSorts = [...sorts];
|
||||||
|
if (!sorts.length || sorts[sorts.length - 1].column) {
|
||||||
|
newSorts.push(defaultSort);
|
||||||
|
}
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
sorts: newSorts,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
filters: (filters: ValueFilter[]) => {
|
||||||
|
const newFilters = [...filters];
|
||||||
|
if (
|
||||||
|
!filters.length ||
|
||||||
|
filters[filters.length - 1].value ||
|
||||||
|
filters[filters.length - 1].key
|
||||||
|
) {
|
||||||
|
newFilters.push(defaultFilter);
|
||||||
|
}
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
filters: newFilters,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
offset: (o: number) => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
offset: o,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
limit: (l: number) => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
limit: l,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
runQuery,
|
||||||
|
state,
|
||||||
|
count,
|
||||||
|
setState: setter,
|
||||||
|
};
|
||||||
|
};
|
142
console/src/components/Common/FilterQuery/types.ts
Normal file
142
console/src/components/Common/FilterQuery/types.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { OrderBy } from '../utils/v1QueryUtils';
|
||||||
|
import { Nullable } from '../utils/tsUtils';
|
||||||
|
|
||||||
|
// Types for sorts
|
||||||
|
export type SetSorts = (s: OrderBy[]) => void;
|
||||||
|
|
||||||
|
// All supported postgres operators
|
||||||
|
export type Operator =
|
||||||
|
| '$eq'
|
||||||
|
| '$ne'
|
||||||
|
| '$in'
|
||||||
|
| '$nin'
|
||||||
|
| '$gt'
|
||||||
|
| '$lt'
|
||||||
|
| '$gte'
|
||||||
|
| '$lte'
|
||||||
|
| '$like'
|
||||||
|
| '$nlike'
|
||||||
|
| '$ilike'
|
||||||
|
| '$nilike'
|
||||||
|
| '$similar'
|
||||||
|
| '$nsimilar';
|
||||||
|
|
||||||
|
// Operator with names and aliases
|
||||||
|
export type OperatorDef = {
|
||||||
|
alias: string;
|
||||||
|
operator: Operator;
|
||||||
|
name: string;
|
||||||
|
default?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Value filter. Eg: { name: { $eq: "jondoe" } }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ValueFilter = {
|
||||||
|
kind: 'value';
|
||||||
|
key: string;
|
||||||
|
operator: Nullable<Operator>;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Constructor for value filter
|
||||||
|
*/
|
||||||
|
export const makeValueFilter = (
|
||||||
|
key: string,
|
||||||
|
operator: Nullable<Operator>,
|
||||||
|
value: string
|
||||||
|
): ValueFilter => ({ kind: 'value', key, operator, value });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Relationship filter filter. Eg: { user { name: { $eq: "jondoe" } } }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type RelationshipFilter = {
|
||||||
|
kind: 'relationship';
|
||||||
|
key: string;
|
||||||
|
value: Filter;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Constructor for relationship filter
|
||||||
|
*/
|
||||||
|
export const makeRelationshipFilter = (
|
||||||
|
key: string,
|
||||||
|
value: Filter
|
||||||
|
): RelationshipFilter => ({ kind: 'relationship', key, value });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Filter with logical gates
|
||||||
|
* Eg: { $and: [ { title: { $eq: "My Title" } }, { author: { name: { $eq: "jon" }}} ]}
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LogicGate = '$or' | '$and' | '$not';
|
||||||
|
export type OperatorFilter = {
|
||||||
|
kind: 'operator';
|
||||||
|
key: LogicGate;
|
||||||
|
value: Filter[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Constructor for operation filter
|
||||||
|
*/
|
||||||
|
export const makeOperationFilter = (
|
||||||
|
key: LogicGate,
|
||||||
|
value: Filter[]
|
||||||
|
): OperatorFilter => ({ kind: 'operator', key, value });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Filter for building the where clause
|
||||||
|
* Filter could be a value filter, relationship filter, or a combination of filters
|
||||||
|
*/
|
||||||
|
export type Filter = ValueFilter | RelationshipFilter | OperatorFilter;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setter function for Filters
|
||||||
|
*/
|
||||||
|
export type SetValueFilters = (s: ValueFilter[]) => void;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Local state for the filter query component
|
||||||
|
*/
|
||||||
|
export type FilterState = {
|
||||||
|
filters: ValueFilter[];
|
||||||
|
sorts: OrderBy[];
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Constructor for FilterState
|
||||||
|
*/
|
||||||
|
export const makeFilterState = (
|
||||||
|
filters: ValueFilter[],
|
||||||
|
sorts: OrderBy[],
|
||||||
|
limit: number,
|
||||||
|
offset: number
|
||||||
|
): FilterState => ({ filters, sorts, limit, offset });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Local state setter for the filter query component
|
||||||
|
*/
|
||||||
|
export type SetFilterState = {
|
||||||
|
sorts: SetSorts;
|
||||||
|
filters: SetValueFilters;
|
||||||
|
offset: (o: number) => void;
|
||||||
|
limit: (l: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunQueryOptions = {
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
sorts?: OrderBy[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunQuery = (options?: RunQueryOptions) => void;
|
||||||
|
|
||||||
|
export type FilterRenderProp = (
|
||||||
|
rows: any[],
|
||||||
|
count: number | undefined,
|
||||||
|
state: FilterState,
|
||||||
|
setState: SetFilterState,
|
||||||
|
runQuery: RunQuery
|
||||||
|
) => React.ReactNode;
|
66
console/src/components/Common/FilterQuery/utils.ts
Normal file
66
console/src/components/Common/FilterQuery/utils.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Operator, OperatorDef, Filter } from './types';
|
||||||
|
|
||||||
|
export const allOperators: OperatorDef[] = [
|
||||||
|
{ name: 'equals', operator: '$eq', alias: '_eq' },
|
||||||
|
{ name: 'not equals', operator: '$ne', alias: '_neq' },
|
||||||
|
{ name: 'in', operator: '$in', alias: '_in', default: '[]' },
|
||||||
|
{ name: 'not in', operator: '$nin', alias: '_nin', default: '[]' },
|
||||||
|
{ name: '>', operator: '$gt', alias: '_gt' },
|
||||||
|
{ name: '<', operator: '$lt', alias: '_lt' },
|
||||||
|
{ name: '>=', operator: '$gte', alias: '_gte' },
|
||||||
|
{ name: '<=', operator: '$lte', alias: '_lte' },
|
||||||
|
{ name: 'like', operator: '$like', alias: '_like', default: '%%' },
|
||||||
|
{
|
||||||
|
name: 'not like',
|
||||||
|
operator: '$nlike',
|
||||||
|
alias: '_nlike',
|
||||||
|
default: '%%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'like (case-insensitive)',
|
||||||
|
operator: '$ilike',
|
||||||
|
alias: '_ilike',
|
||||||
|
default: '%%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'not like (case-insensitive)',
|
||||||
|
operator: '$nilike',
|
||||||
|
alias: '_nilike',
|
||||||
|
default: '%%',
|
||||||
|
},
|
||||||
|
{ name: 'similar', operator: '$similar', alias: '_similar' },
|
||||||
|
{ name: 'not similar', operator: '$nsimilar', alias: '_nsimilar' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getOperatorDefaultValue = (op: Operator) => {
|
||||||
|
const operator = allOperators.find(o => o.operator === op);
|
||||||
|
return operator ? operator.default : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFilter = (f: Filter): any => {
|
||||||
|
switch (f.kind) {
|
||||||
|
case 'value':
|
||||||
|
return f.operator
|
||||||
|
? {
|
||||||
|
[f.key]: {
|
||||||
|
[f.operator]: f.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'relationship':
|
||||||
|
return {
|
||||||
|
[f.key]: parseFilter(f.value),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'operator':
|
||||||
|
return {
|
||||||
|
[f.key]: f.value.map(opFilter => parseFilter(opFilter)),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return parseFilter(f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
@ -1,91 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styles from './Headers.scss';
|
|
||||||
import DropdownButton from '../DropdownButton/DropdownButton';
|
|
||||||
import { addPlaceholderHeader } from './utils';
|
|
||||||
|
|
||||||
const Headers = ({ headers, setHeaders }) => {
|
|
||||||
return headers.map(({ name, value, type }, i) => {
|
|
||||||
const setHeaderType = e => {
|
|
||||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
|
||||||
newHeaders[i].type = e.target.getAttribute('value');
|
|
||||||
addPlaceholderHeader(newHeaders);
|
|
||||||
setHeaders(newHeaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHeaderKey = e => {
|
|
||||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
|
||||||
newHeaders[i].name = e.target.value;
|
|
||||||
addPlaceholderHeader(newHeaders);
|
|
||||||
setHeaders(newHeaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setHeaderValue = e => {
|
|
||||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
|
||||||
newHeaders[i].value = e.target.value;
|
|
||||||
addPlaceholderHeader(newHeaders);
|
|
||||||
setHeaders(newHeaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeHeader = () => {
|
|
||||||
const newHeaders = JSON.parse(JSON.stringify(headers));
|
|
||||||
setHeaders([...newHeaders.slice(0, i), ...newHeaders.slice(i + 1)]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeaderNameInput = () => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
value={name}
|
|
||||||
onChange={setHeaderKey}
|
|
||||||
placeholder="key"
|
|
||||||
className={`form-control ${styles.add_mar_right} ${styles.headerInputWidth}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHeaderValueInput = () => {
|
|
||||||
return (
|
|
||||||
<div className={styles.headerInputWidth}>
|
|
||||||
<DropdownButton
|
|
||||||
dropdownOptions={[
|
|
||||||
{ display_text: 'Value', value: 'static' },
|
|
||||||
{ display_text: 'From env var', value: 'env' },
|
|
||||||
]}
|
|
||||||
title={type === 'env' ? 'From env var' : 'Value'}
|
|
||||||
dataKey={type === 'env' ? 'env' : 'static'}
|
|
||||||
onButtonChange={setHeaderType}
|
|
||||||
onInputChange={setHeaderValue}
|
|
||||||
required
|
|
||||||
bsClass={styles.dropdown_button}
|
|
||||||
inputVal={value}
|
|
||||||
id={`header-value-${i}`}
|
|
||||||
inputPlaceHolder={type === 'env' ? 'HEADER_FROM_ENV' : 'value'}
|
|
||||||
testId={`header-value-${i}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRemoveButton = () => {
|
|
||||||
if (i === headers.length - 1) return null;
|
|
||||||
return (
|
|
||||||
<i
|
|
||||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
|
||||||
onClick={removeHeader}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
{getHeaderNameInput()}
|
|
||||||
{getHeaderValueInput()}
|
|
||||||
{getRemoveButton()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Headers;
|
|
95
console/src/components/Common/Headers/Headers.tsx
Normal file
95
console/src/components/Common/Headers/Headers.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './Headers.scss';
|
||||||
|
import DropdownButton from '../DropdownButton/DropdownButton';
|
||||||
|
import { addPlaceholderHeader } from './utils';
|
||||||
|
|
||||||
|
export type Header = {
|
||||||
|
type: 'static' | 'env';
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultHeader: Header = {
|
||||||
|
name: '',
|
||||||
|
type: 'static',
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HeadersListProps extends React.ComponentProps<'div'> {
|
||||||
|
headers: Header[];
|
||||||
|
setHeaders: (h: Header[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Headers: React.FC<HeadersListProps> = ({ headers, setHeaders }) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{headers.map(({ name, value, type }, i) => {
|
||||||
|
const setHeaderType = (e: React.BaseSyntheticEvent) => {
|
||||||
|
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||||
|
newHeaders[i].type = e.target.getAttribute('value');
|
||||||
|
addPlaceholderHeader(newHeaders);
|
||||||
|
setHeaders(newHeaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHeaderKey = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||||
|
newHeaders[i].name = e.target.value;
|
||||||
|
addPlaceholderHeader(newHeaders);
|
||||||
|
setHeaders(newHeaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHeaderValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||||
|
newHeaders[i].value = e.target.value;
|
||||||
|
addPlaceholderHeader(newHeaders);
|
||||||
|
setHeaders(newHeaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHeader = () => {
|
||||||
|
const newHeaders = JSON.parse(JSON.stringify(headers));
|
||||||
|
setHeaders([...newHeaders.slice(0, i), ...newHeaders.slice(i + 1)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.display_flex} ${styles.add_mar_bottom_mid}`}
|
||||||
|
key={i.toString()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={setHeaderKey}
|
||||||
|
placeholder="key"
|
||||||
|
className={`form-control ${styles.add_mar_right} ${styles.headerInputWidth}`}
|
||||||
|
/>
|
||||||
|
<div className={styles.headerInputWidth}>
|
||||||
|
<DropdownButton
|
||||||
|
dropdownOptions={[
|
||||||
|
{ display_text: 'Value', value: 'static' },
|
||||||
|
{ display_text: 'From env var', value: 'env' },
|
||||||
|
]}
|
||||||
|
title={type === 'env' ? 'From env var' : 'Value'}
|
||||||
|
dataKey={type === 'env' ? 'env' : 'static'}
|
||||||
|
onButtonChange={setHeaderType}
|
||||||
|
onInputChange={setHeaderValue}
|
||||||
|
required={false}
|
||||||
|
bsClass={styles.dropdown_button}
|
||||||
|
inputVal={value}
|
||||||
|
id={`header-value-${i}`}
|
||||||
|
inputPlaceHolder={type === 'env' ? 'HEADER_FROM_ENV' : 'value'}
|
||||||
|
testId={`header-value-${i}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{i < headers.length - 1 ? (
|
||||||
|
<i
|
||||||
|
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||||
|
onClick={removeHeader}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Headers;
|
@ -1,13 +1,11 @@
|
|||||||
const emptyHeader = {
|
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) {
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
13
console/src/components/Common/Icons/Clock.js
Normal file
13
console/src/components/Common/Icons/Clock.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Clock = ({ className, title = '' }) => {
|
||||||
|
return (
|
||||||
|
<i
|
||||||
|
className={`fa fa-clock-o ${className || ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Clock;
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
13
console/src/components/Common/Icons/Invalid.js
Normal file
13
console/src/components/Common/Icons/Invalid.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Invalid = ({ className, title = '' }) => {
|
||||||
|
return (
|
||||||
|
<i
|
||||||
|
className={`fa fa-exclamation ${className || ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Invalid;
|
7
console/src/components/Common/Icons/Reload.js
Normal file
7
console/src/components/Common/Icons/Reload.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Reload = ({ className }) => {
|
||||||
|
return <i className={`fa fa-repeat ${className || ''}`} aria-hidden="true" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Reload;
|
@ -1,52 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
class BreadCrumb extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { breadCrumbs } = this.props;
|
|
||||||
const styles = require('../../TableCommon/Table.scss');
|
|
||||||
|
|
||||||
let bC = null;
|
|
||||||
|
|
||||||
if (breadCrumbs && breadCrumbs.length > 0) {
|
|
||||||
bC = breadCrumbs.map((b, i) => {
|
|
||||||
let bCElem;
|
|
||||||
|
|
||||||
const Sp = () => {
|
|
||||||
const space = ' ';
|
|
||||||
return space;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addArrow = () => [
|
|
||||||
<Sp key={'breadcrumb-space-before' + i} />,
|
|
||||||
<i key={'l' + i} className="fa fa-angle-right" aria-hidden="true" />,
|
|
||||||
<Sp key={'breadcrumb-space-after' + i} />,
|
|
||||||
];
|
|
||||||
|
|
||||||
const isLastElem = i === breadCrumbs.length - 1;
|
|
||||||
|
|
||||||
if (!isLastElem) {
|
|
||||||
bCElem = [
|
|
||||||
<Link key={'l' + i} to={`${b.url}`}>
|
|
||||||
{b.title}
|
|
||||||
</Link>,
|
|
||||||
addArrow(),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
bCElem = [b.title];
|
|
||||||
}
|
|
||||||
|
|
||||||
return bCElem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={styles.dataBreadCrumb}>You are here: {bC}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BreadCrumb.propTypes = {
|
|
||||||
breadCrumbs: PropTypes.array.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BreadCrumb;
|
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import styles from '../../TableCommon/Table.scss';
|
||||||
|
|
||||||
|
export type BreadCrumb = {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
breadCrumbs: BreadCrumb[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const BreadCrumb: React.FC<Props> = ({ breadCrumbs }) => {
|
||||||
|
let bC = null;
|
||||||
|
|
||||||
|
if (breadCrumbs && breadCrumbs.length > 0) {
|
||||||
|
bC = breadCrumbs.map((b: BreadCrumb, i: number) => {
|
||||||
|
let bCElem;
|
||||||
|
|
||||||
|
const addArrow = () => (
|
||||||
|
<React.Fragment>
|
||||||
|
|
||||||
|
<i
|
||||||
|
key={`${b.title}-arrow`}
|
||||||
|
className="fa fa-angle-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLastElem = i === breadCrumbs.length - 1;
|
||||||
|
|
||||||
|
if (!isLastElem) {
|
||||||
|
bCElem = [
|
||||||
|
<Link key={`bc-title-${b.title}`} to={`${b.url}`}>
|
||||||
|
{b.title}
|
||||||
|
</Link>,
|
||||||
|
addArrow(),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
bCElem = [b.title];
|
||||||
|
}
|
||||||
|
|
||||||
|
return bCElem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={styles.dataBreadCrumb}>You are here: {bC}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BreadCrumb;
|
@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import BreadCrumb from '../BreadCrumb/BreadCrumb';
|
|
||||||
import Tabs from '../ReusableTabs/ReusableTabs';
|
|
||||||
|
|
||||||
class CommonTabLayout extends React.Component {
|
|
||||||
render() {
|
|
||||||
const styles = require('./CommonTabLayout.scss');
|
|
||||||
const {
|
|
||||||
breadCrumbs,
|
|
||||||
heading,
|
|
||||||
appPrefix,
|
|
||||||
currentTab,
|
|
||||||
tabsInfo,
|
|
||||||
baseUrl,
|
|
||||||
showLoader,
|
|
||||||
testPrefix,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.subHeader}>
|
|
||||||
<BreadCrumb breadCrumbs={breadCrumbs} />
|
|
||||||
<h2 className={styles.heading_text + ' ' + styles.set_line_height}>
|
|
||||||
{heading || ''}
|
|
||||||
</h2>
|
|
||||||
<Tabs
|
|
||||||
appPrefix={appPrefix}
|
|
||||||
tabName={currentTab}
|
|
||||||
tabsInfo={tabsInfo}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
showLoader={showLoader}
|
|
||||||
testPrefix={testPrefix}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommonTabLayout;
|
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import BreadCrumb, {
|
||||||
|
BreadCrumb as BreadCrumbType,
|
||||||
|
} from '../BreadCrumb/BreadCrumb';
|
||||||
|
import Tabs, { Tabs as TabsType } from '../ReusableTabs/ReusableTabs';
|
||||||
|
import styles from './CommonTabLayout.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
breadCrumbs: BreadCrumbType[];
|
||||||
|
heading: React.ReactNode;
|
||||||
|
appPrefix: string;
|
||||||
|
currentTab: string;
|
||||||
|
tabsInfo: TabsType;
|
||||||
|
baseUrl: string;
|
||||||
|
showLoader: boolean;
|
||||||
|
testPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommonTabLayout: React.FC<Props> = props => {
|
||||||
|
const {
|
||||||
|
breadCrumbs,
|
||||||
|
heading,
|
||||||
|
appPrefix,
|
||||||
|
currentTab,
|
||||||
|
tabsInfo,
|
||||||
|
baseUrl,
|
||||||
|
showLoader,
|
||||||
|
testPrefix,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.subHeader}>
|
||||||
|
<BreadCrumb breadCrumbs={breadCrumbs} />
|
||||||
|
<h2 className={`${styles.heading_text} ${styles.set_line_height}`}>
|
||||||
|
{heading || ''}
|
||||||
|
</h2>
|
||||||
|
<Tabs
|
||||||
|
appPrefix={appPrefix}
|
||||||
|
tabName={currentTab}
|
||||||
|
tabsInfo={tabsInfo}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
showLoader={showLoader}
|
||||||
|
testPrefix={testPrefix}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommonTabLayout;
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
class LeftContainer extends React.Component {
|
|
||||||
render() {
|
|
||||||
const styles = require('../../TableCommon/Table.scss');
|
|
||||||
|
|
||||||
const { children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.pageSidebar + ' col-xs-12 ' + styles.padd_remove}>
|
|
||||||
<div>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftContainer;
|
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from '../../TableCommon/Table.scss';
|
||||||
|
|
||||||
|
const LeftContainer: React.FC = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.pageSidebar} col-xs-12 ${styles.padd_remove}`}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeftContainer;
|
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import styles from './LeftSubSidebar.scss';
|
||||||
|
|
||||||
|
interface LeftSidebarItem {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeftSidebarSectionProps extends React.ComponentProps<'div'> {
|
||||||
|
items: LeftSidebarItem[];
|
||||||
|
currentItem?: LeftSidebarItem;
|
||||||
|
getServiceEntityLink: (s: string) => string;
|
||||||
|
service: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLeftSidebarSection = ({
|
||||||
|
items = [],
|
||||||
|
currentItem,
|
||||||
|
service,
|
||||||
|
getServiceEntityLink,
|
||||||
|
}: LeftSidebarSectionProps) => {
|
||||||
|
// TODO needs refactor to accomodate other services
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = React.useState('');
|
||||||
|
|
||||||
|
const getSearchInput = () => {
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="form-control"
|
||||||
|
placeholder={`search ${service}`}
|
||||||
|
data-test={`search-${service}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO test search
|
||||||
|
let itemList: LeftSidebarItem[] = [];
|
||||||
|
if (searchText) {
|
||||||
|
const secondaryResults: LeftSidebarItem[] = [];
|
||||||
|
items.forEach(a => {
|
||||||
|
if (a.name.startsWith(searchText)) {
|
||||||
|
itemList.push(a);
|
||||||
|
} else if (a.name.includes(searchText)) {
|
||||||
|
secondaryResults.push(a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemList = [...itemList, ...secondaryResults];
|
||||||
|
} else {
|
||||||
|
itemList = [...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChildList = () => {
|
||||||
|
let childList;
|
||||||
|
if (itemList.length === 0) {
|
||||||
|
childList = (
|
||||||
|
<li className={styles.noChildren} data-test="sidebar-no-services">
|
||||||
|
<i>No {service} available</i>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
childList = itemList.map(a => {
|
||||||
|
let activeTableClass = '';
|
||||||
|
if (currentItem && currentItem.name === a.name) {
|
||||||
|
activeTableClass = styles.activeLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={activeTableClass}
|
||||||
|
key={a.name}
|
||||||
|
data-test={`action-sidebar-links-${a.name}`}
|
||||||
|
>
|
||||||
|
<Link to={getServiceEntityLink(a.name)} data-test={a.name}>
|
||||||
|
<i
|
||||||
|
className={`${styles.tableIcon} fa fa-wrench`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{a.name}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return childList;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getChildList,
|
||||||
|
getSearchInput,
|
||||||
|
count: itemList.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getLeftSidebarSection;
|
@ -1,86 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
import Button from '../../Button/Button';
|
|
||||||
|
|
||||||
class LeftSubSidebar extends React.Component {
|
|
||||||
render() {
|
|
||||||
const styles = require('./LeftSubSidebar.scss');
|
|
||||||
|
|
||||||
const {
|
|
||||||
showAddBtn,
|
|
||||||
searchInput,
|
|
||||||
heading,
|
|
||||||
addLink,
|
|
||||||
addLabel,
|
|
||||||
addTestString,
|
|
||||||
children,
|
|
||||||
childListTestString,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const getAddButton = () => {
|
|
||||||
let addButton = null;
|
|
||||||
|
|
||||||
if (showAddBtn) {
|
|
||||||
addButton = (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'col-xs-4 text-center ' +
|
|
||||||
styles.padd_left_remove +
|
|
||||||
' ' +
|
|
||||||
styles.sidebarCreateTable
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link className={styles.padd_remove_full} to={addLink}>
|
|
||||||
<Button size="xs" color="white" data-test={addTestString}>
|
|
||||||
{addLabel}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return addButton;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.subSidebarList}>
|
|
||||||
<div className={styles.display_flex + ' ' + styles.padd_top_medium}>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
styles.sidebarSearch +
|
|
||||||
' form-group col-xs-12 ' +
|
|
||||||
styles.padd_remove
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i className="fa fa-search" aria-hidden="true" />
|
|
||||||
{searchInput}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className={styles.sidebarHeadingWrapper}>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'col-xs-8 ' +
|
|
||||||
styles.sidebarHeading +
|
|
||||||
' ' +
|
|
||||||
styles.padd_left_remove
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{heading}
|
|
||||||
</div>
|
|
||||||
{getAddButton()}
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
className={styles.subSidebarListUL}
|
|
||||||
data-test={childListTestString}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftSubSidebar;
|
|
@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
import Button from '../../Button/Button';
|
||||||
|
import styles from './LeftSubSidebar.scss';
|
||||||
|
|
||||||
|
interface Props extends React.ComponentProps<'div'> {
|
||||||
|
showAddBtn: boolean;
|
||||||
|
searchInput: React.ReactNode;
|
||||||
|
heading: string;
|
||||||
|
addLink: string;
|
||||||
|
addLabel: string;
|
||||||
|
addTestString: string;
|
||||||
|
childListTestString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeftSubSidebar: React.FC<Props> = props => {
|
||||||
|
const {
|
||||||
|
showAddBtn,
|
||||||
|
searchInput,
|
||||||
|
heading,
|
||||||
|
addLink,
|
||||||
|
addLabel,
|
||||||
|
addTestString,
|
||||||
|
children,
|
||||||
|
childListTestString,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const getAddButton = () => {
|
||||||
|
let addButton = null;
|
||||||
|
|
||||||
|
if (showAddBtn) {
|
||||||
|
addButton = (
|
||||||
|
<div
|
||||||
|
className={`col-xs-4 text-center ${styles.padd_left_remove} ${styles.sidebarCreateTable}`}
|
||||||
|
>
|
||||||
|
<Link className={styles.padd_remove_full} to={addLink}>
|
||||||
|
<Button size="xs" color="white" data-test={addTestString}>
|
||||||
|
{addLabel}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.subSidebarList}>
|
||||||
|
<div className={`${styles.display_flex} ${styles.padd_top_medium}`}>
|
||||||
|
<div
|
||||||
|
className={`${styles.sidebarSearch} form-group col-xs-12 ${styles.padd_remove}`}
|
||||||
|
>
|
||||||
|
<i className="fa fa-search" aria-hidden="true" />
|
||||||
|
{searchInput}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.sidebarHeadingWrapper}>
|
||||||
|
<div
|
||||||
|
className={`col-xs-8 ${styles.sidebarHeading} ${styles.padd_left_remove}`}
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</div>
|
||||||
|
{getAddButton()}
|
||||||
|
</div>
|
||||||
|
<ul className={styles.subSidebarListUL} data-test={childListTestString}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeftSubSidebar;
|
@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
|
|
||||||
class PageContainer extends React.Component {
|
|
||||||
render() {
|
|
||||||
const styles = require('../../Common.scss');
|
|
||||||
|
|
||||||
const { helmet, leftContainer, children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Helmet title={helmet} />
|
|
||||||
<div className={styles.wd20 + ' ' + styles.align_left}>
|
|
||||||
{leftContainer}
|
|
||||||
</div>
|
|
||||||
<div className={styles.wd80}>{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageContainer;
|
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
import styles from '../../Common.scss';
|
||||||
|
|
||||||
|
interface PageContainerProps extends React.ComponentProps<'div'> {
|
||||||
|
helmet: string;
|
||||||
|
leftContainer: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageContainer: React.FC<PageContainerProps> = ({
|
||||||
|
helmet,
|
||||||
|
leftContainer,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Helmet title={helmet} />
|
||||||
|
<div className={`${styles.wd20} ${styles.align_left}`}>
|
||||||
|
{leftContainer}
|
||||||
|
</div>
|
||||||
|
<div className={styles.wd80}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageContainer;
|
@ -1,51 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
const Tabs = ({
|
|
||||||
appPrefix,
|
|
||||||
tabsInfo,
|
|
||||||
tabName,
|
|
||||||
count,
|
|
||||||
baseUrl,
|
|
||||||
showLoader,
|
|
||||||
testPrefix,
|
|
||||||
}) => {
|
|
||||||
let showCount = '';
|
|
||||||
if (!(count === null || count === undefined)) {
|
|
||||||
showCount = '(' + count + ')';
|
|
||||||
}
|
|
||||||
const styles = require('./ReusableTabs.scss');
|
|
||||||
const dataLoader = () => {
|
|
||||||
return (
|
|
||||||
<span className={styles.loader_ml}>
|
|
||||||
<i className="fa fa-spinner fa-spin" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return [
|
|
||||||
<div className={styles.common_nav} key={'reusable-tabs-1'}>
|
|
||||||
<ul className="nav nav-pills">
|
|
||||||
{Object.keys(tabsInfo).map((t, i) => (
|
|
||||||
<li
|
|
||||||
role="presentation"
|
|
||||||
className={tabName === t ? styles.active : ''}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={`${baseUrl}/${t}`}
|
|
||||||
data-test={`${
|
|
||||||
testPrefix ? testPrefix + '-' : ''
|
|
||||||
}${appPrefix.slice(1)}-${t}`}
|
|
||||||
>
|
|
||||||
{tabsInfo[t].display_text} {tabName === t ? showCount : null}
|
|
||||||
{tabName === t && showLoader ? dataLoader() : null}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>,
|
|
||||||
<div className="clearfix" key={'reusable-tabs-2'} />,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tabs;
|
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import styles from './ReusableTabs.scss';
|
||||||
|
|
||||||
|
export type Tabs = Record<string, { display_text: string }>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appPrefix: string;
|
||||||
|
tabsInfo: Tabs;
|
||||||
|
tabName: string;
|
||||||
|
count?: number;
|
||||||
|
baseUrl: string;
|
||||||
|
showLoader: boolean;
|
||||||
|
testPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tabs: React.FC<Props> = ({
|
||||||
|
appPrefix,
|
||||||
|
tabsInfo,
|
||||||
|
tabName,
|
||||||
|
count,
|
||||||
|
baseUrl,
|
||||||
|
showLoader,
|
||||||
|
testPrefix,
|
||||||
|
}) => {
|
||||||
|
let showCount = '';
|
||||||
|
if (!(count === null || count === undefined)) {
|
||||||
|
showCount = `(${count})`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={styles.common_nav} key="reusable-tabs-1">
|
||||||
|
<ul className="nav nav-pills">
|
||||||
|
{Object.keys(tabsInfo).map((t: string) => (
|
||||||
|
<li
|
||||||
|
role="presentation"
|
||||||
|
className={tabName === t ? styles.active : ''}
|
||||||
|
key={t}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`${baseUrl}/${t}`}
|
||||||
|
data-test={`${
|
||||||
|
testPrefix ? `${testPrefix}-` : ''
|
||||||
|
}${appPrefix.slice(1)}-${t}`}
|
||||||
|
>
|
||||||
|
{tabsInfo[t].display_text} {tabName === t ? showCount : null}
|
||||||
|
{tabName === t && showLoader ? (
|
||||||
|
<span className={styles.loader_ml}>
|
||||||
|
<i className="fa fa-spinner fa-spin" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="clearfix" key="reusable-tabs-2" />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tabs;
|
@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const RightContainer = ({ children }) => {
|
|
||||||
const styles = require('./RightContainer.scss');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container + ' container-fluid'}>
|
|
||||||
<div className="row">
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
styles.main + ' ' + styles.padd_left_remove + ' ' + styles.padd_top
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.rightBar + ' '}>
|
|
||||||
{children && React.cloneElement(children)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
schema: state.tables.allSchemas,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightContainerConnector = connect =>
|
|
||||||
connect(mapStateToProps)(RightContainer);
|
|
||||||
|
|
||||||
export default rightContainerConnector;
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Connect } from 'react-redux';
|
||||||
|
import styles from './RightContainer.scss';
|
||||||
|
|
||||||
|
const RightContainer: React.FC = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container} container-fluid`}>
|
||||||
|
<div className="row">
|
||||||
|
<div
|
||||||
|
className={`${styles.main} ${styles.padd_left_remove} ${styles.padd_top}`}
|
||||||
|
>
|
||||||
|
<div className={`${styles.rightBar} `}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightContainerConnector = (connect: Connect) => connect()(RightContainer);
|
||||||
|
|
||||||
|
export default rightContainerConnector;
|
@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Spinner = ({ className = '' }) => {
|
|
||||||
const styles = require('./Spinner.scss');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.sk_circle + ' ' + className}>
|
|
||||||
<div className={styles.sk_circle1 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle2 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle3 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle4 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle5 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle6 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle7 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle8 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle9 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle10 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle11 + ' ' + styles.sk_child} />
|
|
||||||
<div className={styles.sk_circle12 + ' ' + styles.sk_child} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Spinner;
|
|
27
console/src/components/Common/Spinner/Spinner.tsx
Normal file
27
console/src/components/Common/Spinner/Spinner.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './Spinner.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Spinner: React.FC<Props> = ({ className = '' }) => {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.sk_circle} ${className}`}>
|
||||||
|
<div className={`${styles.sk_circle1} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle2} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle3} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle4} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle5} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle6} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle7} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle8} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle9} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle10} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle11} ${styles.sk_child}`} />
|
||||||
|
<div className={`${styles.sk_circle12} ${styles.sk_child}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spinner;
|
@ -150,3 +150,22 @@
|
|||||||
.ReactTable .rt-thead [role='columnheader'] {
|
.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;
|
||||||
|
}
|
||||||
|
@ -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}`}
|
@ -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,20 +245,18 @@ 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');
|
||||||
|
};
|
@ -1,425 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { isEqual, isString } from './jsUtils';
|
|
||||||
|
|
||||||
/*** Table/View utils ***/
|
|
||||||
|
|
||||||
export const getTableName = table => {
|
|
||||||
return table.table_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableSchema = table => {
|
|
||||||
return table.table_schema;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableType = table => {
|
|
||||||
return table.table_type;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: figure out better pattern for overloading fns
|
|
||||||
// tableName and tableNameWithSchema are either/or arguments
|
|
||||||
export const generateTableDef = (
|
|
||||||
tableName,
|
|
||||||
tableSchema = 'public',
|
|
||||||
tableNameWithSchema = null
|
|
||||||
) => {
|
|
||||||
if (tableNameWithSchema) {
|
|
||||||
tableSchema = tableNameWithSchema.split('.')[0];
|
|
||||||
tableName = tableNameWithSchema.split('.')[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
schema: tableSchema,
|
|
||||||
name: tableName,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableDef = table => {
|
|
||||||
return generateTableDef(getTableName(table), getTableSchema(table));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getQualifiedTableDef = tableDef => {
|
|
||||||
return isString(tableDef) ? generateTableDef(tableDef) : tableDef;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableNameWithSchema = (tableDef, wrapDoubleQuotes = false) => {
|
|
||||||
let _fullTableName;
|
|
||||||
|
|
||||||
if (wrapDoubleQuotes) {
|
|
||||||
_fullTableName =
|
|
||||||
'"' + tableDef.schema + '"' + '.' + '"' + tableDef.name + '"';
|
|
||||||
} else {
|
|
||||||
_fullTableName = tableDef.schema + '.' + tableDef.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _fullTableName;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkIfTable = table => {
|
|
||||||
return table.table_type === 'TABLE';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const displayTableName = table => {
|
|
||||||
const tableName = getTableName(table);
|
|
||||||
const isTable = checkIfTable(table);
|
|
||||||
|
|
||||||
return isTable ? <span>{tableName}</span> : <i>{tableName}</i>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findTable = (allTables, tableDef) => {
|
|
||||||
return allTables.find(t => isEqual(getTableDef(t), tableDef));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTrackedTables = tables => {
|
|
||||||
return tables.filter(t => t.is_table_tracked);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUntrackedTables = tables => {
|
|
||||||
return tables.filter(t => !t.is_table_tracked);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOnlyTables = tablesOrViews => {
|
|
||||||
return tablesOrViews.filter(t => checkIfTable(t));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOnlyViews = tablesOrViews => {
|
|
||||||
return tablesOrViews.filter(t => !checkIfTable(t));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QUERY_TYPES = ['insert', 'select', 'update', 'delete'];
|
|
||||||
|
|
||||||
export const getTableSupportedQueries = table => {
|
|
||||||
let supportedQueryTypes;
|
|
||||||
|
|
||||||
if (checkIfTable(table)) {
|
|
||||||
supportedQueryTypes = QUERY_TYPES;
|
|
||||||
} else {
|
|
||||||
// is View
|
|
||||||
supportedQueryTypes = [];
|
|
||||||
|
|
||||||
// Add insert/update permission if it is insertable/updatable as returned by pg
|
|
||||||
if (table.view_info) {
|
|
||||||
if (
|
|
||||||
table.view_info.is_insertable_into === 'YES' ||
|
|
||||||
table.view_info.is_trigger_insertable_into === 'YES'
|
|
||||||
) {
|
|
||||||
supportedQueryTypes.push('insert');
|
|
||||||
}
|
|
||||||
|
|
||||||
supportedQueryTypes.push('select'); // to maintain order
|
|
||||||
|
|
||||||
if (table.view_info.is_updatable === 'YES') {
|
|
||||||
supportedQueryTypes.push('update');
|
|
||||||
supportedQueryTypes.push('delete');
|
|
||||||
} else {
|
|
||||||
if (table.view_info.is_trigger_updatable === 'YES') {
|
|
||||||
supportedQueryTypes.push('update');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table.view_info.is_trigger_deletable === 'YES') {
|
|
||||||
supportedQueryTypes.push('delete');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
supportedQueryTypes.push('select');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return supportedQueryTypes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Table/View column utils ***/
|
|
||||||
|
|
||||||
export const getTableColumns = table => {
|
|
||||||
return table.columns;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getColumnName = column => {
|
|
||||||
return column.column_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableColumnNames = table => {
|
|
||||||
return getTableColumns(table).map(c => getColumnName(c));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableColumn = (table, columnName) => {
|
|
||||||
return getTableColumns(table).find(
|
|
||||||
column => getColumnName(column) === columnName
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getColumnType = column => {
|
|
||||||
let _columnType = column.data_type;
|
|
||||||
|
|
||||||
if (_columnType === 'USER-DEFINED') {
|
|
||||||
_columnType = column.udt_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _columnType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isColumnAutoIncrement = column => {
|
|
||||||
const columnDefault = column.column_default;
|
|
||||||
|
|
||||||
const autoIncrementDefaultRegex = /^nextval\('(.*)_seq'::regclass\)$/;
|
|
||||||
|
|
||||||
return (
|
|
||||||
columnDefault &&
|
|
||||||
columnDefault.match(new RegExp(autoIncrementDefaultRegex, 'gi'))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Table/View relationship utils ***/
|
|
||||||
|
|
||||||
export const getTableRelationships = table => {
|
|
||||||
return table.relationships;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRelationshipName = relationship => {
|
|
||||||
return relationship.rel_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRelationshipDef = relationship => {
|
|
||||||
return relationship.rel_def;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRelationshipType = relationship => {
|
|
||||||
return relationship.rel_type;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableRelationshipNames = table => {
|
|
||||||
return getTableRelationships(table).map(r => getRelationshipName(r));
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getTableRelationship(table, relationshipName) {
|
|
||||||
return getTableRelationships(table).find(
|
|
||||||
relationship => getRelationshipName(relationship) === relationshipName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRelationshipRefTable(table, relationship) {
|
|
||||||
let _refTable = null;
|
|
||||||
|
|
||||||
const relationshipDef = getRelationshipDef(relationship);
|
|
||||||
const relationshipType = getRelationshipType(relationship);
|
|
||||||
|
|
||||||
// if manual relationship
|
|
||||||
if (relationshipDef.manual_configuration) {
|
|
||||||
_refTable = relationshipDef.manual_configuration.remote_table;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if foreign-key based relationship
|
|
||||||
if (relationshipDef.foreign_key_constraint_on) {
|
|
||||||
// if array relationship
|
|
||||||
if (relationshipType === 'array') {
|
|
||||||
_refTable = relationshipDef.foreign_key_constraint_on.table;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if object relationship
|
|
||||||
if (relationshipType === 'object') {
|
|
||||||
const fkCol = relationshipDef.foreign_key_constraint_on;
|
|
||||||
|
|
||||||
for (let i = 0; i < table.foreign_key_constraints.length; i++) {
|
|
||||||
const fkConstraint = table.foreign_key_constraints[i];
|
|
||||||
const fkConstraintCol = Object.keys(fkConstraint.column_mapping)[0];
|
|
||||||
if (fkCol === fkConstraintCol) {
|
|
||||||
_refTable = generateTableDef(
|
|
||||||
fkConstraint.ref_table,
|
|
||||||
fkConstraint.ref_table_table_schema
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof _refTable === 'string') {
|
|
||||||
_refTable = generateTableDef(_refTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _refTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} currentSchema
|
|
||||||
* @param {string} currentTable
|
|
||||||
* @param {Array<{[key: string]: any}>} allSchemas
|
|
||||||
*
|
|
||||||
* @returns {Array<{
|
|
||||||
* columnName: string,
|
|
||||||
* enumTableName: string,
|
|
||||||
* enumColumnName: string,
|
|
||||||
* }>}
|
|
||||||
*/
|
|
||||||
export const getEnumColumnMappings = (allSchemas, tableName, tableSchema) => {
|
|
||||||
const currentTable = findTable(
|
|
||||||
allSchemas,
|
|
||||||
generateTableDef(tableName, tableSchema)
|
|
||||||
);
|
|
||||||
|
|
||||||
const relationsMap = [];
|
|
||||||
if (!currentTable.foreign_key_constraints.length) return;
|
|
||||||
|
|
||||||
currentTable.foreign_key_constraints.map(
|
|
||||||
({ ref_table, ref_table_table_schema, column_mapping }) => {
|
|
||||||
const refTableSchema = findTable(
|
|
||||||
allSchemas,
|
|
||||||
generateTableDef(ref_table, ref_table_table_schema)
|
|
||||||
);
|
|
||||||
if (!refTableSchema || !refTableSchema.is_enum) return;
|
|
||||||
|
|
||||||
const keys = Object.keys(column_mapping);
|
|
||||||
if (!keys.length) return;
|
|
||||||
|
|
||||||
const _columnName = keys[0];
|
|
||||||
const _enumColumnName = column_mapping[_columnName];
|
|
||||||
|
|
||||||
if (_columnName && _enumColumnName) {
|
|
||||||
relationsMap.push({
|
|
||||||
columnName: _columnName,
|
|
||||||
enumTableName: ref_table,
|
|
||||||
enumColumnName: _enumColumnName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return relationsMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Table/View permissions utils ***/
|
|
||||||
|
|
||||||
export const getTablePermissions = (table, role = null, action = null) => {
|
|
||||||
let tablePermissions = table.permissions;
|
|
||||||
|
|
||||||
if (role) {
|
|
||||||
tablePermissions = tablePermissions.find(p => p.role_name === role);
|
|
||||||
|
|
||||||
if (tablePermissions && action) {
|
|
||||||
tablePermissions = tablePermissions.permissions[action];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tablePermissions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Table/View Check Constraints utils ***/
|
|
||||||
|
|
||||||
export const getTableCheckConstraints = table => {
|
|
||||||
return table.check_constraints;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCheckConstraintName = constraint => {
|
|
||||||
return constraint.constraint_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findTableCheckConstraint = (checkConstraints, constraintName) => {
|
|
||||||
return checkConstraints.find(
|
|
||||||
c => getCheckConstraintName(c) === constraintName
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Function utils ***/
|
|
||||||
|
|
||||||
export const getFunctionSchema = pgFunction => {
|
|
||||||
return pgFunction.function_schema;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFunctionName = pgFunction => {
|
|
||||||
return pgFunction.function_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFunctionDefinition = pgFunction => {
|
|
||||||
return pgFunction.function_definition;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSchemaFunctions = (allFunctions, fnSchema) => {
|
|
||||||
return allFunctions.filter(fn => getFunctionSchema(fn) === fnSchema);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findFunction = (allFunctions, functionName, functionSchema) => {
|
|
||||||
return allFunctions.find(
|
|
||||||
f =>
|
|
||||||
getFunctionName(f) === functionName &&
|
|
||||||
getFunctionSchema(f) === functionSchema
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Schema utils ***/
|
|
||||||
|
|
||||||
export const getSchemaName = schema => {
|
|
||||||
return schema.schema_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSchemaTables = (allTables, tableSchema) => {
|
|
||||||
return allTables.filter(t => getTableSchema(t) === tableSchema);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSchemaTableNames = (allTables, tableSchema) => {
|
|
||||||
return getSchemaTables(allTables, tableSchema).map(t => getTableName(t));
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Custom table fields utils ***/
|
|
||||||
|
|
||||||
export const getTableCustomRootFields = table => {
|
|
||||||
if (table.configuration) {
|
|
||||||
return table.configuration.custom_root_fields || {};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTableCustomColumnNames = table => {
|
|
||||||
if (table.configuration) {
|
|
||||||
return table.configuration.custom_column_names || {};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
/*** Table/View Computed Field utils ***/
|
|
||||||
|
|
||||||
export const getTableComputedFields = table => {
|
|
||||||
return table.computed_fields;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getComputedFieldName = computedField => {
|
|
||||||
return computedField.computed_field_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGroupedTableComputedFields = (table, allFunctions) => {
|
|
||||||
const groupedComputedFields = { scalar: [], table: [] };
|
|
||||||
|
|
||||||
getTableComputedFields(table).forEach(computedField => {
|
|
||||||
const computedFieldFnDef = computedField.definition.function;
|
|
||||||
const computedFieldFn = findFunction(
|
|
||||||
allFunctions,
|
|
||||||
computedFieldFnDef.name,
|
|
||||||
computedFieldFnDef.schema
|
|
||||||
);
|
|
||||||
|
|
||||||
if (computedFieldFn && computedFieldFn.return_type_type === 'b') {
|
|
||||||
groupedComputedFields.scalar.push(computedField);
|
|
||||||
} else {
|
|
||||||
groupedComputedFields.table.push(computedField);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return groupedComputedFields;
|
|
||||||
};
|
|
||||||
|
|
||||||
// export const getDependentTables = (table) => {
|
|
||||||
|
|
||||||
// return [
|
|
||||||
// {
|
|
||||||
// table_schema: table.table_schema,
|
|
||||||
// table_name: table.table_name,
|
|
||||||
// },
|
|
||||||
// ...table.foreign_key_constraints.map(fk_obj => ({
|
|
||||||
// table_name: fk_obj.ref_table,
|
|
||||||
// table_schema: fk_obj.ref_table_table_schema
|
|
||||||
// })),
|
|
||||||
// ...table.opp_foreign_key_constraints.map(fk_obj => ({
|
|
||||||
// table_name: fk_obj.table_name,
|
|
||||||
// table_schema: fk_obj.table_schema,
|
|
||||||
// }))
|
|
||||||
// ]
|
|
||||||
|
|
||||||
// };
|
|
582
console/src/components/Common/utils/pgUtils.tsx
Normal file
582
console/src/components/Common/utils/pgUtils.tsx
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { isEqual, isString } from './jsUtils';
|
||||||
|
import { Nullable } from './tsUtils';
|
||||||
|
import { TableDefinition, FunctionDefinition } from './v1QueryUtils';
|
||||||
|
|
||||||
|
/** * Table/View utils ** */
|
||||||
|
|
||||||
|
export type TableRelationship = {
|
||||||
|
rel_name: string;
|
||||||
|
rel_def: {
|
||||||
|
manual_configuration?: any;
|
||||||
|
foreign_key_constraint_on?: any; // TODO
|
||||||
|
};
|
||||||
|
rel_type: 'object' | 'array';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TablePermission = {
|
||||||
|
role_name: string;
|
||||||
|
permissions: {
|
||||||
|
[action: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BaseTableColumn {
|
||||||
|
column_name: string;
|
||||||
|
data_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableColumn extends BaseTableColumn {
|
||||||
|
column_name: string;
|
||||||
|
data_type: string;
|
||||||
|
udt_name: string;
|
||||||
|
column_default: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ForeignKeyConstraint = {
|
||||||
|
ref_table: string;
|
||||||
|
ref_table_table_schema: string;
|
||||||
|
column_mapping: {
|
||||||
|
[lcol: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckConstraint = {
|
||||||
|
constraint_name: string;
|
||||||
|
check: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComputedField = {
|
||||||
|
computed_field_name: string;
|
||||||
|
definition: {
|
||||||
|
function: FunctionDefinition;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Schema = {
|
||||||
|
schema_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BaseTable {
|
||||||
|
table_name: string;
|
||||||
|
table_schema: string;
|
||||||
|
columns: BaseTableColumn[];
|
||||||
|
is_enum: boolean;
|
||||||
|
}
|
||||||
|
export const makeBaseTable = (
|
||||||
|
name: string,
|
||||||
|
schema: string,
|
||||||
|
columns: BaseTableColumn[],
|
||||||
|
isEnum = false
|
||||||
|
): BaseTable => ({
|
||||||
|
table_name: name,
|
||||||
|
table_schema: schema,
|
||||||
|
columns,
|
||||||
|
is_enum: isEnum,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface Table extends BaseTable {
|
||||||
|
table_name: string;
|
||||||
|
table_schema: string;
|
||||||
|
table_type: string;
|
||||||
|
is_table_tracked: boolean;
|
||||||
|
columns: TableColumn[];
|
||||||
|
relationships: TableRelationship[];
|
||||||
|
permissions: TablePermission[];
|
||||||
|
foreign_key_constraints: ForeignKeyConstraint[];
|
||||||
|
check_constraints: CheckConstraint[];
|
||||||
|
configuration?: {
|
||||||
|
custom_column_names: {
|
||||||
|
[column: string]: string;
|
||||||
|
};
|
||||||
|
custom_root_fields: {
|
||||||
|
select: Nullable<string>;
|
||||||
|
select_by_pk: Nullable<string>;
|
||||||
|
select_aggregate?: Nullable<string>;
|
||||||
|
insert?: Nullable<string>;
|
||||||
|
insert_one?: Nullable<string>;
|
||||||
|
update?: Nullable<string>;
|
||||||
|
update_by_pk?: Nullable<string>;
|
||||||
|
delete?: Nullable<string>;
|
||||||
|
delete_by_pk?: Nullable<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
computed_fields: ComputedField[];
|
||||||
|
is_enum: boolean;
|
||||||
|
view_info: {
|
||||||
|
is_trigger_insertable_into: 'YES' | 'NO';
|
||||||
|
is_insertable_into: 'YES' | 'NO';
|
||||||
|
is_updatable: 'YES' | 'NO';
|
||||||
|
is_trigger_updatable: 'YES' | 'NO';
|
||||||
|
is_trigger_deletable: 'YES' | 'NO';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PGFunction = {
|
||||||
|
function_name: string;
|
||||||
|
function_schema: string;
|
||||||
|
function_definition: string;
|
||||||
|
return_type_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PGSchema = {
|
||||||
|
schema_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableName = (table: Table) => {
|
||||||
|
return table.table_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableSchema = (table: Table) => {
|
||||||
|
return table.table_schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableType = (table: Table) => {
|
||||||
|
return table.table_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: figure out better pattern for overloading fns
|
||||||
|
// tableName and tableNameWithSchema are either/or arguments
|
||||||
|
export const generateTableDef = (
|
||||||
|
tableName: string,
|
||||||
|
tableSchema: Nullable<string> = 'public',
|
||||||
|
tableNameWithSchema: Nullable<string> = null
|
||||||
|
) => {
|
||||||
|
if (tableNameWithSchema) {
|
||||||
|
return {
|
||||||
|
schema: tableNameWithSchema.split('.')[0],
|
||||||
|
name: tableNameWithSchema.split('.')[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema: tableSchema || 'public',
|
||||||
|
name: tableName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableDef = (table: Table) => {
|
||||||
|
return generateTableDef(getTableName(table), getTableSchema(table));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQualifiedTableDef = (tableDef: TableDefinition | string) => {
|
||||||
|
return isString(tableDef) ? generateTableDef(tableDef as string) : tableDef;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableNameWithSchema = (
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
wrapDoubleQuotes = false
|
||||||
|
) => {
|
||||||
|
let fullTableName;
|
||||||
|
|
||||||
|
if (wrapDoubleQuotes) {
|
||||||
|
fullTableName = `"${tableDef.schema}"."${tableDef.name}"`;
|
||||||
|
} else {
|
||||||
|
fullTableName = `${tableDef.schema}.${tableDef.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullTableName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkIfTable = (table: Table) => {
|
||||||
|
return table.table_type === 'TABLE';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const displayTableName = (table: Table) => {
|
||||||
|
const tableName = getTableName(table);
|
||||||
|
const isTable = checkIfTable(table);
|
||||||
|
|
||||||
|
return isTable ? <span>{tableName}</span> : <i>{tableName}</i>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findTable = (allTables: Table[], tableDef: TableDefinition) => {
|
||||||
|
return allTables.find(t => isEqual(getTableDef(t), tableDef));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTrackedTables = (tables: Table[]) => {
|
||||||
|
return tables.filter(t => t.is_table_tracked);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUntrackedTables = (tables: Table[]) => {
|
||||||
|
return tables.filter(t => !t.is_table_tracked);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOnlyTables = (tablesOrViews: Table[]) => {
|
||||||
|
return tablesOrViews.filter(t => checkIfTable(t));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOnlyViews = (tablesOrViews: Table[]) => {
|
||||||
|
return tablesOrViews.filter(t => !checkIfTable(t));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QUERY_TYPES = ['insert', 'select', 'update', 'delete'];
|
||||||
|
|
||||||
|
export const getTableSupportedQueries = (table: Table) => {
|
||||||
|
let supportedQueryTypes;
|
||||||
|
|
||||||
|
if (checkIfTable(table)) {
|
||||||
|
supportedQueryTypes = QUERY_TYPES;
|
||||||
|
} else {
|
||||||
|
// is View
|
||||||
|
supportedQueryTypes = [];
|
||||||
|
|
||||||
|
// Add insert/update permission if it is insertable/updatable as returned by pg
|
||||||
|
if (table.view_info) {
|
||||||
|
if (
|
||||||
|
table.view_info.is_insertable_into === 'YES' ||
|
||||||
|
table.view_info.is_trigger_insertable_into === 'YES'
|
||||||
|
) {
|
||||||
|
supportedQueryTypes.push('insert');
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedQueryTypes.push('select'); // to maintain order
|
||||||
|
|
||||||
|
if (table.view_info.is_updatable === 'YES') {
|
||||||
|
supportedQueryTypes.push('update');
|
||||||
|
supportedQueryTypes.push('delete');
|
||||||
|
} else {
|
||||||
|
if (table.view_info.is_trigger_updatable === 'YES') {
|
||||||
|
supportedQueryTypes.push('update');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table.view_info.is_trigger_deletable === 'YES') {
|
||||||
|
supportedQueryTypes.push('delete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
supportedQueryTypes.push('select');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportedQueryTypes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Table/View column utils ** */
|
||||||
|
|
||||||
|
export const getTableColumns = (table: Nullable<Table>) => {
|
||||||
|
return table ? table.columns : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getColumnName = (column: TableColumn) => {
|
||||||
|
return column.column_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableColumnNames = (table: Table) => {
|
||||||
|
return getTableColumns(table).map(c => getColumnName(c));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableColumn = (table: Table, columnName: string) => {
|
||||||
|
return getTableColumns(table).find(
|
||||||
|
column => getColumnName(column) === columnName
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getColumnType = (column: TableColumn) => {
|
||||||
|
let columnType = column.data_type;
|
||||||
|
|
||||||
|
if (columnType === 'USER-DEFINED') {
|
||||||
|
columnType = column.udt_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isColumnAutoIncrement = (column: TableColumn) => {
|
||||||
|
const columnDefault = column.column_default;
|
||||||
|
|
||||||
|
const autoIncrementDefaultRegex = /^nextval\('(.*)_seq'::regclass\)$/;
|
||||||
|
|
||||||
|
return (
|
||||||
|
columnDefault &&
|
||||||
|
columnDefault.match(new RegExp(autoIncrementDefaultRegex, 'gi'))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Table/View relationship utils ** */
|
||||||
|
|
||||||
|
export const getTableRelationships = (table: Table) => {
|
||||||
|
return table.relationships;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRelationshipName = (relationship: TableRelationship) => {
|
||||||
|
return relationship.rel_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRelationshipDef = (relationship: TableRelationship) => {
|
||||||
|
return relationship.rel_def;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRelationshipType = (relationship: TableRelationship) => {
|
||||||
|
return relationship.rel_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableRelationshipNames = (table: Table) => {
|
||||||
|
return getTableRelationships(table).map(r => getRelationshipName(r));
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTableRelationship(table: Table, relationshipName: string) {
|
||||||
|
return getTableRelationships(table).find(
|
||||||
|
relationship => getRelationshipName(relationship) === relationshipName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRelationshipRefTable(
|
||||||
|
table: Table,
|
||||||
|
relationship: TableRelationship
|
||||||
|
) {
|
||||||
|
let refTable = null;
|
||||||
|
|
||||||
|
const relationshipDef = getRelationshipDef(relationship);
|
||||||
|
const relationshipType = getRelationshipType(relationship);
|
||||||
|
|
||||||
|
// if manual relationship
|
||||||
|
if (relationshipDef.manual_configuration) {
|
||||||
|
refTable = relationshipDef.manual_configuration.remote_table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if foreign-key based relationship
|
||||||
|
if (relationshipDef.foreign_key_constraint_on) {
|
||||||
|
// if array relationship
|
||||||
|
if (relationshipType === 'array') {
|
||||||
|
refTable = relationshipDef.foreign_key_constraint_on.table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if object relationship
|
||||||
|
if (relationshipType === 'object') {
|
||||||
|
const fkCol = relationshipDef.foreign_key_constraint_on;
|
||||||
|
|
||||||
|
for (let i = 0; i < table.foreign_key_constraints.length; i++) {
|
||||||
|
const fkConstraint = table.foreign_key_constraints[i];
|
||||||
|
const fkConstraintCol = Object.keys(fkConstraint.column_mapping)[0];
|
||||||
|
if (fkCol === fkConstraintCol) {
|
||||||
|
refTable = generateTableDef(
|
||||||
|
fkConstraint.ref_table,
|
||||||
|
fkConstraint.ref_table_table_schema
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof refTable === 'string') {
|
||||||
|
refTable = generateTableDef(refTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
return refTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} currentSchema
|
||||||
|
* @param {string} currentTable
|
||||||
|
* @param {Array<{[key: string]: any}>} allSchemas
|
||||||
|
*
|
||||||
|
* @returns {Array<{
|
||||||
|
* columnName: string,
|
||||||
|
* enumTableName: string,
|
||||||
|
* enumColumnName: string,
|
||||||
|
* }>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getEnumColumnMappings = (
|
||||||
|
allSchemas: Table[],
|
||||||
|
tableName: string,
|
||||||
|
tableSchema: string
|
||||||
|
) => {
|
||||||
|
const currentTable = findTable(
|
||||||
|
allSchemas,
|
||||||
|
generateTableDef(tableName, tableSchema)
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationsMap: any[] = [];
|
||||||
|
if (!currentTable) return null;
|
||||||
|
if (!currentTable.foreign_key_constraints.length) return null;
|
||||||
|
|
||||||
|
currentTable.foreign_key_constraints.forEach(
|
||||||
|
({ ref_table, ref_table_table_schema, column_mapping }) => {
|
||||||
|
const refTable = findTable(
|
||||||
|
allSchemas,
|
||||||
|
generateTableDef(ref_table, ref_table_table_schema)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!refTable || !refTable.is_enum) return;
|
||||||
|
|
||||||
|
const keys = Object.keys(column_mapping);
|
||||||
|
if (!keys.length) return;
|
||||||
|
|
||||||
|
const columnName = keys[0];
|
||||||
|
const enumColumnName = column_mapping[columnName];
|
||||||
|
|
||||||
|
if (columnName && enumColumnName) {
|
||||||
|
relationsMap.push({
|
||||||
|
columnName,
|
||||||
|
enumTableName: ref_table,
|
||||||
|
enumColumnName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return relationsMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Table/View permissions utils ** */
|
||||||
|
|
||||||
|
export const getTablePermissions = (
|
||||||
|
table: Table,
|
||||||
|
role: string | null = null,
|
||||||
|
action: string | null = null
|
||||||
|
) => {
|
||||||
|
const tablePermissions = table.permissions;
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
const rolePermissions = tablePermissions.find(p => p.role_name === role);
|
||||||
|
|
||||||
|
if (rolePermissions && action) {
|
||||||
|
return rolePermissions.permissions[action];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tablePermissions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Table/View Check Constraints utils ** */
|
||||||
|
|
||||||
|
export const getTableCheckConstraints = (table: Table) => {
|
||||||
|
return table.check_constraints;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCheckConstraintName = (constraint: CheckConstraint) => {
|
||||||
|
return constraint.constraint_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findTableCheckConstraint = (
|
||||||
|
checkConstraints: CheckConstraint[],
|
||||||
|
constraintName: string
|
||||||
|
) => {
|
||||||
|
return checkConstraints.find(
|
||||||
|
c => getCheckConstraintName(c) === constraintName
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Function utils ** */
|
||||||
|
|
||||||
|
export const getFunctionSchema = (pgFunction: PGFunction) => {
|
||||||
|
return pgFunction.function_schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFunctionName = (pgFunction: PGFunction) => {
|
||||||
|
return pgFunction.function_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFunctionDefinition = (pgFunction: PGFunction) => {
|
||||||
|
return pgFunction.function_definition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSchemaFunctions = (
|
||||||
|
allFunctions: PGFunction[],
|
||||||
|
fnSchema: string
|
||||||
|
) => {
|
||||||
|
return allFunctions.filter(fn => getFunctionSchema(fn) === fnSchema);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findFunction = (
|
||||||
|
allFunctions: PGFunction[],
|
||||||
|
functionName: string,
|
||||||
|
functionSchema: string
|
||||||
|
) => {
|
||||||
|
return allFunctions.find(
|
||||||
|
f =>
|
||||||
|
getFunctionName(f) === functionName &&
|
||||||
|
getFunctionSchema(f) === functionSchema
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Schema utils ** */
|
||||||
|
|
||||||
|
export const getSchemaName = (schema: PGSchema) => {
|
||||||
|
return schema.schema_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSchemaTables = (allTables: Table[], tableSchema: string) => {
|
||||||
|
return allTables.filter(t => getTableSchema(t) === tableSchema);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSchemaTableNames = (
|
||||||
|
allTables: Table[],
|
||||||
|
tableSchema: string
|
||||||
|
) => {
|
||||||
|
return getSchemaTables(allTables, tableSchema).map(t => getTableName(t));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Custom table fields utils ** */
|
||||||
|
|
||||||
|
export const getTableCustomRootFields = (table: Table) => {
|
||||||
|
if (table.configuration) {
|
||||||
|
return table.configuration.custom_root_fields || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTableCustomColumnNames = (table: Table) => {
|
||||||
|
if (table.configuration) {
|
||||||
|
return table.configuration.custom_column_names || {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** * Table/View Computed Field utils ** */
|
||||||
|
|
||||||
|
export const getTableComputedFields = (table: Table) => {
|
||||||
|
return table.computed_fields;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComputedFieldName = (computedField: ComputedField) => {
|
||||||
|
return computedField.computed_field_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGroupedTableComputedFields = (
|
||||||
|
table: Table,
|
||||||
|
allFunctions: PGFunction[]
|
||||||
|
) => {
|
||||||
|
const groupedComputedFields: {
|
||||||
|
scalar: ComputedField[];
|
||||||
|
table: ComputedField[];
|
||||||
|
} = { scalar: [], table: [] };
|
||||||
|
|
||||||
|
getTableComputedFields(table).forEach(computedField => {
|
||||||
|
const computedFieldFnDef = computedField.definition.function;
|
||||||
|
const computedFieldFn = findFunction(
|
||||||
|
allFunctions,
|
||||||
|
computedFieldFnDef.name,
|
||||||
|
computedFieldFnDef.schema
|
||||||
|
);
|
||||||
|
|
||||||
|
if (computedFieldFn && computedFieldFn.return_type_type === 'b') {
|
||||||
|
groupedComputedFields.scalar.push(computedField);
|
||||||
|
} else {
|
||||||
|
groupedComputedFields.table.push(computedField);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedComputedFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const getDependentTables = (table) => {
|
||||||
|
|
||||||
|
// return [
|
||||||
|
// {
|
||||||
|
// table_schema: table.table_schema,
|
||||||
|
// table_name: table.table_name,
|
||||||
|
// },
|
||||||
|
// ...table.foreign_key_constraints.map(fk_obj => ({
|
||||||
|
// table_name: fk_obj.ref_table,
|
||||||
|
// table_schema: fk_obj.ref_table_table_schema
|
||||||
|
// })),
|
||||||
|
// ...table.opp_foreign_key_constraints.map(fk_obj => ({
|
||||||
|
// table_name: fk_obj.table_name,
|
||||||
|
// table_schema: fk_obj.table_schema,
|
||||||
|
// }))
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// };
|
11
console/src/components/Common/utils/reactUtils.ts
Normal file
11
console/src/components/Common/utils/reactUtils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Dispatch } from '../../../types';
|
||||||
|
|
||||||
|
export const getReactHelmetTitle = (feature: string, service: string) => {
|
||||||
|
return `${feature} - ${service} | Hasura`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* called "mapDispatchToPropsEmpty" because it just maps
|
||||||
|
* the "dispatch" function and not any custom dispatchers
|
||||||
|
*/
|
||||||
|
export const mapDispatchToPropsEmpty = (dispatch: Dispatch) => ({ dispatch });
|
@ -68,3 +68,91 @@ export const getActionsBaseRoute = () => {
|
|||||||
export const getActionsCreateRoute = () => {
|
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');
|
||||||
|
};
|
||||||
|
@ -1,31 +1,23 @@
|
|||||||
interface SqlUtilsOptions {
|
export const sqlEscapeText = (rawText: string) => {
|
||||||
tableName: string;
|
let text = rawText;
|
||||||
schemaName: string;
|
|
||||||
constraintName: string;
|
|
||||||
check?: string;
|
|
||||||
selectedPkColumns?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sqlEscapeText = (text: string) => {
|
if (text) {
|
||||||
let escapedText = text;
|
text = text.replace(/'/g, "\\'");
|
||||||
|
|
||||||
if (escapedText) {
|
|
||||||
escapedText = escapedText.replace(/'/g, "\\'");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `E'${escapedText}'`;
|
return `E'${text}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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,14 +62,14 @@ 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;
|
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -1,345 +0,0 @@
|
|||||||
import { terminateSql } from './sqlUtils';
|
|
||||||
|
|
||||||
export const getRunSqlQuery = (sql, shouldCascade, readOnly) => {
|
|
||||||
return {
|
|
||||||
type: 'run_sql',
|
|
||||||
args: {
|
|
||||||
sql: terminateSql(sql),
|
|
||||||
cascade: !!shouldCascade,
|
|
||||||
read_only: !!readOnly,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCreatePermissionQuery = (
|
|
||||||
action,
|
|
||||||
tableDef,
|
|
||||||
role,
|
|
||||||
permission
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
type: 'create_' + action + '_permission',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
role: role,
|
|
||||||
permission: permission,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDropPermissionQuery = (action, tableDef, role) => {
|
|
||||||
return {
|
|
||||||
type: 'drop_' + action + '_permission',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
role: role,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateSetCustomTypesQuery = customTypes => {
|
|
||||||
return {
|
|
||||||
type: 'set_custom_types',
|
|
||||||
args: customTypes,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateCreateActionQuery = (name, definition, comment) => {
|
|
||||||
return {
|
|
||||||
type: 'create_action',
|
|
||||||
args: {
|
|
||||||
name,
|
|
||||||
definition,
|
|
||||||
comment,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateDropActionQuery = name => {
|
|
||||||
return {
|
|
||||||
type: 'drop_action',
|
|
||||||
args: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFetchActionsQuery = () => {
|
|
||||||
return {
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'hdb_action',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: ['*.*'],
|
|
||||||
order_by: [{ column: 'action_name', type: 'asc' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFetchCustomTypesQuery = () => {
|
|
||||||
return {
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'hdb_custom_types',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: ['*.*'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSetCustomRootFieldsQuery = (
|
|
||||||
tableDef,
|
|
||||||
rootFields,
|
|
||||||
customColumnNames
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
type: 'set_table_custom_fields',
|
|
||||||
version: 2,
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
custom_root_fields: rootFields,
|
|
||||||
custom_column_names: customColumnNames,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFetchAllRolesQuery = () => ({
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
name: 'hdb_role',
|
|
||||||
},
|
|
||||||
columns: ['role_name'],
|
|
||||||
order_by: { column: 'role_name', type: 'asc' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getCreateActionPermissionQuery = (def, actionName) => {
|
|
||||||
return {
|
|
||||||
type: 'create_action_permission',
|
|
||||||
args: {
|
|
||||||
action: actionName,
|
|
||||||
role: def.role,
|
|
||||||
definition: {
|
|
||||||
select: {
|
|
||||||
filter: def.filter,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUpdateActionQuery = (def, actionName, actionComment) => {
|
|
||||||
return {
|
|
||||||
type: 'update_action',
|
|
||||||
args: {
|
|
||||||
name: actionName,
|
|
||||||
definition: def,
|
|
||||||
comment: actionComment,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDropActionPermissionQuery = (role, actionName) => {
|
|
||||||
return {
|
|
||||||
type: 'drop_action_permission',
|
|
||||||
args: {
|
|
||||||
action: actionName,
|
|
||||||
role,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSetTableEnumQuery = (tableDef, isEnum) => {
|
|
||||||
return {
|
|
||||||
type: 'set_table_is_enum',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
is_enum: isEnum,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTrackTableQuery = tableDef => {
|
|
||||||
return {
|
|
||||||
type: 'add_existing_table_or_view',
|
|
||||||
args: tableDef,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUntrackTableQuery = tableDef => {
|
|
||||||
return {
|
|
||||||
type: 'untrack_table',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAddComputedFieldQuery = (
|
|
||||||
tableDef,
|
|
||||||
computedFieldName,
|
|
||||||
definition,
|
|
||||||
comment
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
type: 'add_computed_field',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
name: computedFieldName,
|
|
||||||
definition: {
|
|
||||||
...definition,
|
|
||||||
},
|
|
||||||
comment: comment,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDropComputedFieldQuery = (tableDef, computedFieldName) => {
|
|
||||||
return {
|
|
||||||
type: 'drop_computed_field',
|
|
||||||
args: {
|
|
||||||
table: tableDef,
|
|
||||||
name: computedFieldName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDeleteQuery = (pkClause, tableName, schemaName) => {
|
|
||||||
return {
|
|
||||||
type: 'delete',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: tableName,
|
|
||||||
schema: schemaName,
|
|
||||||
},
|
|
||||||
where: pkClause,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBulkDeleteQuery = (pkClauses, tableName, schemaName) =>
|
|
||||||
pkClauses.map(pkClause => getDeleteQuery(pkClause, tableName, schemaName));
|
|
||||||
|
|
||||||
export const getEnumOptionsQuery = (request, currentSchema) => {
|
|
||||||
return {
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: request.enumTableName,
|
|
||||||
schema: currentSchema,
|
|
||||||
},
|
|
||||||
columns: [request.enumColumnName],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const inconsistentObjectsQuery = {
|
|
||||||
type: 'get_inconsistent_metadata',
|
|
||||||
args: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dropInconsistentObjectsQuery = {
|
|
||||||
type: 'drop_inconsistent_metadata',
|
|
||||||
args: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getReloadMetadataQuery = shouldReloadRemoteSchemas => ({
|
|
||||||
type: 'reload_metadata',
|
|
||||||
args: {
|
|
||||||
reload_remote_schemas: shouldReloadRemoteSchemas,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getReloadRemoteSchemaCacheQuery = remoteSchemaName => {
|
|
||||||
return {
|
|
||||||
type: 'reload_remote_schema',
|
|
||||||
args: {
|
|
||||||
name: remoteSchemaName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const exportMetadataQuery = {
|
|
||||||
type: 'export_metadata',
|
|
||||||
args: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateReplaceMetadataQuery = metadataJson => ({
|
|
||||||
type: 'replace_metadata',
|
|
||||||
args: metadataJson,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resetMetadataQuery = {
|
|
||||||
type: 'clear_metadata',
|
|
||||||
args: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateSelectQuery = (
|
|
||||||
type,
|
|
||||||
tableDef,
|
|
||||||
{ where, limit, offset, order_by, columns }
|
|
||||||
) => ({
|
|
||||||
type,
|
|
||||||
args: {
|
|
||||||
columns,
|
|
||||||
where,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
order_by,
|
|
||||||
table: tableDef,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getFetchManualTriggersQuery = tableName => ({
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_triggers',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: ['*'],
|
|
||||||
order_by: {
|
|
||||||
column: 'name',
|
|
||||||
type: 'asc',
|
|
||||||
nulls: 'last',
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
table_name: tableName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getConsoleOptsQuery = () =>
|
|
||||||
generateSelectQuery(
|
|
||||||
'select',
|
|
||||||
{ name: 'hdb_version', schema: 'hdb_catalog' },
|
|
||||||
{
|
|
||||||
columns: ['hasura_uuid', 'console_state'],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getSaveRemoteRelQuery = (args, isNew) => ({
|
|
||||||
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
|
||||||
args,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getDropRemoteRelQuery = (name, table) => ({
|
|
||||||
type: 'delete_remote_relationship',
|
|
||||||
args: {
|
|
||||||
name,
|
|
||||||
table,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getRemoteSchemaIntrospectionQuery = remoteSchemaName => ({
|
|
||||||
type: 'introspect_remote_schema',
|
|
||||||
args: {
|
|
||||||
name: remoteSchemaName,
|
|
||||||
},
|
|
||||||
});
|
|
680
console/src/components/Common/utils/v1QueryUtils.ts
Normal file
680
console/src/components/Common/utils/v1QueryUtils.ts
Normal file
@ -0,0 +1,680 @@
|
|||||||
|
import { terminateSql } from './sqlUtils';
|
||||||
|
import { LocalScheduledTriggerState } from '../../Services/Events/CronTriggers/state';
|
||||||
|
import { LocalAdhocEventState } from '../../Services/Events/AdhocEvents/Add/state';
|
||||||
|
import { LocalEventTriggerState } from '../../Services/Events/EventTriggers/state';
|
||||||
|
import { RemoteRelationshipPayload } from '../../Services/Data/TableRelationships/RemoteRelationships/utils';
|
||||||
|
import { transformHeaders } from '../Headers/utils';
|
||||||
|
import { generateTableDef } from './pgUtils';
|
||||||
|
import { Nullable } from './tsUtils';
|
||||||
|
|
||||||
|
// TODO add type for the where clause
|
||||||
|
|
||||||
|
// TODO extend all queries with v1 query type
|
||||||
|
|
||||||
|
export type OrderByType = 'asc' | 'desc';
|
||||||
|
export type OrderByNulls = 'first' | 'last';
|
||||||
|
|
||||||
|
export type OrderBy = {
|
||||||
|
column: string;
|
||||||
|
type: OrderByType;
|
||||||
|
nulls: Nullable<OrderByNulls>;
|
||||||
|
};
|
||||||
|
export const makeOrderBy = (
|
||||||
|
column: string,
|
||||||
|
type: OrderByType,
|
||||||
|
nulls: Nullable<OrderByNulls> = 'last'
|
||||||
|
): OrderBy => ({
|
||||||
|
column,
|
||||||
|
type,
|
||||||
|
nulls,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WhereClause = any;
|
||||||
|
|
||||||
|
export type TableDefinition = {
|
||||||
|
name: string;
|
||||||
|
schema: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FunctionDefinition = {
|
||||||
|
name: string;
|
||||||
|
schema: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GraphQLArgument = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Header = {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
value_from_env?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRunSqlQuery = (
|
||||||
|
sql: string,
|
||||||
|
shouldCascade: boolean,
|
||||||
|
readOnly: boolean
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'run_sql',
|
||||||
|
args: {
|
||||||
|
sql: terminateSql(sql),
|
||||||
|
cascade: !!shouldCascade,
|
||||||
|
read_only: !!readOnly,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCreatePermissionQuery = (
|
||||||
|
action: string,
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
role: string,
|
||||||
|
permission: any
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: `create_${action}_permission`,
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
role,
|
||||||
|
permission,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDropPermissionQuery = (
|
||||||
|
action: string,
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
role: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: `drop_${action}_permission`,
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
role,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomTypeScalar = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomTypeEnumValue = {
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
type CustomTypeEnum = {
|
||||||
|
name: string;
|
||||||
|
values: CustomTypeEnumValue[];
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomTypeObjectField = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
type CustomTypeObject = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
fields: CustomTypeObjectField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomTypeInputObjectField = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
type CustomTypeInputObject = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
fields: CustomTypeInputObjectField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomTypes = {
|
||||||
|
scalars: CustomTypeScalar[];
|
||||||
|
enums: CustomTypeEnum[];
|
||||||
|
objects: CustomTypeObject[];
|
||||||
|
input_objects: CustomTypeInputObject[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSetCustomTypesQuery = (customTypes: CustomTypes) => {
|
||||||
|
return {
|
||||||
|
type: 'set_custom_types',
|
||||||
|
args: customTypes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionDefinition = {
|
||||||
|
arguments: GraphQLArgument[];
|
||||||
|
kind: 'synchronous' | 'asynchronous';
|
||||||
|
output_type: string;
|
||||||
|
handler: string;
|
||||||
|
headers: Header[];
|
||||||
|
forward_client_headers: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateCreateActionQuery = (
|
||||||
|
name: string,
|
||||||
|
definition: ActionDefinition,
|
||||||
|
comment: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'create_action',
|
||||||
|
args: {
|
||||||
|
name,
|
||||||
|
definition,
|
||||||
|
comment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateDropActionQuery = (name: string) => {
|
||||||
|
return {
|
||||||
|
type: 'drop_action',
|
||||||
|
args: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFetchActionsQuery = () => {
|
||||||
|
return {
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: 'hdb_action',
|
||||||
|
schema: 'hdb_catalog',
|
||||||
|
},
|
||||||
|
columns: ['*.*'],
|
||||||
|
order_by: [{ column: 'action_name', type: 'asc' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFetchCustomTypesQuery = () => {
|
||||||
|
return {
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: 'hdb_custom_types',
|
||||||
|
schema: 'hdb_catalog',
|
||||||
|
},
|
||||||
|
columns: ['*.*'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomRootFields = {
|
||||||
|
select: string;
|
||||||
|
select_by_pk: string;
|
||||||
|
select_aggregate: string;
|
||||||
|
insert: string;
|
||||||
|
update: string;
|
||||||
|
delete: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CustomColumnNames = {
|
||||||
|
[columnName: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSetCustomRootFieldsQuery = (
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
rootFields: CustomRootFields,
|
||||||
|
customColumnNames: CustomColumnNames
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'set_table_custom_fields',
|
||||||
|
version: 2,
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
custom_root_fields: rootFields,
|
||||||
|
custom_column_names: customColumnNames,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFetchAllRolesQuery = () => ({
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
schema: 'hdb_catalog',
|
||||||
|
name: 'hdb_role',
|
||||||
|
},
|
||||||
|
columns: ['role_name'],
|
||||||
|
order_by: [{ column: 'role_name', type: 'asc' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO Refactor and accept role, filter and action name
|
||||||
|
export const getCreateActionPermissionQuery = (
|
||||||
|
def: { role: string; filter: any },
|
||||||
|
actionName: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'create_action_permission',
|
||||||
|
args: {
|
||||||
|
action: actionName,
|
||||||
|
role: def.role,
|
||||||
|
definition: {
|
||||||
|
select: {
|
||||||
|
filter: def.filter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUpdateActionQuery = (
|
||||||
|
def: ActionDefinition,
|
||||||
|
actionName: string,
|
||||||
|
actionComment: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'update_action',
|
||||||
|
args: {
|
||||||
|
name: actionName,
|
||||||
|
definition: def,
|
||||||
|
comment: actionComment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDropActionPermissionQuery = (
|
||||||
|
role: string,
|
||||||
|
actionName: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'drop_action_permission',
|
||||||
|
args: {
|
||||||
|
action: actionName,
|
||||||
|
role,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSetTableEnumQuery = (
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
isEnum: boolean
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'set_table_is_enum',
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
is_enum: isEnum,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTrackTableQuery = (tableDef: TableDefinition) => {
|
||||||
|
return {
|
||||||
|
type: 'add_existing_table_or_view',
|
||||||
|
args: tableDef,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUntrackTableQuery = (tableDef: TableDefinition) => {
|
||||||
|
return {
|
||||||
|
type: 'untrack_table',
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAddComputedFieldQuery = (
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
computedFieldName: string,
|
||||||
|
definition: any, // TODO
|
||||||
|
comment: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'add_computed_field',
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
name: computedFieldName,
|
||||||
|
definition: {
|
||||||
|
...definition,
|
||||||
|
},
|
||||||
|
comment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDropComputedFieldQuery = (
|
||||||
|
tableDef: TableDefinition,
|
||||||
|
computedFieldName: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'drop_computed_field',
|
||||||
|
args: {
|
||||||
|
table: tableDef,
|
||||||
|
name: computedFieldName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeleteQuery = (
|
||||||
|
pkClause: WhereClause,
|
||||||
|
tableName: string,
|
||||||
|
schemaName: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'delete',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: tableName,
|
||||||
|
schema: schemaName,
|
||||||
|
},
|
||||||
|
where: pkClause,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBulkDeleteQuery = (
|
||||||
|
pkClauses: WhereClause,
|
||||||
|
tableName: string,
|
||||||
|
schemaName: string
|
||||||
|
) =>
|
||||||
|
pkClauses.map((pkClause: WhereClause) =>
|
||||||
|
getDeleteQuery(pkClause, tableName, schemaName)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getEnumOptionsQuery = (
|
||||||
|
request: { enumTableName: string; enumColumnName: string },
|
||||||
|
currentSchema: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: request.enumTableName,
|
||||||
|
schema: currentSchema,
|
||||||
|
},
|
||||||
|
columns: [request.enumColumnName],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inconsistentObjectsQuery = {
|
||||||
|
type: 'get_inconsistent_metadata',
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dropInconsistentObjectsQuery = {
|
||||||
|
type: 'drop_inconsistent_metadata',
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getReloadMetadataQuery = (shouldReloadRemoteSchemas: boolean) => ({
|
||||||
|
type: 'reload_metadata',
|
||||||
|
args: {
|
||||||
|
reload_remote_schemas: shouldReloadRemoteSchemas,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getReloadRemoteSchemaCacheQuery = (remoteSchemaName: string) => {
|
||||||
|
return {
|
||||||
|
type: 'reload_remote_schema',
|
||||||
|
args: {
|
||||||
|
name: remoteSchemaName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportMetadataQuery = {
|
||||||
|
type: 'export_metadata',
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// type the metadata
|
||||||
|
export const generateReplaceMetadataQuery = (metadataJson: any) => ({
|
||||||
|
type: 'replace_metadata',
|
||||||
|
args: metadataJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resetMetadataQuery = {
|
||||||
|
type: 'clear_metadata',
|
||||||
|
args: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEventTriggersQuery = {
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: 'event_triggers',
|
||||||
|
schema: 'hdb_catalog',
|
||||||
|
},
|
||||||
|
columns: ['*'],
|
||||||
|
order_by: [{ column: 'name', type: 'asc' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchScheduledTriggersQuery = {
|
||||||
|
type: 'select',
|
||||||
|
args: {
|
||||||
|
table: {
|
||||||
|
name: 'hdb_cron_triggers',
|
||||||
|
schema: 'hdb_catalog',
|
||||||
|
},
|
||||||
|
columns: ['*'],
|
||||||
|
order_by: [{ column: 'name', type: 'asc' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBulkQuery = (args: any[]) => {
|
||||||
|
return {
|
||||||
|
type: 'bulk',
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateCreateEventTriggerQuery = (
|
||||||
|
state: LocalEventTriggerState,
|
||||||
|
replace = false
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type: 'create_event_trigger',
|
||||||
|
args: {
|
||||||
|
name: state.name.trim(),
|
||||||
|
table: state.table,
|
||||||
|
webhook:
|
||||||
|
state.webhook.type === 'static' ? state.webhook.value.trim() : null,
|
||||||
|
webhook_from_env:
|
||||||
|
state.webhook.type === 'env' ? state.webhook.value.trim() : null,
|
||||||
|
insert: state.operations.insert
|
||||||
|
? {
|
||||||
|
columns: '*',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
update: state.operations.update
|
||||||
|
? {
|
||||||
|
columns: state.operationColumns.map(c => c.name),
|
||||||
|
payload: state.operationColumns.map(c => c.name),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
delete: state.operations.delete
|
||||||
|
? {
|
||||||
|
columns: '*',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
enable_manual: state.operations.enable_manual,
|
||||||
|
retry_conf: state.retryConf,
|
||||||
|
headers: transformHeaders(state.headers),
|
||||||
|
replace,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDropEventTriggerQuery = (name: string) => ({
|
||||||
|
type: 'delete_event_trigger',
|
||||||
|
args: {
|
||||||
|
name: name.trim(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateCreateScheduledTriggerQuery = (
|
||||||
|
state: LocalScheduledTriggerState,
|
||||||
|
replace = false
|
||||||
|
) => ({
|
||||||
|
type: 'create_cron_trigger',
|
||||||
|
args: {
|
||||||
|
name: state.name.trim(),
|
||||||
|
webhook: state.webhook,
|
||||||
|
schedule: state.schedule,
|
||||||
|
payload: JSON.parse(state.payload),
|
||||||
|
headers: transformHeaders(state.headers),
|
||||||
|
retry_conf: {
|
||||||
|
num_retries: state.retryConf.num_retries,
|
||||||
|
retry_interval_seconds: state.retryConf.interval_sec,
|
||||||
|
timeout_seconds: state.retryConf.timeout_sec,
|
||||||
|
tolerance_seconds: state.retryConf.tolerance_sec,
|
||||||
|
},
|
||||||
|
comment: state.comment,
|
||||||
|
include_in_metadata: state.includeInMetadata,
|
||||||
|
replace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateUpdateScheduledTriggerQuery = (
|
||||||
|
state: LocalScheduledTriggerState
|
||||||
|
) => generateCreateScheduledTriggerQuery(state, true);
|
||||||
|
|
||||||
|
export const getDropScheduledTriggerQuery = (name: string) => ({
|
||||||
|
type: 'delete_cron_trigger',
|
||||||
|
args: {
|
||||||
|
name: name.trim(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getCreateScheduledEventQuery = (state: LocalAdhocEventState) => {
|
||||||
|
return {
|
||||||
|
type: 'create_scheduled_event',
|
||||||
|
args: {
|
||||||
|
webhook: state.webhook,
|
||||||
|
schedule_at: state.time.toISOString(),
|
||||||
|
headers: transformHeaders(state.headers),
|
||||||
|
retry_conf: {
|
||||||
|
num_retries: state.retryConf.num_retries,
|
||||||
|
retry_interval_seconds: state.retryConf.interval_sec,
|
||||||
|
timeout_seconds: state.retryConf.timeout_sec,
|
||||||
|
tolerance_seconds: state.retryConf.tolerance_sec,
|
||||||
|
},
|
||||||
|
payload: state.payload,
|
||||||
|
comment: state.comment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectColumn = string | { name: string; columns: SelectColumn[] };
|
||||||
|
|
||||||
|
export const getSelectQuery = (
|
||||||
|
type: 'select' | 'count',
|
||||||
|
table: TableDefinition,
|
||||||
|
columns: SelectColumn[],
|
||||||
|
where: Nullable<WhereClause>,
|
||||||
|
offset: Nullable<number>,
|
||||||
|
limit: Nullable<number>,
|
||||||
|
order_by: Nullable<OrderBy[]>
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
args: {
|
||||||
|
table,
|
||||||
|
columns,
|
||||||
|
where,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
order_by,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFetchInvocationLogsQuery = (
|
||||||
|
where: Nullable<WhereClause>,
|
||||||
|
offset: Nullable<number>,
|
||||||
|
order_by: Nullable<OrderBy[]>,
|
||||||
|
limit: Nullable<number>
|
||||||
|
) => {
|
||||||
|
return getSelectQuery(
|
||||||
|
'select',
|
||||||
|
generateTableDef('hdb_scheduled_event_invocation_logs', 'hdb_catalog'),
|
||||||
|
['*'],
|
||||||
|
where,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
order_by
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectQueryGenerator = typeof getFetchInvocationLogsQuery;
|
||||||
|
|
||||||
|
export const getFetchManualTriggersQuery = (tableDef: TableDefinition) =>
|
||||||
|
getSelectQuery(
|
||||||
|
'select',
|
||||||
|
generateTableDef('event_triggers', 'hdb_catalog'),
|
||||||
|
['*'],
|
||||||
|
{
|
||||||
|
table_name: tableDef.name,
|
||||||
|
schema_name: tableDef.schema,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
column: 'name',
|
||||||
|
type: 'asc',
|
||||||
|
nulls: 'last',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRedeliverDataEventQuery = (eventId: string) => ({
|
||||||
|
type: 'redeliver_event',
|
||||||
|
args: {
|
||||||
|
event_id: eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSaveRemoteRelQuery = (
|
||||||
|
args: RemoteRelationshipPayload,
|
||||||
|
isNew: boolean
|
||||||
|
) => ({
|
||||||
|
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDropRemoteRelQuery = (
|
||||||
|
name: string,
|
||||||
|
table: TableDefinition
|
||||||
|
) => ({
|
||||||
|
type: 'delete_remote_relationship',
|
||||||
|
args: {
|
||||||
|
name,
|
||||||
|
table,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getRemoteSchemaIntrospectionQuery = (
|
||||||
|
remoteSchemaName: string
|
||||||
|
) => ({
|
||||||
|
type: 'introspect_remote_schema',
|
||||||
|
args: {
|
||||||
|
name: remoteSchemaName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getConsoleOptsQuery = () =>
|
||||||
|
getSelectQuery(
|
||||||
|
'select',
|
||||||
|
{ name: 'hdb_version', schema: 'hdb_catalog' },
|
||||||
|
['hasura_uuid', 'console_state'],
|
||||||
|
{},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
@ -1,54 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import globals from '../../Globals';
|
|
||||||
|
|
||||||
export class NotFoundError extends Error {}
|
|
||||||
|
|
||||||
class PageNotFound extends Component {
|
|
||||||
render() {
|
|
||||||
const errorImage = `${globals.assetsPath}/common/img/hasura_icon_green.svg`;
|
|
||||||
const styles = require('./ErrorPage.scss');
|
|
||||||
|
|
||||||
const { resetCallback } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.viewContainer}>
|
|
||||||
<Helmet title="404 - Page Not Found | Hasura" />
|
|
||||||
<div className={'container ' + styles.centerContent}>
|
|
||||||
<div className={'row ' + styles.message}>
|
|
||||||
<div className="col-xs-8">
|
|
||||||
<h1>404</h1>
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
This page doesn't exist. Head back{' '}
|
|
||||||
<Link to="/" onClick={resetCallback}>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-xs-4">
|
|
||||||
<img
|
|
||||||
src={errorImage}
|
|
||||||
className="img-responsive"
|
|
||||||
name="hasura"
|
|
||||||
title="We think you are lost!"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PageNotFound.propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
resetCallback: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect()(PageNotFound);
|
|
50
console/src/components/Error/PageNotFound.tsx
Normal file
50
console/src/components/Error/PageNotFound.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import Helmet from 'react-helmet';
|
||||||
|
import globals from '../../Globals';
|
||||||
|
import styles from './ErrorPage.scss';
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {}
|
||||||
|
|
||||||
|
type PageNotFoundProps = {
|
||||||
|
resetCallback: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageNotFound = (props: PageNotFoundProps) => {
|
||||||
|
const errorImage = `${globals.assetsPath}/common/img/hasura_icon_green.svg`;
|
||||||
|
|
||||||
|
const { resetCallback } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.viewContainer}>
|
||||||
|
<Helmet title="404 - Page Not Found | Hasura" />
|
||||||
|
<div className={`container ${styles.centerContent}`}>
|
||||||
|
<div className={`row ${styles.message}`}>
|
||||||
|
<div className="col-xs-8">
|
||||||
|
<h1>404</h1>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
This page does not exist. Head back{' '}
|
||||||
|
<Link to="/" onClick={resetCallback}>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-xs-4">
|
||||||
|
<img
|
||||||
|
src={errorImage}
|
||||||
|
className="img-responsive"
|
||||||
|
title="We think you are lost!"
|
||||||
|
alt="Not found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect()(PageNotFound);
|
@ -739,12 +739,20 @@ class Main extends React.Component {
|
|||||||
tooltips.remoteSchema,
|
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}>
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
class TopicDescription extends React.Component {
|
|
||||||
render() {
|
|
||||||
const Rectangle = require('./images/Rectangle.svg');
|
|
||||||
const styles = require('../../RemoteSchema/RemoteSchema.scss');
|
|
||||||
const { title, imgUrl, imgAlt, description } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.subHeaderText}>
|
|
||||||
<img className={'img-responsive'} src={Rectangle} alt={'Rectangle'} />
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div className={styles.remoteSchemaImg}>
|
|
||||||
<img className={'img-responsive'} src={imgUrl} alt={imgAlt} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.descriptionText + ' ' + styles.wd60}>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TopicDescription.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
imgUrl: PropTypes.string.isRequired,
|
|
||||||
imgAlt: PropTypes.string.isRequired,
|
|
||||||
description: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
export default TopicDescription;
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from '../../RemoteSchema/RemoteSchema.scss';
|
||||||
|
|
||||||
|
const Rectangle = require('./images/Rectangle.svg');
|
||||||
|
|
||||||
|
type TopicDescriptionProps = {
|
||||||
|
title: string;
|
||||||
|
imgUrl: string;
|
||||||
|
imgAlt: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TopicDescription = (props: TopicDescriptionProps) => {
|
||||||
|
const { title, imgUrl, imgAlt, description } = props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.subHeaderText}>
|
||||||
|
<img className="img-responsive" src={Rectangle} alt="Rectangle" />
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={styles.remoteSchemaImg}>
|
||||||
|
<img className="img-responsive" src={imgUrl} alt={imgAlt} />
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.descriptionText} ${styles.wd60}`}>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopicDescription;
|
@ -1,16 +1,63 @@
|
|||||||
import React from 'react';
|
import 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',
|
{
|
||||||
title,
|
level: 'success',
|
||||||
message: message ? message : null,
|
title,
|
||||||
})
|
message,
|
||||||
|
},
|
||||||
|
'success'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const showInfoNotification = title => {
|
const showInfoNotification = (title: string): Thunk => {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification(
|
||||||
title,
|
{
|
||||||
autoDismiss: 0,
|
title,
|
||||||
})
|
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',
|
{
|
||||||
title,
|
level: 'warning',
|
||||||
message,
|
title,
|
||||||
children,
|
message,
|
||||||
})
|
children,
|
||||||
|
},
|
||||||
|
'warning'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -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" />
|
||||||
|
7
console/src/components/Services/Data/Common/Headers.ts
Normal file
7
console/src/components/Services/Data/Common/Headers.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { GetReduxState } from '../../../../types';
|
||||||
|
|
||||||
|
const dataHeaders = (getState: GetReduxState) => {
|
||||||
|
return getState().tables.dataHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dataHeaders;
|
@ -12,7 +12,7 @@ import globals from '../../../../Globals';
|
|||||||
import returnMigrateUrl from '../Common/getMigrateUrl';
|
import 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';
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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> = ({
|
||||||
|
@ -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> = ({
|
||||||
|
@ -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(
|
||||||
|
@ -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),
|
||||||
|
@ -1,414 +0,0 @@
|
|||||||
import defaultState from './AddState';
|
|
||||||
import _push from '../push';
|
|
||||||
import { loadTriggers, makeMigrationCall, setTrigger } from '../EventActions';
|
|
||||||
import { showSuccessNotification } from '../../Common/Notification';
|
|
||||||
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
|
|
||||||
import { updateSchemaInfo } from '../../Data/DataActions';
|
|
||||||
|
|
||||||
const SET_DEFAULTS = 'AddTrigger/SET_DEFAULTS';
|
|
||||||
const SET_TRIGGERNAME = 'AddTrigger/SET_TRIGGERNAME';
|
|
||||||
const SET_TABLENAME = 'AddTrigger/SET_TABLENAME';
|
|
||||||
const SET_SCHEMANAME = 'AddTrigger/SET_SCHEMANAME';
|
|
||||||
const SET_WEBHOOK_URL = 'AddTrigger/SET_WEBHOOK_URL';
|
|
||||||
const SET_RETRY_NUM = 'AddTrigger/SET_RETRY_NUM';
|
|
||||||
const SET_RETRY_INTERVAL = 'AddTrigger/SET_RETRY_INTERVAL';
|
|
||||||
const SET_RETRY_TIMEOUT = 'AddTrigger/SET_RETRY_TIMEOUT';
|
|
||||||
const MAKING_REQUEST = 'AddTrigger/MAKING_REQUEST';
|
|
||||||
const REQUEST_SUCCESS = 'AddTrigger/REQUEST_SUCCESS';
|
|
||||||
const REQUEST_ERROR = 'AddTrigger/REQUEST_ERROR';
|
|
||||||
const VALIDATION_ERROR = 'AddTrigger/VALIDATION_ERROR';
|
|
||||||
const TOGGLE_COLUMNS = 'AddTrigger/TOGGLE_COLUMNS';
|
|
||||||
const TOGGLE_ALL_COLUMNS = 'AddTrigger/TOGGLE_ALL_COLUMNS';
|
|
||||||
const TOGGLE_OPERATION = 'AddTrigger/TOGGLE_OPERATION';
|
|
||||||
const TOGGLE_ENABLE_MANUAL_CONFIG = 'AddTrigger/TOGGLE_ENABLE_MANUAL_CONFIG';
|
|
||||||
// const TOGGLE_QUERY_TYPE_SELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_SELECTED';
|
|
||||||
// const TOGGLE_QUERY_TYPE_DESELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_DESELECTED';
|
|
||||||
const REMOVE_HEADER = 'AddTrigger/REMOVE_HEADER';
|
|
||||||
const SET_HEADERKEY = 'AddTrigger/SET_HEADERKEY';
|
|
||||||
const SET_HEADERTYPE = 'AddTrigger/SET_HEADERTYPE';
|
|
||||||
const SET_HEADERVALUE = 'AddTrigger/SET_HEADERVALUE';
|
|
||||||
const ADD_HEADER = 'AddTrigger/ADD_HEADER';
|
|
||||||
const UPDATE_WEBHOOK_URL_TYPE = 'AddTrigger/UPDATE_WEBHOOK_URL_TYPE';
|
|
||||||
|
|
||||||
const setTriggerName = value => ({ type: SET_TRIGGERNAME, value });
|
|
||||||
const setTableName = value => ({ type: SET_TABLENAME, value });
|
|
||||||
const setSchemaName = value => ({ type: SET_SCHEMANAME, value });
|
|
||||||
const setWebhookURL = value => ({ type: SET_WEBHOOK_URL, value });
|
|
||||||
const setRetryNum = value => ({ type: SET_RETRY_NUM, value });
|
|
||||||
const setRetryInterval = value => ({ type: SET_RETRY_INTERVAL, value });
|
|
||||||
const setRetryTimeout = value => ({ type: SET_RETRY_TIMEOUT, value });
|
|
||||||
const setDefaults = () => ({ type: SET_DEFAULTS });
|
|
||||||
const addHeader = () => ({ type: ADD_HEADER });
|
|
||||||
const removeHeader = i => ({ type: REMOVE_HEADER, index: i });
|
|
||||||
const setHeaderKey = (key, index) => ({
|
|
||||||
type: SET_HEADERKEY,
|
|
||||||
key,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
const setHeaderType = (headerType, index) => ({
|
|
||||||
type: SET_HEADERTYPE,
|
|
||||||
headerType,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
const setHeaderValue = (headerValue, index) => ({
|
|
||||||
type: SET_HEADERVALUE,
|
|
||||||
headerValue,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
|
|
||||||
// General error during validation.
|
|
||||||
// const validationError = (error) => ({type: VALIDATION_ERROR, error: error});
|
|
||||||
const validationError = error => {
|
|
||||||
alert(error);
|
|
||||||
return { type: VALIDATION_ERROR, error };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWebhookKey = (type, val) => {
|
|
||||||
return { [type === 'url' ? 'webhook' : 'webhook_from_env']: val };
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTrigger = () => {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({ type: MAKING_REQUEST });
|
|
||||||
dispatch(showSuccessNotification('Creating Trigger...'));
|
|
||||||
const currentState = getState().addTrigger;
|
|
||||||
const currentSchema = currentState.schemaName;
|
|
||||||
const triggerName = currentState.triggerName;
|
|
||||||
const tableName = currentState.tableName;
|
|
||||||
const webhook = currentState.webhookURL;
|
|
||||||
const webhookType = currentState.webhookUrlType;
|
|
||||||
|
|
||||||
// apply migrations
|
|
||||||
const migrationName = 'create_trigger_' + triggerName.trim();
|
|
||||||
const payload = {
|
|
||||||
type: 'create_event_trigger',
|
|
||||||
args: {
|
|
||||||
name: triggerName,
|
|
||||||
table: { name: tableName, schema: currentSchema },
|
|
||||||
...getWebhookKey(webhookType, webhook),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const downPayload = {
|
|
||||||
type: 'delete_event_trigger',
|
|
||||||
args: {
|
|
||||||
name: triggerName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// operation definition
|
|
||||||
if (currentState.selectedOperations.insert) {
|
|
||||||
payload.args.insert = { columns: currentState.operations.insert };
|
|
||||||
}
|
|
||||||
if ('enableManual' in currentState) {
|
|
||||||
payload.args.enable_manual = currentState.enableManual;
|
|
||||||
}
|
|
||||||
if (currentState.selectedOperations.update) {
|
|
||||||
payload.args.update = { columns: currentState.operations.update };
|
|
||||||
}
|
|
||||||
if (currentState.selectedOperations.delete) {
|
|
||||||
payload.args.delete = { columns: currentState.operations.delete };
|
|
||||||
}
|
|
||||||
// retry logic
|
|
||||||
if (currentState.retryConf) {
|
|
||||||
payload.args.retry_conf = currentState.retryConf;
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.args.retry_conf = {
|
|
||||||
num_retries:
|
|
||||||
currentState.retryConf.num_retries === ''
|
|
||||||
? 0
|
|
||||||
: parseInt(currentState.retryConf.num_retries, 10),
|
|
||||||
interval_sec:
|
|
||||||
currentState.retryConf.interval_sec === ''
|
|
||||||
? 10
|
|
||||||
: parseInt(currentState.retryConf.interval_sec, 10),
|
|
||||||
timeout_sec:
|
|
||||||
currentState.retryConf.timeout_sec === ''
|
|
||||||
? 60
|
|
||||||
: parseInt(currentState.retryConf.timeout_sec, 10),
|
|
||||||
};
|
|
||||||
|
|
||||||
// create header payload
|
|
||||||
const headers = [];
|
|
||||||
currentState.headers.map(header => {
|
|
||||||
if (header.key !== '' && header.type !== '') {
|
|
||||||
if (header.type === 'static') {
|
|
||||||
headers.push({ name: header.key, value: header.value });
|
|
||||||
} else if (header.type === 'env') {
|
|
||||||
headers.push({ name: header.key, value_from_env: header.value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
payload.args.headers = headers;
|
|
||||||
const upQueryArgs = [];
|
|
||||||
upQueryArgs.push(payload);
|
|
||||||
const downQueryArgs = [];
|
|
||||||
downQueryArgs.push(downPayload);
|
|
||||||
const upQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: upQueryArgs,
|
|
||||||
};
|
|
||||||
const downQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: downQueryArgs,
|
|
||||||
};
|
|
||||||
const requestMsg = 'Creating trigger...';
|
|
||||||
const successMsg = 'Trigger Created';
|
|
||||||
const errorMsg = 'Create trigger failed';
|
|
||||||
|
|
||||||
const customOnSuccess = () => {
|
|
||||||
dispatch(setTrigger(triggerName.trim()));
|
|
||||||
dispatch(loadTriggers([triggerName])).then(() => {
|
|
||||||
dispatch(
|
|
||||||
_push('/manage/triggers/' + triggerName.trim() + '/processed')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const customOnError = err => {
|
|
||||||
dispatch({ type: REQUEST_ERROR, data: errorMsg });
|
|
||||||
dispatch({ type: UPDATE_MIGRATION_STATUS_ERROR, data: err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
makeMigrationCall(
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
upQuery.args,
|
|
||||||
downQuery.args,
|
|
||||||
migrationName,
|
|
||||||
customOnSuccess,
|
|
||||||
customOnError,
|
|
||||||
requestMsg,
|
|
||||||
successMsg,
|
|
||||||
errorMsg,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadTableList = schemaName => {
|
|
||||||
return dispatch => dispatch(updateSchemaInfo({ schemas: [schemaName] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const operationToggleColumn = (column, operation) => {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const currentOperations = getState().addTrigger.operations;
|
|
||||||
const currentCols = currentOperations[operation];
|
|
||||||
// check if column is in currentCols. if not, push
|
|
||||||
const isExists = currentCols.includes(column);
|
|
||||||
let finalCols = currentCols;
|
|
||||||
if (isExists) {
|
|
||||||
finalCols = currentCols.filter(col => col !== column);
|
|
||||||
} else {
|
|
||||||
finalCols.push(column);
|
|
||||||
}
|
|
||||||
dispatch({ type: TOGGLE_COLUMNS, cols: finalCols, op: operation });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const operationToggleAllColumns = columns => {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: TOGGLE_ALL_COLUMNS, cols: columns });
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const setOperationSelection = type => {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: TOGGLE_OPERATION, data: type });
|
|
||||||
/*
|
|
||||||
if (isChecked) {
|
|
||||||
dispatch({ type: TOGGLE_QUERY_TYPE_SELECTED, data: type });
|
|
||||||
} else {
|
|
||||||
dispatch({ type: TOGGLE_QUERY_TYPE_DESELECTED, data: type });
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTriggerReducer = (state = defaultState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case ADD_HEADER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [...state.headers, { key: '', type: 'static', value: '' }],
|
|
||||||
};
|
|
||||||
case REMOVE_HEADER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [
|
|
||||||
...state.headers.slice(0, action.index),
|
|
||||||
...state.headers.slice(action.index + 1),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case SET_HEADERKEY:
|
|
||||||
const i = action.index;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [
|
|
||||||
...state.headers.slice(0, i),
|
|
||||||
{ ...state.headers[i], key: action.key },
|
|
||||||
...state.headers.slice(i + 1),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case SET_HEADERTYPE:
|
|
||||||
const ij = action.index;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [
|
|
||||||
...state.headers.slice(0, ij),
|
|
||||||
{ ...state.headers[ij], type: action.headerType },
|
|
||||||
...state.headers.slice(ij + 1),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case SET_HEADERVALUE:
|
|
||||||
const ik = action.index;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [
|
|
||||||
...state.headers.slice(0, ik),
|
|
||||||
{ ...state.headers[ik], value: action.headerValue },
|
|
||||||
...state.headers.slice(ik + 1),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
case SET_DEFAULTS:
|
|
||||||
return {
|
|
||||||
...defaultState,
|
|
||||||
operations: {
|
|
||||||
...defaultState.operations,
|
|
||||||
insert: [],
|
|
||||||
update: [],
|
|
||||||
delete: [],
|
|
||||||
},
|
|
||||||
selectedOperations: {
|
|
||||||
...defaultState.selectedOperations,
|
|
||||||
insert: false,
|
|
||||||
update: false,
|
|
||||||
delete: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case MAKING_REQUEST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ongoingRequest: true,
|
|
||||||
lastError: null,
|
|
||||||
lastSuccess: null,
|
|
||||||
};
|
|
||||||
case REQUEST_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: null,
|
|
||||||
lastSuccess: true,
|
|
||||||
};
|
|
||||||
case REQUEST_ERROR:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: action.data,
|
|
||||||
lastSuccess: null,
|
|
||||||
};
|
|
||||||
case VALIDATION_ERROR:
|
|
||||||
return { ...state, internalError: action.error, lastSuccess: null };
|
|
||||||
case SET_TRIGGERNAME:
|
|
||||||
return { ...state, triggerName: action.value };
|
|
||||||
case SET_WEBHOOK_URL:
|
|
||||||
return { ...state, webhookURL: action.value };
|
|
||||||
case SET_RETRY_NUM:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
num_retries: action.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_RETRY_INTERVAL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
interval_sec: action.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_RETRY_TIMEOUT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
timeout_sec: action.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_TABLENAME:
|
|
||||||
return { ...state, tableName: action.value };
|
|
||||||
case SET_SCHEMANAME:
|
|
||||||
return { ...state, schemaName: action.value };
|
|
||||||
case TOGGLE_COLUMNS:
|
|
||||||
const operations = state.operations;
|
|
||||||
operations[action.op] = action.cols;
|
|
||||||
return { ...state, operations: { ...operations } };
|
|
||||||
case TOGGLE_ALL_COLUMNS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
operations: {
|
|
||||||
insert: '*',
|
|
||||||
delete: '*',
|
|
||||||
update: action.cols,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case TOGGLE_OPERATION:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedOperations: {
|
|
||||||
...state.selectedOperations,
|
|
||||||
[action.data]: !state.selectedOperations[action.data],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
case TOGGLE_ENABLE_MANUAL_CONFIG:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
enableManual: !state.enableManual,
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
case TOGGLE_QUERY_TYPE_SELECTED:
|
|
||||||
const selectedOperations = state.selectedOperations;
|
|
||||||
selectedOperations[action.data] = true;
|
|
||||||
return { ...state, selectedOperations: { ...selectedOperations } };
|
|
||||||
case TOGGLE_QUERY_TYPE_DESELECTED:
|
|
||||||
const deselectedOperations = state.selectedOperations;
|
|
||||||
deselectedOperations[action.data] = false;
|
|
||||||
return { ...state, selectedOperations: { ...deselectedOperations } };
|
|
||||||
*/
|
|
||||||
case UPDATE_WEBHOOK_URL_TYPE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
webhookUrlType: action.data,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default addTriggerReducer;
|
|
||||||
export {
|
|
||||||
addHeader,
|
|
||||||
setHeaderKey,
|
|
||||||
setHeaderValue,
|
|
||||||
setHeaderType,
|
|
||||||
removeHeader,
|
|
||||||
setTriggerName,
|
|
||||||
setTableName,
|
|
||||||
setSchemaName,
|
|
||||||
setWebhookURL,
|
|
||||||
setRetryNum,
|
|
||||||
setRetryInterval,
|
|
||||||
setRetryTimeout,
|
|
||||||
createTrigger,
|
|
||||||
loadTableList,
|
|
||||||
operationToggleColumn,
|
|
||||||
operationToggleAllColumns,
|
|
||||||
setOperationSelection,
|
|
||||||
setDefaults,
|
|
||||||
UPDATE_WEBHOOK_URL_TYPE,
|
|
||||||
TOGGLE_ENABLE_MANUAL_CONFIG,
|
|
||||||
};
|
|
||||||
export { validationError };
|
|
@ -1,26 +0,0 @@
|
|||||||
const defaultState = {
|
|
||||||
triggerName: '',
|
|
||||||
tableName: '',
|
|
||||||
schemaName: 'public',
|
|
||||||
operations: { insert: [], update: [], delete: [] },
|
|
||||||
enableManual: false,
|
|
||||||
selectedOperations: {
|
|
||||||
insert: false,
|
|
||||||
update: false,
|
|
||||||
delete: false,
|
|
||||||
},
|
|
||||||
webhookURL: '',
|
|
||||||
webhookUrlType: 'url',
|
|
||||||
retryConf: {
|
|
||||||
num_retries: 0,
|
|
||||||
interval_sec: 10,
|
|
||||||
timeout_sec: 60,
|
|
||||||
},
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: null,
|
|
||||||
internalError: null,
|
|
||||||
lastSuccess: null,
|
|
||||||
headers: [{ key: '', type: 'static', value: '' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defaultState;
|
|
@ -1,629 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import * as tooltip from './Tooltips';
|
|
||||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
|
||||||
import Button from '../../../Common/Button/Button';
|
|
||||||
import Operations from './Operations';
|
|
||||||
|
|
||||||
import {
|
|
||||||
removeHeader,
|
|
||||||
setHeaderKey,
|
|
||||||
setHeaderValue,
|
|
||||||
setHeaderType,
|
|
||||||
addHeader,
|
|
||||||
setTriggerName,
|
|
||||||
setTableName,
|
|
||||||
setSchemaName,
|
|
||||||
setWebhookURL,
|
|
||||||
setRetryNum,
|
|
||||||
setRetryInterval,
|
|
||||||
setRetryTimeout,
|
|
||||||
operationToggleColumn,
|
|
||||||
operationToggleAllColumns,
|
|
||||||
setOperationSelection,
|
|
||||||
setDefaults,
|
|
||||||
UPDATE_WEBHOOK_URL_TYPE,
|
|
||||||
loadTableList,
|
|
||||||
} from './AddActions';
|
|
||||||
import { listDuplicate } from '../../../../utils/data';
|
|
||||||
import { showErrorNotification } from '../../Common/Notification';
|
|
||||||
import { createTrigger } from './AddActions';
|
|
||||||
|
|
||||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
|
||||||
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
|
|
||||||
import {
|
|
||||||
getOnlyTables,
|
|
||||||
getSchemaName,
|
|
||||||
getSchemaTables,
|
|
||||||
getTableName,
|
|
||||||
getTrackedTables,
|
|
||||||
} from '../../../Common/utils/pgUtils';
|
|
||||||
|
|
||||||
class AddTrigger extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.props.dispatch(loadTableList('public'));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// set defaults
|
|
||||||
this.props.dispatch(setDefaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// set defaults
|
|
||||||
this.props.dispatch(setDefaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWebhookUrlType(e) {
|
|
||||||
const field = e.target.getAttribute('value');
|
|
||||||
if (field === 'env' || field === 'url') {
|
|
||||||
this.props.dispatch({ type: UPDATE_WEBHOOK_URL_TYPE, data: field });
|
|
||||||
this.props.dispatch(setWebhookURL(''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
submitValidation(e) {
|
|
||||||
// validations
|
|
||||||
e.preventDefault();
|
|
||||||
let isValid = true;
|
|
||||||
let errorMsg = '';
|
|
||||||
let customMsg = '';
|
|
||||||
if (this.props.triggerName === '') {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Trigger name cannot be empty';
|
|
||||||
customMsg = 'Trigger name cannot be empty. Please add a name';
|
|
||||||
} else if (!this.props.tableName) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Table cannot be empty';
|
|
||||||
customMsg = 'Please select a table name';
|
|
||||||
} else if (this.props.webhookURL === '') {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Webhook URL cannot be empty';
|
|
||||||
customMsg = 'Webhook URL cannot be empty. Please add a valid URL';
|
|
||||||
} else if (this.props.retryConf) {
|
|
||||||
const iNumRetries =
|
|
||||||
this.props.retryConf.num_retries === ''
|
|
||||||
? 0
|
|
||||||
: parseInt(this.props.retryConf.num_retries, 10);
|
|
||||||
const iRetryInterval =
|
|
||||||
this.props.retryConf.interval_sec === ''
|
|
||||||
? 10
|
|
||||||
: parseInt(this.props.retryConf.interval_sec, 10);
|
|
||||||
const iTimeout =
|
|
||||||
this.props.retryConf.timeout_sec === ''
|
|
||||||
? 60
|
|
||||||
: parseInt(this.props.retryConf.timeout_sec, 10);
|
|
||||||
|
|
||||||
if (iNumRetries < 0 || isNaN(iNumRetries)) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Number of retries is not valid';
|
|
||||||
customMsg = 'Numer of retries must be a non-negative number';
|
|
||||||
}
|
|
||||||
if (iRetryInterval <= 0 || isNaN(iRetryInterval)) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Retry interval is not valid';
|
|
||||||
customMsg = 'Retry interval must be a postiive number';
|
|
||||||
}
|
|
||||||
if (isNaN(iTimeout) || iTimeout <= 0) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Timeout is not valid';
|
|
||||||
customMsg = 'Timeout must be a positive number';
|
|
||||||
}
|
|
||||||
} else if (this.props.selectedOperations.insert) {
|
|
||||||
// check if columns are selected.
|
|
||||||
if (this.props.operations.insert.length === 0) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'No columns selected for insert operation';
|
|
||||||
customMsg =
|
|
||||||
'Please select a minimum of one column for insert operation';
|
|
||||||
}
|
|
||||||
} else if (this.props.selectedOperations.update) {
|
|
||||||
// check if columns are selected.
|
|
||||||
if (this.props.operations.update.length === 0) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'No columns selected for update operation';
|
|
||||||
customMsg =
|
|
||||||
'Please select a minimum of one column for update operation';
|
|
||||||
}
|
|
||||||
} else if (this.props.headers.length === 1) {
|
|
||||||
if (this.props.headers[0].key !== '') {
|
|
||||||
// let the default value through and ignore it while querying?
|
|
||||||
// Need a better method
|
|
||||||
if (this.props.headers[0].type === '') {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'No type selected for trigger header';
|
|
||||||
customMsg = 'Please select a type for the trigger header';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.props.headers.length > 1) {
|
|
||||||
// repitition check
|
|
||||||
const repeatList = listDuplicate(
|
|
||||||
this.props.headers.map(header => header.key)
|
|
||||||
);
|
|
||||||
if (repeatList.length > 0) {
|
|
||||||
isValid = false;
|
|
||||||
errorMsg = 'Duplicate entries in trigger headers';
|
|
||||||
customMsg = `You have the following column names repeated: [${repeatList}]`;
|
|
||||||
}
|
|
||||||
// Check for empty header keys and key/value validation?
|
|
||||||
}
|
|
||||||
if (isValid) {
|
|
||||||
this.props.dispatch(createTrigger());
|
|
||||||
} else {
|
|
||||||
this.props.dispatch(
|
|
||||||
showErrorNotification('Error creating trigger!', errorMsg, {
|
|
||||||
custom: customMsg,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tableName,
|
|
||||||
allSchemas,
|
|
||||||
schemaName,
|
|
||||||
schemaList,
|
|
||||||
selectedOperations,
|
|
||||||
operations,
|
|
||||||
dispatch,
|
|
||||||
ongoingRequest,
|
|
||||||
lastError,
|
|
||||||
lastSuccess,
|
|
||||||
internalError,
|
|
||||||
headers,
|
|
||||||
webhookURL,
|
|
||||||
webhookUrlType,
|
|
||||||
enableManual,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const styles = require('../TableCommon/EventTable.scss');
|
|
||||||
|
|
||||||
let createBtnText = 'Create Event Trigger';
|
|
||||||
if (ongoingRequest) {
|
|
||||||
createBtnText = 'Creating...';
|
|
||||||
} else if (lastError) {
|
|
||||||
createBtnText = 'Creating Failed. Try again';
|
|
||||||
} else if (internalError) {
|
|
||||||
createBtnText = 'Creating Failed. Try again';
|
|
||||||
} else if (lastSuccess) {
|
|
||||||
createBtnText = 'Created! Redirecting...';
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOperationSelection = e => {
|
|
||||||
dispatch(setOperationSelection(e.target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTableList = e => {
|
|
||||||
const selectedSchemaName = e.target.value;
|
|
||||||
dispatch(setSchemaName(selectedSchemaName));
|
|
||||||
dispatch(loadTableList(selectedSchemaName));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTableSelection = e => {
|
|
||||||
const selectedTableName = e.target.value;
|
|
||||||
dispatch(setTableName(selectedTableName));
|
|
||||||
const tableSchema = allSchemas.find(
|
|
||||||
t => t.table_name === selectedTableName && t.table_schema === schemaName
|
|
||||||
);
|
|
||||||
const columns = [];
|
|
||||||
if (tableSchema) {
|
|
||||||
tableSchema.columns.map(colObj => {
|
|
||||||
const column = colObj.column_name;
|
|
||||||
columns.push(column);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dispatch(operationToggleAllColumns(columns));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getColumnList = type => {
|
|
||||||
const dispatchToggleColumn = e => {
|
|
||||||
const column = e.target.value;
|
|
||||||
dispatch(operationToggleColumn(column, type));
|
|
||||||
};
|
|
||||||
const tableSchema = allSchemas.find(
|
|
||||||
t => t.table_name === tableName && t.table_schema === schemaName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tableSchema) {
|
|
||||||
return <i>Select a table first to get column list</i>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableSchema.columns.map((colObj, i) => {
|
|
||||||
const column = colObj.column_name;
|
|
||||||
const columnDataType = colObj.udt_name;
|
|
||||||
const checked = operations[type]
|
|
||||||
? operations[type].includes(column)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const isDisabled = false;
|
|
||||||
const inputHtml = (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={checked}
|
|
||||||
value={column}
|
|
||||||
onChange={dispatchToggleColumn}
|
|
||||||
disabled={isDisabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`${styles.padd_remove} ${styles.columnListElement}`}
|
|
||||||
>
|
|
||||||
<div className={'checkbox '}>
|
|
||||||
<label>
|
|
||||||
{inputHtml}
|
|
||||||
{column}
|
|
||||||
<small> ({columnDataType})</small>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackedSchemaTables = getOnlyTables(
|
|
||||||
getTrackedTables(getSchemaTables(allSchemas, schemaName))
|
|
||||||
);
|
|
||||||
|
|
||||||
const advancedColumnSection = (
|
|
||||||
<div>
|
|
||||||
<h4 className={styles.subheading_text}>
|
|
||||||
Listen columns for update
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.advancedOperationDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>{' '}
|
|
||||||
</h4>
|
|
||||||
{selectedOperations.update ? (
|
|
||||||
<div className={styles.clear_fix + ' ' + styles.listenColumnWrapper}>
|
|
||||||
{getColumnList('update')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.clear_fix + ' ' + styles.listenColumnWrapper}>
|
|
||||||
<i>Applicable only if update operation is selected.</i>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const headersList = headers.map((header, i) => {
|
|
||||||
let removeIcon;
|
|
||||||
if (i + 1 === headers.length) {
|
|
||||||
removeIcon = <i className={`${styles.fontAwosomeClose}`} />;
|
|
||||||
} else {
|
|
||||||
removeIcon = (
|
|
||||||
<i
|
|
||||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(removeHeader(i));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div key={i} className={`${styles.display_flex} form-group`}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`${styles.input} form-control ${styles.add_mar_right}`}
|
|
||||||
value={header.key}
|
|
||||||
placeholder="key"
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setHeaderKey(e.target.value, i));
|
|
||||||
}}
|
|
||||||
data-test={`header-${i}`}
|
|
||||||
/>
|
|
||||||
<div className={styles.dropDownGroup}>
|
|
||||||
<DropdownButton
|
|
||||||
dropdownOptions={[
|
|
||||||
{ display_text: 'Value', value: 'static' },
|
|
||||||
{ display_text: 'From env var', value: 'env' },
|
|
||||||
]}
|
|
||||||
title={
|
|
||||||
(header.type === 'static' && 'Value') ||
|
|
||||||
(header.type === 'env' && 'From env var') ||
|
|
||||||
'Value'
|
|
||||||
}
|
|
||||||
dataKey={
|
|
||||||
(header.type === 'static' && 'static') ||
|
|
||||||
(header.type === 'env' && 'env')
|
|
||||||
}
|
|
||||||
title={header.type === 'env' ? 'From env var' : 'Value'}
|
|
||||||
dataKey={header.type === 'env' ? 'env' : 'static'}
|
|
||||||
onButtonChange={e => {
|
|
||||||
dispatch(setHeaderType(e.target.getAttribute('value'), i));
|
|
||||||
}}
|
|
||||||
onInputChange={e => {
|
|
||||||
dispatch(setHeaderValue(e.target.value, i));
|
|
||||||
if (i + 1 === headers.length) {
|
|
||||||
dispatch(addHeader());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
bsClass={styles.dropdown_button}
|
|
||||||
inputVal={header.value}
|
|
||||||
id={`header-value-${i}`}
|
|
||||||
inputPlaceHolder={
|
|
||||||
header.type === 'env' ? 'HEADER_FROM_ENV' : 'value'
|
|
||||||
}
|
|
||||||
testId={`header-value-${i}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>{removeIcon}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${styles.addTablesBody} ${styles.clear_fix} ${styles.padd_left}`}
|
|
||||||
>
|
|
||||||
<Helmet title="Create Trigger - Events | Hasura" />
|
|
||||||
<div className={styles.subHeader}>
|
|
||||||
<h2 className={styles.heading_text}>Create a new event trigger</h2>
|
|
||||||
<div className="clearfix" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div className={`container-fluid ${styles.padd_left_remove}`}>
|
|
||||||
<form onSubmit={this.submitValidation.bind(this)}>
|
|
||||||
<div
|
|
||||||
className={`${styles.addCol} col-xs-12 ${styles.padd_left_remove}`}
|
|
||||||
>
|
|
||||||
<h4 className={styles.subheading_text}>
|
|
||||||
Trigger Name
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.triggerNameDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>{' '}
|
|
||||||
</h4>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
data-test="trigger-name"
|
|
||||||
placeholder="trigger_name"
|
|
||||||
required
|
|
||||||
pattern="^[A-Za-z]+[A-Za-z0-9_\\-]*$"
|
|
||||||
className={`${styles.tableNameInput} form-control`}
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setTriggerName(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<h4 className={styles.subheading_text}>
|
|
||||||
Schema/Table
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.postgresDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>{' '}
|
|
||||||
</h4>
|
|
||||||
<select
|
|
||||||
onChange={updateTableList}
|
|
||||||
data-test="select-schema"
|
|
||||||
className={styles.selectTrigger + ' form-control'}
|
|
||||||
>
|
|
||||||
{schemaList.map(s => {
|
|
||||||
const sName = getSchemaName(s);
|
|
||||||
return (
|
|
||||||
<option
|
|
||||||
value={sName}
|
|
||||||
key={sName}
|
|
||||||
selected={sName === schemaName}
|
|
||||||
>
|
|
||||||
{sName}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
onChange={updateTableSelection}
|
|
||||||
data-test="select-table"
|
|
||||||
required
|
|
||||||
className={
|
|
||||||
styles.selectTrigger + ' form-control ' + styles.add_mar_left
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="">Select table</option>
|
|
||||||
{trackedSchemaTables.map(t => {
|
|
||||||
const tName = getTableName(t);
|
|
||||||
return (
|
|
||||||
<option key={tName} value={tName}>
|
|
||||||
{tName}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
<hr />
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
styles.add_mar_bottom + ' ' + styles.selectOperations
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Operations
|
|
||||||
dispatch={dispatch}
|
|
||||||
enableManual={enableManual}
|
|
||||||
selectedOperations={selectedOperations}
|
|
||||||
handleOperationSelection={handleOperationSelection}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div className={styles.add_mar_bottom}>
|
|
||||||
<h4 className={styles.subheading_text}>
|
|
||||||
Webhook URL
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.webhookUrlDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>{' '}
|
|
||||||
</h4>
|
|
||||||
<div>
|
|
||||||
<div className={styles.dropdown_wrapper}>
|
|
||||||
<DropdownButton
|
|
||||||
dropdownOptions={[
|
|
||||||
{ display_text: 'URL', value: 'url' },
|
|
||||||
{ display_text: 'From env var', value: 'env' },
|
|
||||||
]}
|
|
||||||
title={
|
|
||||||
(webhookUrlType === 'url' && 'URL') ||
|
|
||||||
(webhookUrlType === 'env' && 'From env var') ||
|
|
||||||
'Value'
|
|
||||||
}
|
|
||||||
dataKey={
|
|
||||||
(webhookUrlType === 'url' && 'url') ||
|
|
||||||
(webhookUrlType === 'env' && 'env')
|
|
||||||
}
|
|
||||||
onButtonChange={this.updateWebhookUrlType.bind(this)}
|
|
||||||
onInputChange={e => {
|
|
||||||
dispatch(setWebhookURL(e.target.value));
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
bsClass={styles.dropdown_button}
|
|
||||||
inputVal={webhookURL}
|
|
||||||
id="webhook-url"
|
|
||||||
inputPlaceHolder={
|
|
||||||
(webhookUrlType === 'url' &&
|
|
||||||
'http://httpbin.org/post') ||
|
|
||||||
(webhookUrlType === 'env' && 'MY_WEBHOOK_URL')
|
|
||||||
}
|
|
||||||
testId="webhook"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
Note: Specifying the webhook URL via an environmental variable
|
|
||||||
is recommended if you have different URLs for multiple
|
|
||||||
environments.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<CollapsibleToggle
|
|
||||||
title={
|
|
||||||
<h4 className={styles.subheading_text}>Advanced Settings</h4>
|
|
||||||
}
|
|
||||||
testId="advanced-settings"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{advancedColumnSection}
|
|
||||||
<hr />
|
|
||||||
<div className={styles.add_mar_top}>
|
|
||||||
<h4 className={styles.subheading_text}>Retry Logic</h4>
|
|
||||||
<div className={styles.retrySection}>
|
|
||||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
|
||||||
<label
|
|
||||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
|
||||||
>
|
|
||||||
Number of retries (default: 0)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
|
||||||
<input
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setRetryNum(e.target.value));
|
|
||||||
}}
|
|
||||||
data-test="no-of-retries"
|
|
||||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="no of retries"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.retrySection}>
|
|
||||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
|
||||||
<label
|
|
||||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
|
||||||
>
|
|
||||||
Retry Interval in seconds (default: 10)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
|
||||||
<input
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setRetryInterval(e.target.value));
|
|
||||||
}}
|
|
||||||
data-test="interval-seconds"
|
|
||||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="interval time in seconds"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.retrySection}>
|
|
||||||
<div className={`col-md-3 ${styles.padd_left_remove}`}>
|
|
||||||
<label
|
|
||||||
className={`${styles.add_mar_right} ${styles.retryLabel}`}
|
|
||||||
>
|
|
||||||
Timeout in seconds (default: 60)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={`col-md-6 ${styles.padd_left_remove}`}>
|
|
||||||
<input
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setRetryTimeout(e.target.value));
|
|
||||||
}}
|
|
||||||
data-test="timeout-seconds"
|
|
||||||
className={`${styles.display_inline} form-control ${styles.width300}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="timeout in seconds"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<div className={styles.add_mar_top}>
|
|
||||||
<h4 className={styles.subheading_text}>Headers</h4>
|
|
||||||
{headersList}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CollapsibleToggle>
|
|
||||||
<hr />
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
color="yellow"
|
|
||||||
size="sm"
|
|
||||||
data-test="trigger-create"
|
|
||||||
>
|
|
||||||
{createBtnText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddTrigger.propTypes = {
|
|
||||||
triggerName: PropTypes.string,
|
|
||||||
tableName: PropTypes.string,
|
|
||||||
schemaName: PropTypes.string,
|
|
||||||
schemaList: PropTypes.array,
|
|
||||||
allSchemas: PropTypes.array.isRequired,
|
|
||||||
selectedOperations: PropTypes.object,
|
|
||||||
operations: PropTypes.object,
|
|
||||||
ongoingRequest: PropTypes.bool.isRequired,
|
|
||||||
lastError: PropTypes.object,
|
|
||||||
internalError: PropTypes.string,
|
|
||||||
lastSuccess: PropTypes.bool,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
...state.addTrigger,
|
|
||||||
schemaList: state.tables.schemaList,
|
|
||||||
allSchemas: state.tables.allSchemas,
|
|
||||||
serverVersion: state.main.serverVersion ? state.main.serverVersion : '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTriggerConnector = connect => connect(mapStateToProps)(AddTrigger);
|
|
||||||
|
|
||||||
export default addTriggerConnector;
|
|
@ -1,118 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
|
||||||
import * as tooltip from './Tooltips';
|
|
||||||
import { TOGGLE_ENABLE_MANUAL_CONFIG } from './AddActions';
|
|
||||||
import KnowMoreLink from '../../../Common/KnowMoreLink/KnowMoreLink';
|
|
||||||
|
|
||||||
const Operations = ({
|
|
||||||
enableManual,
|
|
||||||
selectedOperations,
|
|
||||||
handleOperationSelection,
|
|
||||||
dispatch,
|
|
||||||
}) => {
|
|
||||||
const styles = require('../TableCommon/EventTable.scss');
|
|
||||||
|
|
||||||
const databaseOperations = [
|
|
||||||
{
|
|
||||||
name: 'insert',
|
|
||||||
testIdentifier: 'insert-operation',
|
|
||||||
isChecked: selectedOperations.insert,
|
|
||||||
onChange: handleOperationSelection,
|
|
||||||
displayName: 'Insert',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'update',
|
|
||||||
testIdentifier: 'update-operation',
|
|
||||||
isChecked: selectedOperations.update,
|
|
||||||
onChange: handleOperationSelection,
|
|
||||||
displayName: 'Update',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
testIdentifier: 'delete-operation',
|
|
||||||
isChecked: selectedOperations.delete,
|
|
||||||
onChange: handleOperationSelection,
|
|
||||||
displayName: 'Delete',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getManualInvokeOperation = () => {
|
|
||||||
const handleManualOperationSelection = () => {
|
|
||||||
dispatch({ type: TOGGLE_ENABLE_MANUAL_CONFIG });
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'enable_manual',
|
|
||||||
testIdentifier: 'enable-manual-operation',
|
|
||||||
isChecked: enableManual,
|
|
||||||
onChange: handleManualOperationSelection,
|
|
||||||
displayName: (
|
|
||||||
<span>
|
|
||||||
Via console
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.manualOperationsDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>
|
|
||||||
|
|
||||||
<KnowMoreLink href="https://hasura.io/docs/1.0/graphql/manual/event-triggers/invoke-trigger-console.html" />
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOperationsList = () => {
|
|
||||||
const manualOperation = getManualInvokeOperation();
|
|
||||||
|
|
||||||
const allOperations = databaseOperations;
|
|
||||||
if (manualOperation) {
|
|
||||||
allOperations.push(manualOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return allOperations.map((o, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`${styles.display_inline} ${styles.add_mar_right}`}
|
|
||||||
>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
onChange={o.onChange}
|
|
||||||
data-test={o.testIdentifier}
|
|
||||||
className={`${styles.display_inline} ${styles.add_mar_right}`}
|
|
||||||
type="checkbox"
|
|
||||||
value={o.name}
|
|
||||||
checked={o.isChecked}
|
|
||||||
/>
|
|
||||||
{o.displayName}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.add_mar_bottom + ' ' + styles.selectOperations}>
|
|
||||||
<h4 className={styles.subheading_text}>
|
|
||||||
Trigger Operations
|
|
||||||
<OverlayTrigger
|
|
||||||
placement="right"
|
|
||||||
overlay={tooltip.operationsDescription}
|
|
||||||
>
|
|
||||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
|
||||||
</OverlayTrigger>{' '}
|
|
||||||
</h4>
|
|
||||||
<div className={styles.add_mar_left_small}>{getOperationsList()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Operations.propTypes = {
|
|
||||||
enableManual: PropTypes.bool.isRequired,
|
|
||||||
selectedOperations: PropTypes.object.isRequired,
|
|
||||||
handleOperationSelection: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Operations;
|
|
@ -1,4 +0,0 @@
|
|||||||
const dataHeaders = currentState => {
|
|
||||||
return currentState().tables.dataHeaders;
|
|
||||||
};
|
|
||||||
export default dataHeaders;
|
|
@ -1,8 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
|
||||||
|
|
||||||
export const statusCodeDescription = (
|
|
||||||
<Tooltip id="tooltip-trigger-status-code-description">
|
|
||||||
Status code of the webhook response
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
@ -1,651 +0,0 @@
|
|||||||
import Endpoints, { globalCookiePolicy } from '../../../Endpoints';
|
|
||||||
import requestAction from '../../../utils/requestAction';
|
|
||||||
import defaultState from './EventState';
|
|
||||||
import processedEventsReducer from './ProcessedEvents/ViewActions';
|
|
||||||
import pendingEventsReducer from './PendingEvents/ViewActions';
|
|
||||||
import runningEventsReducer from './RunningEvents/ViewActions';
|
|
||||||
import streamingLogsReducer from './StreamingLogs/LogActions';
|
|
||||||
import {
|
|
||||||
showSuccessNotification,
|
|
||||||
showErrorNotification,
|
|
||||||
} from '../Common/Notification';
|
|
||||||
import dataHeaders from './Common/Headers';
|
|
||||||
import { loadMigrationStatus } from '../../Main/Actions';
|
|
||||||
import returnMigrateUrl from './Common/getMigrateUrl';
|
|
||||||
import globals from '../../../Globals';
|
|
||||||
import push from './push';
|
|
||||||
import { loadInconsistentObjects } from '../Settings/Actions';
|
|
||||||
import { filterInconsistentMetadataObjects } from '../Settings/utils';
|
|
||||||
import { replace } from 'react-router-redux';
|
|
||||||
import { getEventTriggersQuery } from './utils';
|
|
||||||
|
|
||||||
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
|
|
||||||
import { REQUEST_COMPLETE, REQUEST_ONGOING } from './Modify/Actions';
|
|
||||||
|
|
||||||
const SET_TRIGGER = 'Event/SET_TRIGGER';
|
|
||||||
const LOAD_TRIGGER_LIST = 'Event/LOAD_TRIGGER_LIST';
|
|
||||||
const LOAD_PROCESSED_EVENTS = 'Event/LOAD_PROCESSED_EVENTS';
|
|
||||||
const LOAD_PENDING_EVENTS = 'Event/LOAD_PENDING_EVENTS';
|
|
||||||
const LOAD_RUNNING_EVENTS = 'Event/LOAD_RUNNING_EVENTS';
|
|
||||||
const ADMIN_SECRET_ERROR = 'Event/ADMIN_SECRET_ERROR';
|
|
||||||
const UPDATE_DATA_HEADERS = 'Event/UPDATE_DATA_HEADERS';
|
|
||||||
const LISTING_TRIGGER = 'Event/LISTING_TRIGGER';
|
|
||||||
const LOAD_EVENT_LOGS = 'Event/LOAD_EVENT_LOGS';
|
|
||||||
const MODAL_OPEN = 'Event/MODAL_OPEN';
|
|
||||||
const SET_REDELIVER_EVENT = 'Event/SET_REDELIVER_EVENT';
|
|
||||||
const LOAD_EVENT_INVOCATIONS = 'Event/LOAD_EVENT_INVOCATIONS';
|
|
||||||
const REDELIVER_EVENT_SUCCESS = 'Event/REDELIVER_EVENT_SUCCESS';
|
|
||||||
const REDELIVER_EVENT_FAILURE = 'Event/REDELIVER_EVENT_FAILURE';
|
|
||||||
|
|
||||||
const MAKE_REQUEST = 'Event/MAKE_REQUEST';
|
|
||||||
const REQUEST_SUCCESS = 'Event/REQUEST_SUCCESS';
|
|
||||||
const REQUEST_ERROR = 'Event/REQUEST_ERROR';
|
|
||||||
|
|
||||||
/* ************ action creators *********************** */
|
|
||||||
const loadTriggers = triggerNames => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const body = getEventTriggersQuery(triggerNames);
|
|
||||||
const options = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
|
||||||
data => {
|
|
||||||
if (data.result_type !== 'TuplesOk') {
|
|
||||||
console.error('Failed to event trigger info' + JSON.stringify(data[1]));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let triggerData = JSON.parse(data.result[1]);
|
|
||||||
if (triggerNames.length !== 0) {
|
|
||||||
// getExisting state
|
|
||||||
const existingTriggers = getState().triggers.triggerList.filter(
|
|
||||||
trigger => triggerNames.some(item => item !== trigger.name)
|
|
||||||
);
|
|
||||||
const triggerLists = existingTriggers.concat(triggerData);
|
|
||||||
triggerData = triggerLists.sort((a, b) => {
|
|
||||||
return a.name === b.name ? 0 : +(a.name > b.name) || -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// hydrate undefined config values
|
|
||||||
triggerData.forEach(trigger => {
|
|
||||||
if (!trigger.configuration.headers) {
|
|
||||||
trigger.configuration.headers = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { inconsistentObjects } = getState().metadata;
|
|
||||||
let consistentTriggers = triggerData;
|
|
||||||
if (inconsistentObjects.length > 1) {
|
|
||||||
consistentTriggers = filterInconsistentMetadataObjects(
|
|
||||||
triggerData,
|
|
||||||
inconsistentObjects,
|
|
||||||
'events'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: LOAD_TRIGGER_LIST,
|
|
||||||
triggerList: consistentTriggers,
|
|
||||||
});
|
|
||||||
dispatch(loadInconsistentObjects({ shouldReloadMetadata: false }));
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadPendingEvents = () => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const body = {
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_triggers',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'events',
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
|
||||||
],
|
|
||||||
where: { delivered: false, error: false, tries: 0, archived: false },
|
|
||||||
order_by: ['-created_at'],
|
|
||||||
limit: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
|
||||||
data => {
|
|
||||||
dispatch({ type: LOAD_PENDING_EVENTS, data: data });
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadRunningEvents = () => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const body = {
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_triggers',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'events',
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
|
||||||
],
|
|
||||||
where: {
|
|
||||||
delivered: false,
|
|
||||||
error: false,
|
|
||||||
tries: { $gt: 0 },
|
|
||||||
archived: false,
|
|
||||||
},
|
|
||||||
order_by: ['-created_at'],
|
|
||||||
limit: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
|
||||||
data => {
|
|
||||||
dispatch({ type: LOAD_RUNNING_EVENTS, data: data });
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadEventLogs = triggerName => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const triggerOptions = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_triggers',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: ['*'],
|
|
||||||
where: {
|
|
||||||
name: triggerName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, triggerOptions)).then(
|
|
||||||
triggerData => {
|
|
||||||
if (triggerData.length !== 0) {
|
|
||||||
const body = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_invocation_logs',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'event',
|
|
||||||
columns: ['*'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
where: {
|
|
||||||
event: { trigger_name: triggerData[0].name, archived: false },
|
|
||||||
},
|
|
||||||
order_by: ['-created_at'],
|
|
||||||
limit: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const logOptions = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
};
|
|
||||||
dispatch(requestAction(url, logOptions)).then(
|
|
||||||
logsData => {
|
|
||||||
dispatch({ type: LOAD_EVENT_LOGS, data: logsData[0] });
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error(
|
|
||||||
'Failed to load trigger logs' + JSON.stringify(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(replace('/404'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error(
|
|
||||||
'Failed to fetch trigger information' + JSON.stringify(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadEventInvocations = eventId => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const options = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'event_invocation_logs',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'event',
|
|
||||||
columns: ['*'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
where: { event_id: eventId },
|
|
||||||
order_by: ['-created_at'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
|
||||||
data => {
|
|
||||||
dispatch({ type: LOAD_EVENT_INVOCATIONS, data: data });
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const redeliverEvent = eventId => (dispatch, getState) => {
|
|
||||||
const url = Endpoints.getSchema;
|
|
||||||
const options = {
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
method: 'POST',
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: 'redeliver_event',
|
|
||||||
args: {
|
|
||||||
event_id: eventId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
|
||||||
data => {
|
|
||||||
dispatch({ type: REDELIVER_EVENT_SUCCESS, data: data });
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
|
||||||
dispatch({ type: REDELIVER_EVENT_FAILURE, data: error });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTrigger = triggerName => ({ type: SET_TRIGGER, triggerName });
|
|
||||||
|
|
||||||
const setRedeliverEvent = eventId => dispatch => {
|
|
||||||
/*
|
|
||||||
Redeliver event and mark the redeliverEventId to the redelivered event so that it can be tracked.
|
|
||||||
*/
|
|
||||||
return dispatch(redeliverEvent(eventId)).then(() => {
|
|
||||||
return Promise.all([
|
|
||||||
dispatch({ type: SET_REDELIVER_EVENT, eventId }),
|
|
||||||
dispatch(loadEventInvocations(eventId)),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/* **********Shared functions between table actions********* */
|
|
||||||
|
|
||||||
const handleMigrationErrors = (title, errorMsg) => dispatch => {
|
|
||||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
|
||||||
// handle errors for run_sql based workflow
|
|
||||||
dispatch(showErrorNotification(title, errorMsg.code, errorMsg));
|
|
||||||
} else if (errorMsg.code === 'migration_failed') {
|
|
||||||
dispatch(showErrorNotification(title, 'Migration Failed', errorMsg));
|
|
||||||
} else if (errorMsg.code === 'data_api_error') {
|
|
||||||
const parsedErrorMsg = errorMsg;
|
|
||||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
|
||||||
dispatch(
|
|
||||||
showErrorNotification(title, parsedErrorMsg.message.error, parsedErrorMsg)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// any other unhandled codes
|
|
||||||
const parsedErrorMsg = errorMsg;
|
|
||||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
|
||||||
dispatch(showErrorNotification(title, errorMsg.code, parsedErrorMsg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMigrationCall = (
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
upQueries,
|
|
||||||
downQueries,
|
|
||||||
migrationName,
|
|
||||||
customOnSuccess,
|
|
||||||
customOnError,
|
|
||||||
requestMsg,
|
|
||||||
successMsg,
|
|
||||||
errorMsg
|
|
||||||
) => {
|
|
||||||
const upQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: upQueries,
|
|
||||||
};
|
|
||||||
|
|
||||||
const downQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: downQueries,
|
|
||||||
};
|
|
||||||
|
|
||||||
const migrationBody = {
|
|
||||||
name: migrationName,
|
|
||||||
up: upQuery.args,
|
|
||||||
down: downQuery.args,
|
|
||||||
};
|
|
||||||
|
|
||||||
const currMigrationMode = getState().main.migrationMode;
|
|
||||||
|
|
||||||
const migrateUrl = returnMigrateUrl(currMigrationMode);
|
|
||||||
|
|
||||||
let finalReqBody;
|
|
||||||
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
|
|
||||||
finalReqBody = upQuery;
|
|
||||||
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
|
|
||||||
finalReqBody = migrationBody;
|
|
||||||
}
|
|
||||||
const url = migrateUrl;
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: globalCookiePolicy,
|
|
||||||
headers: dataHeaders(getState),
|
|
||||||
body: JSON.stringify(finalReqBody),
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSuccess = () => {
|
|
||||||
if (globals.consoleMode === CLI_CONSOLE_MODE) {
|
|
||||||
dispatch(loadMigrationStatus()); // don't call for server mode
|
|
||||||
}
|
|
||||||
customOnSuccess();
|
|
||||||
if (successMsg) {
|
|
||||||
dispatch(showSuccessNotification(successMsg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onError = err => {
|
|
||||||
customOnError(err);
|
|
||||||
dispatch(handleMigrationErrors(errorMsg, err));
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch({ type: MAKE_REQUEST });
|
|
||||||
dispatch(showSuccessNotification(requestMsg));
|
|
||||||
dispatch(requestAction(url, options, REQUEST_SUCCESS, REQUEST_ERROR)).then(
|
|
||||||
onSuccess,
|
|
||||||
onError
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTrigger = triggerName => {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch(showSuccessNotification('Deleting Trigger...'));
|
|
||||||
const currentTriggerInfo = getState().triggers.triggerList.filter(
|
|
||||||
t => t.name === triggerName
|
|
||||||
)[0];
|
|
||||||
// apply migrations
|
|
||||||
const migrationName = 'delete_trigger_' + triggerName.trim();
|
|
||||||
const payload = {
|
|
||||||
type: 'delete_event_trigger',
|
|
||||||
args: {
|
|
||||||
name: triggerName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const downPayload = {
|
|
||||||
type: 'create_event_trigger',
|
|
||||||
args: {
|
|
||||||
name: triggerName,
|
|
||||||
table: {
|
|
||||||
name: currentTriggerInfo.table_name,
|
|
||||||
schema: currentTriggerInfo.table_schema,
|
|
||||||
},
|
|
||||||
retry_conf: { ...currentTriggerInfo.configuration.retry_conf },
|
|
||||||
...currentTriggerInfo.configuration.definition,
|
|
||||||
headers: [...currentTriggerInfo.configuration.headers],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (currentTriggerInfo.configuration.webhook_from_env) {
|
|
||||||
downPayload.args.webhook_from_env =
|
|
||||||
currentTriggerInfo.configuration.webhook_from_env;
|
|
||||||
} else {
|
|
||||||
downPayload.args.webhook = currentTriggerInfo.configuration.webhook;
|
|
||||||
}
|
|
||||||
const upQueryArgs = [];
|
|
||||||
upQueryArgs.push(payload);
|
|
||||||
const downQueryArgs = [];
|
|
||||||
downQueryArgs.push(downPayload);
|
|
||||||
const upQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: upQueryArgs,
|
|
||||||
};
|
|
||||||
const downQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: downQueryArgs,
|
|
||||||
};
|
|
||||||
const requestMsg = 'Deleting trigger...';
|
|
||||||
const successMsg = 'Trigger deleted';
|
|
||||||
const errorMsg = 'Delete trigger failed';
|
|
||||||
|
|
||||||
const customOnSuccess = () => {
|
|
||||||
// dispatch({ type: REQUEST_SUCCESS });
|
|
||||||
dispatch({ type: REQUEST_COMPLETE }); // modify trigger action
|
|
||||||
dispatch(showSuccessNotification('Trigger Deleted'));
|
|
||||||
dispatch(push('/manage/triggers'));
|
|
||||||
// remove this trigger from state
|
|
||||||
const existingTriggers = getState().triggers.triggerList.filter(
|
|
||||||
trigger => trigger.name !== triggerName
|
|
||||||
);
|
|
||||||
dispatch({
|
|
||||||
type: LOAD_TRIGGER_LIST,
|
|
||||||
triggerList: existingTriggers,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const customOnError = () => {
|
|
||||||
dispatch({ type: REQUEST_COMPLETE }); // modify trigger action
|
|
||||||
dispatch({ type: REQUEST_ERROR, data: errorMsg });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// modify trigger action
|
|
||||||
dispatch({ type: REQUEST_ONGOING, data: 'delete' });
|
|
||||||
|
|
||||||
makeMigrationCall(
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
upQuery.args,
|
|
||||||
downQuery.args,
|
|
||||||
migrationName,
|
|
||||||
customOnSuccess,
|
|
||||||
customOnError,
|
|
||||||
requestMsg,
|
|
||||||
successMsg,
|
|
||||||
errorMsg,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ******************************************************* */
|
|
||||||
const eventReducer = (state = defaultState, action) => {
|
|
||||||
// eslint-disable-line no-unused-vars
|
|
||||||
if (action.type.indexOf('ProcessedEvents/') === 0) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
view: processedEventsReducer(
|
|
||||||
state.currentTrigger,
|
|
||||||
state.triggerList,
|
|
||||||
state.view,
|
|
||||||
action
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type.indexOf('PendingEvents/') === 0) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
view: pendingEventsReducer(
|
|
||||||
state.currentTrigger,
|
|
||||||
state.triggerList,
|
|
||||||
state.view,
|
|
||||||
action
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type.indexOf('RunningEvents/') === 0) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
view: runningEventsReducer(
|
|
||||||
state.currentTrigger,
|
|
||||||
state.triggerList,
|
|
||||||
state.view,
|
|
||||||
action
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type.indexOf('StreamingLogs/') === 0) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: streamingLogsReducer(
|
|
||||||
state.currentTrigger,
|
|
||||||
state.triggerList,
|
|
||||||
state.log,
|
|
||||||
action
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
switch (action.type) {
|
|
||||||
case LOAD_TRIGGER_LIST:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
triggerList: action.triggerList,
|
|
||||||
listingTrigger: action.triggerList,
|
|
||||||
};
|
|
||||||
case LISTING_TRIGGER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
listingTrigger: action.updatedList,
|
|
||||||
};
|
|
||||||
case LOAD_PROCESSED_EVENTS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
processedEvents: action.data,
|
|
||||||
};
|
|
||||||
case LOAD_PENDING_EVENTS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
pendingEvents: action.data,
|
|
||||||
};
|
|
||||||
case LOAD_RUNNING_EVENTS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
runningEvents: action.data,
|
|
||||||
};
|
|
||||||
case LOAD_EVENT_LOGS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, rows: action.data, count: action.data.length },
|
|
||||||
};
|
|
||||||
case SET_TRIGGER:
|
|
||||||
return { ...state, currentTrigger: action.triggerName };
|
|
||||||
case ADMIN_SECRET_ERROR:
|
|
||||||
return { ...state, adminSecretError: action.data };
|
|
||||||
case UPDATE_DATA_HEADERS:
|
|
||||||
return { ...state, dataHeaders: action.data };
|
|
||||||
case MODAL_OPEN:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, isModalOpen: action.data },
|
|
||||||
};
|
|
||||||
case SET_REDELIVER_EVENT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, redeliverEventId: action.eventId },
|
|
||||||
};
|
|
||||||
case LOAD_EVENT_INVOCATIONS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, eventInvocations: action.data },
|
|
||||||
};
|
|
||||||
case REDELIVER_EVENT_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, redeliverInvocationId: action.data },
|
|
||||||
};
|
|
||||||
case REDELIVER_EVENT_FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
log: { ...state.log, redeliverEventFailure: action.data },
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default eventReducer;
|
|
||||||
export {
|
|
||||||
setTrigger,
|
|
||||||
loadTriggers,
|
|
||||||
deleteTrigger,
|
|
||||||
loadPendingEvents,
|
|
||||||
loadRunningEvents,
|
|
||||||
loadEventLogs,
|
|
||||||
handleMigrationErrors,
|
|
||||||
makeMigrationCall,
|
|
||||||
setRedeliverEvent,
|
|
||||||
loadEventInvocations,
|
|
||||||
redeliverEvent,
|
|
||||||
ADMIN_SECRET_ERROR,
|
|
||||||
UPDATE_DATA_HEADERS,
|
|
||||||
LISTING_TRIGGER,
|
|
||||||
MODAL_OPEN,
|
|
||||||
SET_REDELIVER_EVENT,
|
|
||||||
};
|
|
@ -1,64 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
|
|
||||||
import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer';
|
|
||||||
import EventSubSidebar from './EventSubSidebar';
|
|
||||||
|
|
||||||
const appPrefix = '/events';
|
|
||||||
|
|
||||||
const EventPageContainer = ({
|
|
||||||
schema,
|
|
||||||
currentSchema,
|
|
||||||
children,
|
|
||||||
location,
|
|
||||||
dispatch,
|
|
||||||
}) => {
|
|
||||||
const styles = require('../../Common/TableCommon/Table.scss');
|
|
||||||
|
|
||||||
const currentLocation = location.pathname;
|
|
||||||
|
|
||||||
const sidebarContent = (
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
role="presentation"
|
|
||||||
className={
|
|
||||||
currentLocation.includes('events/manage') ? styles.active : ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link className={styles.linkBorder} to={appPrefix + '/manage'}>
|
|
||||||
Manage
|
|
||||||
</Link>
|
|
||||||
<EventSubSidebar
|
|
||||||
location={location}
|
|
||||||
schema={schema}
|
|
||||||
currentSchema={currentSchema}
|
|
||||||
dispatch={dispatch}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
|
|
||||||
const helmet = 'Events | Hasura';
|
|
||||||
|
|
||||||
const leftContainer = <LeftContainer>{sidebarContent}</LeftContainer>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContainer helmet={helmet} leftContainer={leftContainer}>
|
|
||||||
{children}
|
|
||||||
</PageContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
schema: state.tables.allSchemas,
|
|
||||||
schemaList: state.tables.schemaList,
|
|
||||||
currentSchema: state.tables.currentSchema,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventPageConnector = connect =>
|
|
||||||
connect(mapStateToProps)(EventPageContainer);
|
|
||||||
|
|
||||||
export default eventPageConnector;
|
|
@ -1,13 +0,0 @@
|
|||||||
import eventTriggerReducer from './EventActions';
|
|
||||||
import addTriggerReducer from './Add/AddActions';
|
|
||||||
import modifyTriggerReducer from './Modify/Actions';
|
|
||||||
import invokeEventTriggerReducer from './Common/InvokeManualTrigger/InvokeManualTriggerAction';
|
|
||||||
|
|
||||||
const eventReducer = {
|
|
||||||
triggers: eventTriggerReducer,
|
|
||||||
addTrigger: addTriggerReducer,
|
|
||||||
modifyTrigger: modifyTriggerReducer,
|
|
||||||
invokeEventTrigger: invokeEventTriggerReducer,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default eventReducer;
|
|
@ -1,159 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
// import {push} fropm 'react-router-redux';
|
|
||||||
import { Route, IndexRedirect } from 'react-router';
|
|
||||||
import globals from '../../../Globals';
|
|
||||||
|
|
||||||
import {
|
|
||||||
landingConnector,
|
|
||||||
addTriggerConnector,
|
|
||||||
modifyTriggerConnector,
|
|
||||||
processedEventsConnector,
|
|
||||||
pendingEventsConnector,
|
|
||||||
runningEventsConnector,
|
|
||||||
eventPageConnector,
|
|
||||||
streamingLogsConnector,
|
|
||||||
} from '.';
|
|
||||||
|
|
||||||
import { rightContainerConnector } from '../../Common/Layout';
|
|
||||||
|
|
||||||
import {
|
|
||||||
loadTriggers,
|
|
||||||
loadPendingEvents,
|
|
||||||
loadRunningEvents,
|
|
||||||
} from '../EventTrigger/EventActions';
|
|
||||||
|
|
||||||
const makeEventRouter = (
|
|
||||||
connect,
|
|
||||||
store,
|
|
||||||
composeOnEnterHooks,
|
|
||||||
requireSchema,
|
|
||||||
requirePendingEvents,
|
|
||||||
requireRunningEvents
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
path="events"
|
|
||||||
component={eventPageConnector(connect)}
|
|
||||||
onEnter={composeOnEnterHooks([requireSchema])}
|
|
||||||
>
|
|
||||||
<IndexRedirect to="manage" />
|
|
||||||
<Route path="manage" component={rightContainerConnector(connect)}>
|
|
||||||
<IndexRedirect to="triggers" />
|
|
||||||
<Route path="triggers" component={landingConnector(connect)} />
|
|
||||||
<Route
|
|
||||||
path="triggers/:trigger/processed"
|
|
||||||
component={processedEventsConnector(connect)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="triggers/:trigger/pending"
|
|
||||||
component={pendingEventsConnector(connect)}
|
|
||||||
onEnter={composeOnEnterHooks([requirePendingEvents])}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="triggers/:trigger/running"
|
|
||||||
component={runningEventsConnector(connect)}
|
|
||||||
onEnter={composeOnEnterHooks([requireRunningEvents])}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="triggers/:trigger/logs"
|
|
||||||
component={streamingLogsConnector(connect)}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route
|
|
||||||
path="manage/triggers/add"
|
|
||||||
component={addTriggerConnector(connect)}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="manage/triggers/:trigger/modify"
|
|
||||||
component={modifyTriggerConnector(connect)}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventRouterUtils = (connect, store, composeOnEnterHooks) => {
|
|
||||||
const requireSchema = (nextState, replaceState, cb) => {
|
|
||||||
// check if admin secret is available in localstorage. if so use that.
|
|
||||||
// if localstorage admin secret didn't work, redirect to login (meaning value has changed)
|
|
||||||
// if admin secret is not available in localstorage, check if cli is giving it via window.__env
|
|
||||||
// if admin secret is not available in localstorage and cli, make a api call to data without admin secret.
|
|
||||||
// if the api fails, then redirect to login - this is a fresh user/browser flow
|
|
||||||
const {
|
|
||||||
triggers: { triggerList },
|
|
||||||
} = store.getState();
|
|
||||||
|
|
||||||
if (triggerList.length) {
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([store.dispatch(loadTriggers([]))]).then(
|
|
||||||
() => {
|
|
||||||
cb();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// alert('Could not load schema.');
|
|
||||||
replaceState(globals.urlPrefix);
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requirePendingEvents = (nextState, replaceState, cb) => {
|
|
||||||
const {
|
|
||||||
triggers: { pendingEvents },
|
|
||||||
} = store.getState();
|
|
||||||
|
|
||||||
if (pendingEvents.length) {
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([store.dispatch(loadPendingEvents())]).then(
|
|
||||||
() => {
|
|
||||||
cb();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// alert('Could not load schema.');
|
|
||||||
replaceState(globals.urlPrefix);
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requireRunningEvents = (nextState, replaceState, cb) => {
|
|
||||||
const {
|
|
||||||
triggers: { runningEvents },
|
|
||||||
} = store.getState();
|
|
||||||
|
|
||||||
if (runningEvents.length) {
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([store.dispatch(loadRunningEvents())]).then(
|
|
||||||
() => {
|
|
||||||
cb();
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// alert('Could not load schema.');
|
|
||||||
replaceState(globals.urlPrefix);
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
makeEventRouter: makeEventRouter(
|
|
||||||
connect,
|
|
||||||
store,
|
|
||||||
composeOnEnterHooks,
|
|
||||||
requireSchema,
|
|
||||||
requirePendingEvents,
|
|
||||||
requireRunningEvents
|
|
||||||
),
|
|
||||||
requireSchema,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default eventRouterUtils;
|
|
@ -1,84 +0,0 @@
|
|||||||
const defaultCurFilter = {
|
|
||||||
where: { $and: [{ '': { '': '' } }] },
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
order_by: [{ column: '', type: 'asc', nulls: 'last' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultViewState = {
|
|
||||||
query: {
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'events',
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
rows: [],
|
|
||||||
expandedRow: '',
|
|
||||||
count: 0,
|
|
||||||
curFilter: defaultCurFilter,
|
|
||||||
activePath: [],
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: {},
|
|
||||||
lastSuccess: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultLogState = {
|
|
||||||
query: {
|
|
||||||
columns: [
|
|
||||||
'*',
|
|
||||||
{
|
|
||||||
name: 'event',
|
|
||||||
columns: ['*'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
order_by: ['-created_at'],
|
|
||||||
},
|
|
||||||
rows: [],
|
|
||||||
expandedRow: '',
|
|
||||||
count: 0,
|
|
||||||
curFilter: defaultCurFilter,
|
|
||||||
activePath: [],
|
|
||||||
isLoadingOlder: false,
|
|
||||||
isLoadingNewer: false,
|
|
||||||
isOldAvailable: true,
|
|
||||||
isNewAvailable: true,
|
|
||||||
isModalOpen: false,
|
|
||||||
redeliverEventId: null,
|
|
||||||
eventInvocations: [],
|
|
||||||
redeliverInvocationId: null,
|
|
||||||
redeliverEventFailure: null,
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: {},
|
|
||||||
lastSuccess: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
currentTrigger: null,
|
|
||||||
view: { ...defaultViewState },
|
|
||||||
log: { ...defaultLogState },
|
|
||||||
triggerList: [],
|
|
||||||
listingTrigger: [],
|
|
||||||
processedEvents: [],
|
|
||||||
pendingEvents: [],
|
|
||||||
runningEvents: [],
|
|
||||||
eventLogs: [],
|
|
||||||
schemaList: ['public'],
|
|
||||||
currentSchema: 'public',
|
|
||||||
adminSecretError: false,
|
|
||||||
dataHeaders: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defaultState;
|
|
||||||
export { defaultViewState, defaultLogState, defaultCurFilter };
|
|
@ -1,118 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
// import globals from '../../../Globals';
|
|
||||||
|
|
||||||
import LeftSubSidebar from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar';
|
|
||||||
|
|
||||||
import { LISTING_TRIGGER } from './EventActions';
|
|
||||||
|
|
||||||
const appPrefix = '/events';
|
|
||||||
|
|
||||||
const EventSubSidebar = ({
|
|
||||||
currentTrigger,
|
|
||||||
triggerList,
|
|
||||||
listingTrigger,
|
|
||||||
// children,
|
|
||||||
dispatch,
|
|
||||||
location,
|
|
||||||
readOnlyMode,
|
|
||||||
}) => {
|
|
||||||
const styles = require('../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss');
|
|
||||||
|
|
||||||
function triggerSearch(e) {
|
|
||||||
const searchTerm = e.target.value;
|
|
||||||
// form new schema
|
|
||||||
const matchedTables = [];
|
|
||||||
triggerList.map(trigger => {
|
|
||||||
if (trigger.name.indexOf(searchTerm) !== -1) {
|
|
||||||
matchedTables.push(trigger);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// update schema with matchedTables
|
|
||||||
dispatch({ type: LISTING_TRIGGER, updatedList: matchedTables });
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSearchInput = () => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
onChange={triggerSearch.bind(this)}
|
|
||||||
className="form-control"
|
|
||||||
placeholder="search event triggers"
|
|
||||||
data-test="search-triggers"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getChildList = () => {
|
|
||||||
let triggerLinks = (
|
|
||||||
<li className={styles.noChildren}>
|
|
||||||
<i>No triggers available</i>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggers = {};
|
|
||||||
listingTrigger.map(t => {
|
|
||||||
triggers[t.name] = t;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentLocation = location.pathname;
|
|
||||||
|
|
||||||
if (listingTrigger && listingTrigger.length) {
|
|
||||||
triggerLinks = Object.keys(triggers)
|
|
||||||
.sort()
|
|
||||||
.map((trigger, i) => {
|
|
||||||
let activeTableClass = '';
|
|
||||||
if (
|
|
||||||
trigger === currentTrigger &&
|
|
||||||
currentLocation.indexOf(currentTrigger) !== -1
|
|
||||||
) {
|
|
||||||
activeTableClass = styles.activeLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={activeTableClass} key={i}>
|
|
||||||
<Link
|
|
||||||
to={appPrefix + '/manage/triggers/' + trigger + '/processed'}
|
|
||||||
data-test={trigger}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className={styles.tableIcon + ' fa fa-send-o'}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{trigger}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return triggerLinks;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LeftSubSidebar
|
|
||||||
showAddBtn={!readOnlyMode}
|
|
||||||
searchInput={getSearchInput()}
|
|
||||||
heading={`Event Triggers (${triggerList.length})`}
|
|
||||||
addLink={'/events/manage/triggers/add'}
|
|
||||||
addLabel={'Create'}
|
|
||||||
addTestString={'sidebar-add-table'}
|
|
||||||
childListTestString={'table-links'}
|
|
||||||
>
|
|
||||||
{getChildList()}
|
|
||||||
</LeftSubSidebar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
currentTrigger: state.triggers.currentTrigger,
|
|
||||||
triggerList: state.triggers.triggerList,
|
|
||||||
listingTrigger: state.triggers.listingTrigger,
|
|
||||||
readOnlyMode: state.main.readOnlyMode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(EventSubSidebar);
|
|
@ -1,125 +0,0 @@
|
|||||||
/* eslint-disable space-infix-ops */
|
|
||||||
/* eslint-disable no-loop-func */
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
import { loadTriggers } from '../EventActions';
|
|
||||||
import globals from '../../../../Globals';
|
|
||||||
import Button from '../../../Common/Button/Button';
|
|
||||||
import TopicDescription from '../../Common/Landing/TopicDescription';
|
|
||||||
import TryItOut from '../../Common/Landing/TryItOut';
|
|
||||||
|
|
||||||
const appPrefix = globals.urlPrefix + '/events';
|
|
||||||
|
|
||||||
class EventTrigger extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// Initialize this table
|
|
||||||
const dispatch = this.props.dispatch;
|
|
||||||
dispatch(loadTriggers([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { dispatch, readOnlyMode } = this.props;
|
|
||||||
|
|
||||||
const styles = require('../../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss');
|
|
||||||
|
|
||||||
const queryDefinition = `mutation {
|
|
||||||
insert_user(objects: [{name: "testuser"}] ){
|
|
||||||
affected_rows
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const getAddBtn = () => {
|
|
||||||
if (readOnlyMode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
dispatch(push(`${appPrefix}/manage/triggers/add`));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-test="data-create-trigger"
|
|
||||||
color="yellow"
|
|
||||||
size="sm"
|
|
||||||
className={styles.add_mar_left}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const footerEvent = (
|
|
||||||
<span>
|
|
||||||
Head to the Events tab and see an event invoked under{' '}
|
|
||||||
<span className={styles.fontWeightBold}> test-trigger</span>.
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${styles.padd_left_remove} container-fluid ${styles.padd_top}`}
|
|
||||||
>
|
|
||||||
<div className={styles.padd_left}>
|
|
||||||
<Helmet title="Event Triggers | Hasura" />
|
|
||||||
<div className={styles.display_flex}>
|
|
||||||
<h2 className={`${styles.headerText} ${styles.inline_block}`}>
|
|
||||||
Event Triggers
|
|
||||||
</h2>
|
|
||||||
{getAddBtn()}
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<TopicDescription
|
|
||||||
title="What are Event Triggers?"
|
|
||||||
imgUrl={`${globals.assetsPath}/common/img/event-trigger.png`}
|
|
||||||
imgAlt="Event Triggers"
|
|
||||||
description="Hasura can be used to create event triggers on tables. An Event Trigger atomically captures events (insert, update, delete) on a specified table and then reliably calls a webhook that can carry out any custom logic."
|
|
||||||
/>
|
|
||||||
<hr className={styles.clear_fix} />
|
|
||||||
|
|
||||||
<TryItOut
|
|
||||||
service="eventTrigger"
|
|
||||||
title="Steps to deploy an example Event Trigger to Glitch"
|
|
||||||
queryDefinition={queryDefinition}
|
|
||||||
footerDescription={footerEvent}
|
|
||||||
glitchLink="https://glitch.com/edit/#!/hasura-sample-event-trigger"
|
|
||||||
googleCloudLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/google-cloud-functions/nodejs8"
|
|
||||||
MicrosoftAzureLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/azure-functions/nodejs"
|
|
||||||
awsLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/aws-lambda/nodejs8"
|
|
||||||
adMoreLink="https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/event-triggers/"
|
|
||||||
isAvailable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventTrigger.propTypes = {
|
|
||||||
schema: PropTypes.array.isRequired,
|
|
||||||
untrackedRelations: PropTypes.array.isRequired,
|
|
||||||
currentSchema: PropTypes.string.isRequired,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
schema: state.tables.allSchemas,
|
|
||||||
schemaList: state.tables.schemaList,
|
|
||||||
untrackedRelations: state.tables.untrackedRelations,
|
|
||||||
currentSchema: state.tables.currentSchema,
|
|
||||||
listingTrigger: state.triggers.listingTrigger,
|
|
||||||
readOnlyMode: state.main.readOnlyMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventTriggerConnector = connect => connect(mapStateToProps)(EventTrigger);
|
|
||||||
|
|
||||||
export default eventTriggerConnector;
|
|
@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { deleteTrigger } from '../EventActions';
|
|
||||||
import Button from '../../../Common/Button/Button';
|
|
||||||
import { getConfirmation } from '../../../Common/utils/jsUtils';
|
|
||||||
|
|
||||||
const verifyDeleteTrigger = (triggerName, dispatch) => {
|
|
||||||
const confirmMessage = `This will permanently delete the event trigger "${triggerName}"`;
|
|
||||||
const isOk = getConfirmation(confirmMessage, true, triggerName);
|
|
||||||
if (isOk) {
|
|
||||||
dispatch(deleteTrigger(triggerName));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Buttons = ({ styles, dispatch, triggerName, ongoingRequest }) => (
|
|
||||||
<div className={styles.add_mar_bottom}>
|
|
||||||
<Button
|
|
||||||
color="red"
|
|
||||||
size="sm"
|
|
||||||
data-test="delete-trigger"
|
|
||||||
onClick={() => verifyDeleteTrigger(triggerName, dispatch)}
|
|
||||||
disabled={ongoingRequest === 'delete'}
|
|
||||||
>
|
|
||||||
{ongoingRequest === 'delete' ? 'Deleting ...' : 'Delete'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Buttons;
|
|
@ -1,340 +0,0 @@
|
|||||||
import defaultState from './State';
|
|
||||||
import { loadTriggers, makeMigrationCall, setTrigger } from '../EventActions';
|
|
||||||
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
|
|
||||||
import { showErrorNotification } from '../../Common/Notification';
|
|
||||||
|
|
||||||
import { MANUAL_TRIGGER_VAR } from './utils';
|
|
||||||
|
|
||||||
const SET_DEFAULTS = 'ModifyTrigger/SET_DEFAULTS';
|
|
||||||
export const setDefaults = () => ({ type: SET_DEFAULTS });
|
|
||||||
|
|
||||||
const SET_WEBHOOK_URL = 'ModifyTrigger/SET_WEBHOOK_URL';
|
|
||||||
const SET_WEBHOOK_URL_TYPE = 'ModifyTrigger/SET_WEBHOOK_URL_TYPE';
|
|
||||||
export const setWebhookUrl = data => ({ type: SET_WEBHOOK_URL, data });
|
|
||||||
export const setWebhookUrlType = data => ({
|
|
||||||
type: SET_WEBHOOK_URL_TYPE,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const SET_RETRY_NUM = 'ModifyTrigger/SET_RETRY_NUM';
|
|
||||||
const SET_RETRY_INTERVAL = 'ModifyTrigger/SET_RETRY_INTERVAL';
|
|
||||||
const SET_RETRY_TIMEOUT = 'ModifyTrigger/SET_RETRY_TIMEOUT';
|
|
||||||
export const setRetryNum = data => ({ type: SET_RETRY_NUM, data });
|
|
||||||
export const setRetryInterval = data => ({ type: SET_RETRY_INTERVAL, data });
|
|
||||||
export const setRetryTimeout = data => ({ type: SET_RETRY_TIMEOUT, data });
|
|
||||||
|
|
||||||
const TOGGLE_COLUMN = 'ModifyTrigger/TOGGLE_COLUMNS';
|
|
||||||
const TOGGLE_QUERY_TYPE = 'ModifyTrigger/TOGGLE_QUERY_TYPE_SELECTED';
|
|
||||||
const TOGGLE_MANUAL_QUERY_TYPE = 'ModifyTrigger/TOGGLE_MANUAL_QUERY_SELECTED';
|
|
||||||
export const RESET_MODIFY_STATE = 'ModifyTrigger/RESET_MODIFY_STATE';
|
|
||||||
|
|
||||||
export const toggleQueryType = ({ query, columns, value }) => ({
|
|
||||||
type: TOGGLE_QUERY_TYPE,
|
|
||||||
query,
|
|
||||||
columns,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
export const toggleManualType = ({ value }) => ({
|
|
||||||
type: TOGGLE_MANUAL_QUERY_TYPE,
|
|
||||||
data: value,
|
|
||||||
});
|
|
||||||
export const toggleColumn = (query, column) => ({
|
|
||||||
type: TOGGLE_COLUMN,
|
|
||||||
query,
|
|
||||||
column,
|
|
||||||
});
|
|
||||||
|
|
||||||
const REMOVE_HEADER = 'ModifyTrigger/REMOVE_HEADER';
|
|
||||||
const SET_HEADERKEY = 'ModifyTrigger/SET_HEADERKEY';
|
|
||||||
const SET_HEADERTYPE = 'ModifyTrigger/SET_HEADERTYPE';
|
|
||||||
const SET_HEADERVALUE = 'ModifyTrigger/SET_HEADERVALUE';
|
|
||||||
const ADD_HEADER = 'ModifyTrigger/ADD_HEADER';
|
|
||||||
export const addHeader = () => ({ type: ADD_HEADER });
|
|
||||||
export const removeHeader = data => ({ type: REMOVE_HEADER, data });
|
|
||||||
export const setHeaderKey = (data, index) => ({
|
|
||||||
type: SET_HEADERKEY,
|
|
||||||
data,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
export const setHeaderType = (data, index) => ({
|
|
||||||
type: SET_HEADERTYPE,
|
|
||||||
data,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
export const setHeaderValue = (data, index) => ({
|
|
||||||
type: SET_HEADERVALUE,
|
|
||||||
data,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const REQUEST_ONGOING = 'ModifyTrigger/REQUEST_ONGOING';
|
|
||||||
export const REQUEST_COMPLETE = 'ModifyTrigger/REQUEST_COMPLETE';
|
|
||||||
|
|
||||||
export const showValidationError = message => {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch(
|
|
||||||
showErrorNotification('Error modifying trigger!', 'Invalid input', {
|
|
||||||
custom: message,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const save = (property, triggerName) => {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const { modifyTrigger } = getState();
|
|
||||||
const oldTrigger = getState().triggers.triggerList.find(
|
|
||||||
tr => tr.name === triggerName
|
|
||||||
);
|
|
||||||
const downPayload = {
|
|
||||||
replace: true,
|
|
||||||
name: oldTrigger.name,
|
|
||||||
table: {
|
|
||||||
name: oldTrigger.table_name,
|
|
||||||
schema: oldTrigger.table_schema,
|
|
||||||
},
|
|
||||||
retry_conf: { ...oldTrigger.configuration.retry_conf },
|
|
||||||
...oldTrigger.configuration.definition,
|
|
||||||
headers: [...oldTrigger.configuration.headers],
|
|
||||||
};
|
|
||||||
if (oldTrigger.configuration.webhook_from_env) {
|
|
||||||
downPayload.webhook_from_env = oldTrigger.configuration.webhook_from_env;
|
|
||||||
} else {
|
|
||||||
downPayload.webhook = oldTrigger.configuration.webhook;
|
|
||||||
}
|
|
||||||
const upPayload = {
|
|
||||||
...downPayload,
|
|
||||||
};
|
|
||||||
if (property === 'webhook') {
|
|
||||||
if (modifyTrigger.webhookUrlType === 'env') {
|
|
||||||
delete upPayload.webhook;
|
|
||||||
upPayload.webhook_from_env = modifyTrigger.webhookURL;
|
|
||||||
} else {
|
|
||||||
delete upPayload.webhook_from_env;
|
|
||||||
upPayload.webhook = modifyTrigger.webhookURL;
|
|
||||||
}
|
|
||||||
} else if (property === 'ops') {
|
|
||||||
delete upPayload.update;
|
|
||||||
delete upPayload.delete;
|
|
||||||
delete upPayload.insert;
|
|
||||||
upPayload.update = modifyTrigger.definition.update;
|
|
||||||
upPayload.insert = modifyTrigger.definition.insert;
|
|
||||||
upPayload.delete = modifyTrigger.definition.delete;
|
|
||||||
// Add only if the value is true
|
|
||||||
if (MANUAL_TRIGGER_VAR in modifyTrigger.definition) {
|
|
||||||
delete upPayload[MANUAL_TRIGGER_VAR];
|
|
||||||
upPayload[MANUAL_TRIGGER_VAR] =
|
|
||||||
modifyTrigger.definition[MANUAL_TRIGGER_VAR];
|
|
||||||
}
|
|
||||||
} else if (property === 'retry') {
|
|
||||||
upPayload.retry_conf = {
|
|
||||||
num_retries: modifyTrigger.retryConf.numRetrys,
|
|
||||||
interval_sec: modifyTrigger.retryConf.retryInterval,
|
|
||||||
timeout_sec: modifyTrigger.retryConf.timeout,
|
|
||||||
};
|
|
||||||
} else if (property === 'headers') {
|
|
||||||
delete upPayload.headers;
|
|
||||||
upPayload.headers = [];
|
|
||||||
modifyTrigger.headers
|
|
||||||
.filter(h => Boolean(h.key.trim()))
|
|
||||||
.forEach(h => {
|
|
||||||
const { key, value, type } = h;
|
|
||||||
if (type === 'env') {
|
|
||||||
upPayload.headers.push({
|
|
||||||
name: key.trim(),
|
|
||||||
value_from_env: value.trim(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
upPayload.headers.push({
|
|
||||||
name: key.trim(),
|
|
||||||
value: value.trim(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const upQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'create_event_trigger',
|
|
||||||
args: {
|
|
||||||
...upPayload,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const downQuery = {
|
|
||||||
type: 'bulk',
|
|
||||||
args: [
|
|
||||||
{
|
|
||||||
type: 'create_event_trigger',
|
|
||||||
args: {
|
|
||||||
...downPayload,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const migrationName = `modify_tr_${triggerName}_${property}`;
|
|
||||||
const requestMsg = 'Updating trigger';
|
|
||||||
const successMsg = 'Updated trigger';
|
|
||||||
const errorMsg = 'Updating trigger failed';
|
|
||||||
const customOnSuccess = () => {
|
|
||||||
dispatch({ type: REQUEST_COMPLETE });
|
|
||||||
dispatch(setTrigger(triggerName.trim()));
|
|
||||||
dispatch(loadTriggers([triggerName]));
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
const customOnError = err => {
|
|
||||||
dispatch({ type: REQUEST_COMPLETE });
|
|
||||||
dispatch({ type: UPDATE_MIGRATION_STATUS_ERROR, data: err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
dispatch({ type: REQUEST_ONGOING, data: property });
|
|
||||||
makeMigrationCall(
|
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
upQuery.args,
|
|
||||||
downQuery.args,
|
|
||||||
migrationName,
|
|
||||||
customOnSuccess,
|
|
||||||
customOnError,
|
|
||||||
requestMsg,
|
|
||||||
successMsg,
|
|
||||||
errorMsg,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducer = (state = defaultState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case SET_WEBHOOK_URL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
webhookURL: action.data,
|
|
||||||
};
|
|
||||||
case SET_WEBHOOK_URL_TYPE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
webhookUrlType: action.data,
|
|
||||||
};
|
|
||||||
case TOGGLE_QUERY_TYPE:
|
|
||||||
const newDefinition = { ...state.definition };
|
|
||||||
if (action.value) {
|
|
||||||
if (action.query === 'update') {
|
|
||||||
newDefinition[action.query] = { columns: action.columns };
|
|
||||||
} else {
|
|
||||||
newDefinition[action.query] = { columns: '*' };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete newDefinition[action.query];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
definition: newDefinition,
|
|
||||||
};
|
|
||||||
case TOGGLE_MANUAL_QUERY_TYPE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
definition: {
|
|
||||||
...state.definition,
|
|
||||||
[MANUAL_TRIGGER_VAR]: action.data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case TOGGLE_COLUMN:
|
|
||||||
const queryColumns = [...state.definition[action.query].columns];
|
|
||||||
if (queryColumns.find(qc => qc === action.column)) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
definition: {
|
|
||||||
...state.definition,
|
|
||||||
[action.query]: {
|
|
||||||
columns: queryColumns.filter(qc => qc !== action.column),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
definition: {
|
|
||||||
...state.definition,
|
|
||||||
[action.query]: { columns: [...queryColumns, action.column] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_RETRY_NUM:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
numRetrys: action.data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_RETRY_INTERVAL:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
retryInterval: action.data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case SET_RETRY_TIMEOUT:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
retryConf: {
|
|
||||||
...state.retryConf,
|
|
||||||
timeout: action.data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case ADD_HEADER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: [...state.headers, { key: '', type: 'static', value: '' }],
|
|
||||||
};
|
|
||||||
case REMOVE_HEADER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: state.headers.filter((h, i) => i !== action.data),
|
|
||||||
};
|
|
||||||
case SET_HEADERKEY:
|
|
||||||
const kNewHeaders = [...state.headers];
|
|
||||||
kNewHeaders[action.index].key = action.data;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: kNewHeaders,
|
|
||||||
};
|
|
||||||
case SET_HEADERVALUE:
|
|
||||||
const vNewHeaders = [...state.headers];
|
|
||||||
vNewHeaders[action.index].value = action.data;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: vNewHeaders,
|
|
||||||
};
|
|
||||||
case SET_HEADERTYPE:
|
|
||||||
const tNewHeaders = [...state.headers];
|
|
||||||
tNewHeaders[action.index].type = action.data;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
headers: tNewHeaders,
|
|
||||||
};
|
|
||||||
case REQUEST_ONGOING:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ongoingRequest: action.data,
|
|
||||||
};
|
|
||||||
case REQUEST_COMPLETE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ongoingRequest: null,
|
|
||||||
};
|
|
||||||
case RESET_MODIFY_STATE:
|
|
||||||
return {
|
|
||||||
...defaultState,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reducer;
|
|
@ -1,18 +0,0 @@
|
|||||||
import Modify from './Modify';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
return {
|
|
||||||
modifyTrigger: state.modifyTrigger,
|
|
||||||
modifyTriggerName: ownProps.params.trigger,
|
|
||||||
triggerList: state.triggers.triggerList,
|
|
||||||
schemaList: state.tables.schemaList,
|
|
||||||
allSchemas: state.tables.allSchemas,
|
|
||||||
serverVersion: state.main.serverVersion,
|
|
||||||
currentSchema: state.tables.currentSchema,
|
|
||||||
readOnlyMode: state.main.readOnlyMode,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const modifyTriggerConnector = connect => connect(mapStateToProps)(Modify);
|
|
||||||
|
|
||||||
export default modifyTriggerConnector;
|
|
@ -1,153 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
|
||||||
import AceEditor from 'react-ace';
|
|
||||||
import {
|
|
||||||
addHeader,
|
|
||||||
removeHeader,
|
|
||||||
setHeaderKey,
|
|
||||||
setHeaderType,
|
|
||||||
setHeaderValue,
|
|
||||||
} from './Actions';
|
|
||||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
|
||||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
|
||||||
|
|
||||||
class HeadersEditor extends React.Component {
|
|
||||||
setValues = () => {
|
|
||||||
const { dispatch, headers, modifyTrigger } = this.props;
|
|
||||||
headers.forEach((h, i) => {
|
|
||||||
const { name, value, value_from_env } = h;
|
|
||||||
dispatch(setHeaderKey(name, i));
|
|
||||||
dispatch(setHeaderType(value_from_env ? 'env' : 'static', i));
|
|
||||||
dispatch(setHeaderValue(value || value_from_env, i));
|
|
||||||
if (!modifyTrigger[i + 1]) {
|
|
||||||
this.addExtraHeader();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
addExtraHeader = () => {
|
|
||||||
const { dispatch, modifyTrigger } = this.props;
|
|
||||||
const lastHeader = modifyTrigger.headers[modifyTrigger.headers.length - 1];
|
|
||||||
if (lastHeader.key && lastHeader.value && lastHeader.type) {
|
|
||||||
dispatch(addHeader());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSelectionChange = (e, i) => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(setHeaderType(e.target.getAttribute('value'), i));
|
|
||||||
dispatch(setHeaderValue('', i));
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { headers, styles, save, modifyTrigger, dispatch } = this.props;
|
|
||||||
const sanitiseHeaders = rawHeaders => {
|
|
||||||
return rawHeaders.map(h => {
|
|
||||||
return {
|
|
||||||
name: h.name,
|
|
||||||
value: h.value,
|
|
||||||
value_from_env: h.value_from_env,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const collapsed = () => (
|
|
||||||
<div>
|
|
||||||
{headers.length > 0 ? (
|
|
||||||
<div className={styles.modifyHeaders}>
|
|
||||||
<AceEditor
|
|
||||||
mode="json"
|
|
||||||
theme="github"
|
|
||||||
name="headers"
|
|
||||||
value={JSON.stringify(sanitiseHeaders(headers), null, 2)}
|
|
||||||
minLines={4}
|
|
||||||
maxLines={100}
|
|
||||||
width="100%"
|
|
||||||
showPrintMargin={false}
|
|
||||||
showGutter={false}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.modifyProperty}>No headers</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const expanded = () => (
|
|
||||||
<div className={styles.modifyOpsPadLeft}>
|
|
||||||
{modifyTrigger.headers.map((h, i) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.modifyHeadersCollapsedContent} key={i}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyHeadersTextbox}`}
|
|
||||||
value={h.key}
|
|
||||||
onChange={e => {
|
|
||||||
dispatch(setHeaderKey(e.target.value, i));
|
|
||||||
}}
|
|
||||||
placeholder="key"
|
|
||||||
/>
|
|
||||||
<div className={styles.dropDownGroup}>
|
|
||||||
<DropdownButton
|
|
||||||
dropdownOptions={[
|
|
||||||
{ display_text: 'Value', value: 'static' },
|
|
||||||
{ display_text: 'From env var', value: 'env' },
|
|
||||||
]}
|
|
||||||
title={h.type === 'env' ? 'From env var' : 'Value'}
|
|
||||||
dataKey={h.type === 'env' ? 'env' : 'static'}
|
|
||||||
onButtonChange={e => this.handleSelectionChange(e, i)}
|
|
||||||
onInputChange={e => {
|
|
||||||
dispatch(setHeaderValue(e.target.value, i));
|
|
||||||
this.addExtraHeader();
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
bsClass={styles.dropdown_button}
|
|
||||||
inputVal={h.value}
|
|
||||||
id={`header-value-${i}`}
|
|
||||||
inputPlaceHolder={
|
|
||||||
h.type === 'env' ? 'HEADER_FROM_ENV' : 'value'
|
|
||||||
}
|
|
||||||
testId={`header-value-${i}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
className={`${styles.fontAwosomeClose}
|
|
||||||
${styles.removeHeader}
|
|
||||||
${
|
|
||||||
i !== modifyTrigger.headers.length - 1 &&
|
|
||||||
'fa-lg fa fa-times'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(removeHeader(i));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
|
||||||
<div className={styles.modifySection}>
|
|
||||||
<h4 className={styles.modifySectionHeading}>
|
|
||||||
Headers{' '}
|
|
||||||
<Tooltip message="Edit headers to be sent along with the event to your webhook" />
|
|
||||||
</h4>
|
|
||||||
<Editor
|
|
||||||
editorCollapsed={collapsed}
|
|
||||||
editorExpanded={expanded}
|
|
||||||
expandCallback={this.setValues}
|
|
||||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
|
||||||
property="headers"
|
|
||||||
service="modify-trigger"
|
|
||||||
saveFunc={save}
|
|
||||||
styles={styles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HeadersEditor;
|
|
@ -1,120 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TableHeader from '../TableCommon/TableHeader';
|
|
||||||
import styles from './ModifyEvent.scss';
|
|
||||||
|
|
||||||
import { getTableColumns } from '../utils';
|
|
||||||
|
|
||||||
import Info from './Info';
|
|
||||||
import WebhookEditor from './WebhookEditor';
|
|
||||||
import OperationEditor from './OperationEditor';
|
|
||||||
import RetryConfEditor from './RetryConfEditor';
|
|
||||||
import HeadersEditor from './HeadersEditor';
|
|
||||||
import ActionButtons from './ActionButtons';
|
|
||||||
|
|
||||||
import { save, setDefaults, RESET_MODIFY_STATE } from './Actions';
|
|
||||||
|
|
||||||
import { NotFoundError } from '../../../Error/PageNotFound';
|
|
||||||
|
|
||||||
class Modify extends React.Component {
|
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(setDefaults());
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: RESET_MODIFY_STATE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
modifyTriggerName,
|
|
||||||
modifyTrigger,
|
|
||||||
triggerList,
|
|
||||||
readOnlyMode,
|
|
||||||
dispatch,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const currentTrigger = triggerList.find(
|
|
||||||
tr => tr.name === modifyTriggerName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentTrigger) {
|
|
||||||
// throw a 404 exception
|
|
||||||
throw new NotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
definition,
|
|
||||||
headers,
|
|
||||||
webhook,
|
|
||||||
webhook_from_env,
|
|
||||||
retry_conf,
|
|
||||||
} = currentTrigger.configuration;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.containerWhole + ' container-fluid'}>
|
|
||||||
<TableHeader
|
|
||||||
dispatch={dispatch}
|
|
||||||
triggerName={modifyTriggerName}
|
|
||||||
tabName="modify"
|
|
||||||
readOnlyMode={readOnlyMode}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Info
|
|
||||||
triggerName={currentTrigger.name}
|
|
||||||
tableName={currentTrigger.table_name}
|
|
||||||
schemaName={currentTrigger.table_schema}
|
|
||||||
styles={styles}
|
|
||||||
/>
|
|
||||||
<WebhookEditor
|
|
||||||
webhook={webhook || webhook_from_env}
|
|
||||||
dispatch={dispatch}
|
|
||||||
modifyTrigger={modifyTrigger}
|
|
||||||
env={Boolean(webhook_from_env)}
|
|
||||||
newWebhook={null}
|
|
||||||
save={() => dispatch(save('webhook', modifyTriggerName))}
|
|
||||||
styles={styles}
|
|
||||||
/>
|
|
||||||
<OperationEditor
|
|
||||||
definition={definition}
|
|
||||||
allTableColumns={getTableColumns(currentTrigger)}
|
|
||||||
dispatch={dispatch}
|
|
||||||
modifyTrigger={modifyTrigger}
|
|
||||||
newDefinition={null}
|
|
||||||
styles={styles}
|
|
||||||
save={() => dispatch(save('ops', modifyTriggerName))}
|
|
||||||
/>
|
|
||||||
<RetryConfEditor
|
|
||||||
retryConf={retry_conf}
|
|
||||||
modifyTrigger={modifyTrigger}
|
|
||||||
styles={styles}
|
|
||||||
save={() => dispatch(save('retry', modifyTriggerName))}
|
|
||||||
serverVersion={this.props.serverVersion}
|
|
||||||
dispatch={dispatch}
|
|
||||||
/>
|
|
||||||
<HeadersEditor
|
|
||||||
headers={headers}
|
|
||||||
styles={styles}
|
|
||||||
modifyTrigger={modifyTrigger}
|
|
||||||
save={() => dispatch(save('headers', modifyTriggerName))}
|
|
||||||
dispatch={dispatch}
|
|
||||||
/>
|
|
||||||
<ActionButtons
|
|
||||||
styles={styles}
|
|
||||||
dispatch={dispatch}
|
|
||||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
|
||||||
triggerName={modifyTriggerName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Modify;
|
|
@ -1,229 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
|
||||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
|
||||||
|
|
||||||
import { toggleQueryType, toggleColumn, toggleManualType } from './Actions';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getTriggerOperations,
|
|
||||||
triggerOperationMap,
|
|
||||||
MANUAL_TRIGGER_VAR,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
class OperationEditor extends React.Component {
|
|
||||||
toggleOperation = upObj => {
|
|
||||||
if (upObj.query === MANUAL_TRIGGER_VAR) {
|
|
||||||
return toggleManualType(upObj);
|
|
||||||
}
|
|
||||||
return toggleQueryType(upObj);
|
|
||||||
};
|
|
||||||
|
|
||||||
setValues = () => {
|
|
||||||
const { dispatch, definition } = this.props;
|
|
||||||
/*
|
|
||||||
* Loop through the keys in definition,
|
|
||||||
* this object will have actual internal name.
|
|
||||||
* No need to transform from display to internal name
|
|
||||||
* */
|
|
||||||
for (const queryType in definition) {
|
|
||||||
/* If the definition[queryType] holds true
|
|
||||||
* This will be false or undefined if `queryType` doesn't exist in the object or definition[queryType] is false.
|
|
||||||
* */
|
|
||||||
if (queryType in definition) {
|
|
||||||
if (queryType !== MANUAL_TRIGGER_VAR) {
|
|
||||||
dispatch(
|
|
||||||
this.toggleOperation({
|
|
||||||
query: queryType,
|
|
||||||
columns: definition[queryType].columns,
|
|
||||||
value: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(
|
|
||||||
this.toggleOperation({
|
|
||||||
query: queryType,
|
|
||||||
value: definition[queryType],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
definition,
|
|
||||||
allTableColumns,
|
|
||||||
styles,
|
|
||||||
save,
|
|
||||||
modifyTrigger,
|
|
||||||
dispatch,
|
|
||||||
} = this.props;
|
|
||||||
/*
|
|
||||||
* Query types will have `CONSOLE_QUERY` only for version > 45
|
|
||||||
*
|
|
||||||
* */
|
|
||||||
const operationTypes = getTriggerOperations();
|
|
||||||
const renderOperation = (qt, i) => {
|
|
||||||
const isChecked = Boolean(definition[triggerOperationMap[qt]]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
styles.opsCheckboxWrapper + ' col-md-2 ' + styles.padd_remove
|
|
||||||
}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className={styles.opsCheckboxDisabled}
|
|
||||||
checked={isChecked}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
{qt}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const collapsed = () => (
|
|
||||||
<div className={styles.modifyOps}>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent}>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{operationTypes.map((qt, i) => renderOperation(qt, i))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent}>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
Listen columns for update:
|
|
||||||
</div>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{definition.update ? (
|
|
||||||
allTableColumns.map((col, i) => (
|
|
||||||
<div
|
|
||||||
className={`${styles.opsCheckboxWrapper} ${styles.columnListElement} ${styles.padd_remove}`}
|
|
||||||
key={i}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className={styles.opsCheckboxDisabled}
|
|
||||||
checked={Boolean(
|
|
||||||
definition.update.columns.find(c => c === col.name)
|
|
||||||
)}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
{col.name}
|
|
||||||
<small className={styles.addPaddSmall}> ({col.type})</small>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'col-md-12 ' +
|
|
||||||
styles.padd_remove +
|
|
||||||
' ' +
|
|
||||||
styles.modifyOpsCollapsedtitle
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i>Applicable only if update operation is selected.</i>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const expanded = () => (
|
|
||||||
<div className={styles.modifyOpsPadLeft}>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent}>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{operationTypes.map((qt, i) => (
|
|
||||||
<div
|
|
||||||
className={`${styles.opsCheckboxWrapper} col-md-2 ${styles.padd_remove} ${styles.cursorPointer}`}
|
|
||||||
key={i}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(
|
|
||||||
this.toggleOperation({
|
|
||||||
query: triggerOperationMap[qt],
|
|
||||||
columns: allTableColumns.map(c => c.name),
|
|
||||||
value: !modifyTrigger.definition[triggerOperationMap[qt]],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className={`${styles.opsCheckbox} ${styles.cursorPointer}`}
|
|
||||||
checked={Boolean(
|
|
||||||
modifyTrigger.definition[triggerOperationMap[qt]]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{qt}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent}>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
Listen columns for update:
|
|
||||||
</div>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{modifyTrigger.definition.update ? (
|
|
||||||
allTableColumns.map((col, i) => (
|
|
||||||
<div
|
|
||||||
className={`${styles.opsCheckboxWrapper} ${styles.columnListElement} ${styles.padd_remove} ${styles.cursorPointer}`}
|
|
||||||
key={i}
|
|
||||||
onClick={() => dispatch(toggleColumn('update', col.name))}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className={`${styles.opsCheckbox} ${styles.cursorPointer}`}
|
|
||||||
checked={Boolean(
|
|
||||||
modifyTrigger.definition.update.columns.find(
|
|
||||||
c => c === col.name
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{col.name}
|
|
||||||
<small className={styles.addPaddSmall}> ({col.type})</small>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'col-md-12 ' +
|
|
||||||
styles.padd_remove +
|
|
||||||
' ' +
|
|
||||||
styles.modifyOpsCollapsedtitle
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i>Applicable only if update operation is selected.</i>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
|
||||||
<div className={styles.modifySection}>
|
|
||||||
<h4 className={styles.modifySectionHeading}>
|
|
||||||
Trigger Operations{' '}
|
|
||||||
<Tooltip message="Edit operations and related columns" />
|
|
||||||
</h4>
|
|
||||||
<Editor
|
|
||||||
editorCollapsed={collapsed}
|
|
||||||
editorExpanded={expanded}
|
|
||||||
styles={styles}
|
|
||||||
property="ops"
|
|
||||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
|
||||||
service="modify-trigger"
|
|
||||||
saveFunc={save}
|
|
||||||
expandCallback={this.setValues}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OperationEditor;
|
|
@ -1,154 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
|
||||||
import {
|
|
||||||
setRetryNum,
|
|
||||||
setRetryInterval,
|
|
||||||
setRetryTimeout,
|
|
||||||
showValidationError,
|
|
||||||
} from './Actions';
|
|
||||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
|
||||||
|
|
||||||
class RetryConfEditor extends React.Component {
|
|
||||||
setValues = () => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const retryConf = this.props.retryConf || {};
|
|
||||||
dispatch(setRetryNum(retryConf.num_retries || 0));
|
|
||||||
dispatch(setRetryInterval(retryConf.interval_sec || 10));
|
|
||||||
dispatch(setRetryTimeout(retryConf.timeout_sec || 60));
|
|
||||||
};
|
|
||||||
|
|
||||||
validateAndSave = () => {
|
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
modifyTrigger: {
|
|
||||||
retryConf: { numRetrys, retryInterval, timeout },
|
|
||||||
},
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const iNumRetries = numRetrys === '' ? 0 : parseInt(numRetrys, 10);
|
|
||||||
const iRetryInterval =
|
|
||||||
retryInterval === '' ? 10 : parseInt(retryInterval, 10);
|
|
||||||
const iTimeout = timeout === '' ? 60 : parseInt(timeout, 10);
|
|
||||||
|
|
||||||
if (iNumRetries < 0 || isNaN(iNumRetries)) {
|
|
||||||
dispatch(
|
|
||||||
showValidationError('Number of retries must be a non negative number!')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setRetryNum(iNumRetries));
|
|
||||||
|
|
||||||
if (iRetryInterval <= 0 || isNaN(iRetryInterval)) {
|
|
||||||
dispatch(showValidationError('Retry interval must be a postive number!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setRetryInterval(iRetryInterval));
|
|
||||||
|
|
||||||
if (isNaN(iTimeout) || iTimeout <= 0) {
|
|
||||||
dispatch(showValidationError('Timeout must be a positive number!'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setRetryTimeout(iTimeout));
|
|
||||||
|
|
||||||
this.props.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { styles, dispatch, modifyTrigger } = this.props;
|
|
||||||
const retryConf = this.props.retryConf || {};
|
|
||||||
|
|
||||||
const collapsed = () => (
|
|
||||||
<div className={styles.modifyOps}>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={'col-md-4 ' + styles.padd_remove}>
|
|
||||||
Number of retries:
|
|
||||||
</div>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{retryConf.num_retries || 0}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={'col-md-4 ' + styles.padd_remove}>
|
|
||||||
Retry Interval (sec):
|
|
||||||
</div>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{retryConf.interval_sec || 10}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={'col-md-4 ' + styles.padd_remove}>Timeout (sec):</div>
|
|
||||||
<div className={'col-md-12 ' + styles.padd_remove}>
|
|
||||||
{retryConf.timeout_sec || 60}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const expanded = () => (
|
|
||||||
<div className={styles.modifyOpsPadLeft}>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
|
||||||
Number of retries:
|
|
||||||
</div>
|
|
||||||
<div className="col-md-12">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={modifyTrigger.retryConf.numRetrys}
|
|
||||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
|
||||||
onChange={e => dispatch(setRetryNum(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
|
||||||
Retry interval (sec):
|
|
||||||
</div>
|
|
||||||
<div className="col-md-12">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
|
||||||
value={modifyTrigger.retryConf.retryInterval}
|
|
||||||
onChange={e => dispatch(setRetryInterval(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.modifyOpsCollapsedContent1}>
|
|
||||||
<div className={`col-md-4 ${styles.padd_remove}`}>
|
|
||||||
Timeout (sec):
|
|
||||||
</div>
|
|
||||||
<div className="col-md-12">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`${styles.input} form-control ${styles.add_mar_right} ${styles.modifyRetryConfTextbox}`}
|
|
||||||
value={modifyTrigger.retryConf.timeout}
|
|
||||||
onChange={e => dispatch(setRetryTimeout(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
|
||||||
<div className={styles.modifySection}>
|
|
||||||
<h4 className={styles.modifySectionHeading}>
|
|
||||||
Retry configuration{' '}
|
|
||||||
<Tooltip message="Edit your retry settings for event failures" />
|
|
||||||
</h4>
|
|
||||||
<Editor
|
|
||||||
editorCollapsed={collapsed}
|
|
||||||
editorExpanded={expanded}
|
|
||||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
|
||||||
property={'retry'}
|
|
||||||
saveFunc={this.validateAndSave}
|
|
||||||
service="modify-trigger"
|
|
||||||
expandCallback={this.setValues}
|
|
||||||
styles={styles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RetryConfEditor;
|
|
@ -1,17 +0,0 @@
|
|||||||
const defaultState = {
|
|
||||||
definition: {},
|
|
||||||
webhookURL: '',
|
|
||||||
webhookUrlType: 'url',
|
|
||||||
retryConf: {
|
|
||||||
numRetrys: 0,
|
|
||||||
retryInterval: 10,
|
|
||||||
timeout: 60,
|
|
||||||
},
|
|
||||||
ongoingRequest: false,
|
|
||||||
lastError: null,
|
|
||||||
internalError: null,
|
|
||||||
lastSuccess: null,
|
|
||||||
headers: [{ key: '', type: 'static', value: '' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defaultState;
|
|
@ -1,114 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Editor from '../../../Common/Layout/ExpandableEditor/Editor';
|
|
||||||
import DropdownButton from '../../../Common/DropdownButton/DropdownButton';
|
|
||||||
import {
|
|
||||||
setWebhookUrl,
|
|
||||||
setWebhookUrlType,
|
|
||||||
showValidationError,
|
|
||||||
} from './Actions';
|
|
||||||
import Tooltip from '../../../Common/Tooltip/Tooltip';
|
|
||||||
|
|
||||||
class WebhookEditor extends React.Component {
|
|
||||||
setValues = () => {
|
|
||||||
const { webhook, env, dispatch } = this.props;
|
|
||||||
dispatch(setWebhookUrl(webhook));
|
|
||||||
dispatch(setWebhookUrlType(env ? 'env' : 'url'));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSelectionChange = e => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(setWebhookUrlType(e.target.getAttribute('value')));
|
|
||||||
dispatch(setWebhookUrl(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
validateAndSave = () => {
|
|
||||||
const { modifyTrigger, dispatch } = this.props;
|
|
||||||
if (modifyTrigger.webhookUrlType === 'url') {
|
|
||||||
let tempUrl = false;
|
|
||||||
try {
|
|
||||||
tempUrl = new URL(modifyTrigger.webhookURL);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
if (!tempUrl) {
|
|
||||||
dispatch(showValidationError('Invalid URL'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
webhook,
|
|
||||||
modifyTrigger,
|
|
||||||
env,
|
|
||||||
dispatch,
|
|
||||||
styles,
|
|
||||||
save: saveWebhook,
|
|
||||||
} = this.props;
|
|
||||||
const collapsed = () => (
|
|
||||||
<div className={styles.modifyProperty}>
|
|
||||||
<p>
|
|
||||||
{webhook}
|
|
||||||
|
|
||||||
</p>
|
|
||||||
<i>{env && '- from env'}</i>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const expanded = () => (
|
|
||||||
<div className={styles.modifyWhDropdownWrapper}>
|
|
||||||
<DropdownButton
|
|
||||||
dropdownOptions={[
|
|
||||||
{ display_text: 'URL', value: 'url' },
|
|
||||||
{ display_text: 'From env var', value: 'env' },
|
|
||||||
]}
|
|
||||||
title={
|
|
||||||
modifyTrigger.webhookUrlType === 'env' ? 'From env var' : 'URL'
|
|
||||||
}
|
|
||||||
dataKey={modifyTrigger.webhookUrlType === 'env' ? 'env' : 'url'}
|
|
||||||
onButtonChange={this.handleSelectionChange}
|
|
||||||
onInputChange={e => dispatch(setWebhookUrl(e.target.value))}
|
|
||||||
required
|
|
||||||
bsClass={styles.dropdown_button}
|
|
||||||
inputVal={modifyTrigger.webhookURL}
|
|
||||||
id="webhook-url"
|
|
||||||
inputPlaceHolder={
|
|
||||||
modifyTrigger.webhookUrlType === 'env'
|
|
||||||
? 'MY_WEBHOOK_URL'
|
|
||||||
: 'http://httpbin.org/post'
|
|
||||||
}
|
|
||||||
testId="webhook"
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
Note: Specifying the webhook URL via an environmental variable is
|
|
||||||
recommended if you have different URLs for multiple environments.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.container} ${styles.borderBottom}`}>
|
|
||||||
<div className={styles.modifySection}>
|
|
||||||
<h4 className={styles.modifySectionHeading}>
|
|
||||||
Webhook URL <Tooltip message="Edit your webhook URL" />
|
|
||||||
</h4>
|
|
||||||
<Editor
|
|
||||||
editorCollapsed={collapsed}
|
|
||||||
editorExpanded={expanded}
|
|
||||||
expandCallback={this.setValues}
|
|
||||||
property="webhook"
|
|
||||||
service="modify-trigger"
|
|
||||||
ongoingRequest={modifyTrigger.ongoingRequest}
|
|
||||||
styles={styles}
|
|
||||||
saveFunc={saveWebhook}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WebhookEditor;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user