mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
add event triggers (#329)
This commit is contained in:
parent
d397d932d6
commit
82e09efce6
@ -32,6 +32,7 @@ var testMetadata = map[string][]byte{
|
||||
tables:
|
||||
- array_relationships: []
|
||||
delete_permissions: []
|
||||
event_triggers: []
|
||||
insert_permissions: []
|
||||
object_relationships: []
|
||||
select_permissions: []
|
||||
|
20
console/cypress/helpers/eventHelpers.js
Normal file
20
console/cypress/helpers/eventHelpers.js
Normal file
@ -0,0 +1,20 @@
|
||||
export const baseUrl = Cypress.config('baseUrl');
|
||||
export const queryTypes = ['insert', 'update', 'delete'];
|
||||
export const getTriggerName = (i, testName = '') =>
|
||||
`apic_test_trigger_${testName}_${i}`;
|
||||
export const getTableName = (i, testName = '') =>
|
||||
`apic_test_table_${testName}_${i}`;
|
||||
export const getWebhookURL = () => 'http://httpbin.org/post';
|
||||
export const getNoOfRetries = () => '5';
|
||||
export const getIntervalSeconds = () => '10';
|
||||
export const getElementFromAlias = alias => `[data-test=${alias}]`;
|
||||
export const makeDataAPIUrl = dataApiUrl => `${dataApiUrl}/v1/query`;
|
||||
export const makeDataAPIOptions = (dataApiUrl, key, body) => ({
|
||||
method: 'POST',
|
||||
url: makeDataAPIUrl(dataApiUrl),
|
||||
headers: {
|
||||
'x-hasura-access-key': key,
|
||||
},
|
||||
body,
|
||||
failOnStatusCode: false,
|
||||
});
|
@ -13,7 +13,7 @@ import { testPermissions, permRemove, createView, trackView } from './utils';
|
||||
const testName = 'perm';
|
||||
|
||||
export const passPTCreateTable = () => {
|
||||
// Click on create tabel
|
||||
// Click on create table
|
||||
cy.get(getElementFromAlias('data-create-table')).click();
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/data/schema/public/table/add`);
|
||||
|
188
console/cypress/integration/events/create-trigger/spec.js
Normal file
188
console/cypress/integration/events/create-trigger/spec.js
Normal file
@ -0,0 +1,188 @@
|
||||
import {
|
||||
getElementFromAlias,
|
||||
getTableName,
|
||||
getTriggerName,
|
||||
getWebhookURL,
|
||||
getNoOfRetries,
|
||||
getIntervalSeconds,
|
||||
baseUrl,
|
||||
} from '../../../helpers/eventHelpers';
|
||||
import { getColName } from '../../../helpers/dataHelpers';
|
||||
import {
|
||||
setMetaData,
|
||||
validateCT,
|
||||
validateCTrigger,
|
||||
validateInsert,
|
||||
} from '../../validators/validators';
|
||||
|
||||
const testName = 'ctr'; // create trigger
|
||||
|
||||
export const visitEventsManagePage = () => {
|
||||
cy.visit('/events/manage');
|
||||
};
|
||||
|
||||
export const passPTCreateTable = () => {
|
||||
// Click on create table
|
||||
cy.get(getElementFromAlias('data-create-table')).click();
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/data/schema/public/table/add`);
|
||||
// Type table name
|
||||
cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName));
|
||||
// Set first column
|
||||
cy.get(getElementFromAlias('column-0')).type(getColName(0));
|
||||
cy.get(getElementFromAlias('col-type-0')).select('serial');
|
||||
// Set second column
|
||||
cy.get(getElementFromAlias('column-1')).type(getColName(1));
|
||||
cy.get(getElementFromAlias('col-type-1')).select('integer');
|
||||
// Set third column
|
||||
cy.get(getElementFromAlias('column-2')).type(getColName(2));
|
||||
cy.get(getElementFromAlias('col-type-2')).select('text');
|
||||
// Set primary key
|
||||
cy.get(getElementFromAlias('primary-key-select-0')).select('0');
|
||||
// Create
|
||||
cy.get(getElementFromAlias('table-create')).click();
|
||||
cy.wait(7000);
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`${baseUrl}/data/schema/public/tables/${getTableName(0, testName)}/modify`
|
||||
);
|
||||
};
|
||||
|
||||
export const checkCreateTriggerRoute = () => {
|
||||
// Click on the create trigger button
|
||||
cy.visit('/events/manage');
|
||||
cy.wait(15000);
|
||||
cy.get(getElementFromAlias('data-create-trigger')).click();
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
};
|
||||
|
||||
export const failCTWithoutData = () => {
|
||||
// Type trigger name
|
||||
cy.get(getElementFromAlias('trigger-name')).type(getTriggerName(0, testName));
|
||||
// Click on create
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
// Check if the route didn't change
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
// Validate
|
||||
validateCT(getTriggerName(0, testName), 'failure');
|
||||
};
|
||||
|
||||
export const passCT = () => {
|
||||
// Set trigger name and select table
|
||||
cy.get(getElementFromAlias('trigger-name'))
|
||||
.clear()
|
||||
.type(getTriggerName(0, testName));
|
||||
cy.get(getElementFromAlias('select-table')).select(getTableName(0, testName));
|
||||
|
||||
// operations
|
||||
cy.get(getElementFromAlias('insert-operation')).check();
|
||||
cy.get(getElementFromAlias('update-operation')).check();
|
||||
cy.get(getElementFromAlias('delete-operation')).check();
|
||||
|
||||
// webhook url
|
||||
cy.get(getElementFromAlias('webhook'))
|
||||
.clear()
|
||||
.type(getWebhookURL());
|
||||
|
||||
// advanced settings
|
||||
cy.get(getElementFromAlias('advanced-settings')).click();
|
||||
|
||||
// retry configuration
|
||||
cy.get(getElementFromAlias('no-of-retries')).type(getNoOfRetries());
|
||||
cy.get(getElementFromAlias('interval-seconds')).type(getIntervalSeconds());
|
||||
|
||||
// Click on create
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
cy.wait(10000);
|
||||
// Check if the trigger got created and navigated to processed events page
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`${baseUrl}/events/manage/triggers/${getTriggerName(0, testName)}/processed`
|
||||
);
|
||||
cy.get(getElementFromAlias(getTriggerName(0, testName)));
|
||||
// Validate
|
||||
validateCTrigger(getTriggerName(0, testName), 'success');
|
||||
};
|
||||
|
||||
export const failCTDuplicateTrigger = () => {
|
||||
// Visit create trigger page
|
||||
cy.visit('/events/manage/triggers/add');
|
||||
// trigger and table name
|
||||
cy.get(getElementFromAlias('trigger-name'))
|
||||
.clear()
|
||||
.type(getTriggerName(0, testName));
|
||||
cy.get(getElementFromAlias('select-table')).select(getTableName(0, testName));
|
||||
|
||||
// operations
|
||||
cy.get(getElementFromAlias('insert-operation')).check();
|
||||
cy.get(getElementFromAlias('update-operation')).check();
|
||||
cy.get(getElementFromAlias('delete-operation')).check();
|
||||
|
||||
// webhook url
|
||||
cy.get(getElementFromAlias('webhook'))
|
||||
.clear()
|
||||
.type(getWebhookURL());
|
||||
|
||||
// click on create
|
||||
cy.get(getElementFromAlias('trigger-create')).click();
|
||||
cy.wait(5000);
|
||||
// should be on the same URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers/add`);
|
||||
};
|
||||
|
||||
export const insertTableRow = () => {
|
||||
// visit insert row page
|
||||
cy.visit(`/data/schema/public/tables/${getTableName(0, testName)}/insert`);
|
||||
// one serial column. so insert a row directly.
|
||||
cy.get(getElementFromAlias(`typed-input-${1}`)).type('123');
|
||||
cy.get(getElementFromAlias(`typed-input-${2}`)).type('Some text');
|
||||
cy.get(getElementFromAlias('insert-save-button')).click();
|
||||
cy.wait(300);
|
||||
validateInsert(getTableName(0, testName), 1);
|
||||
// now it should invoke the trigger to webhook
|
||||
cy.wait(10000);
|
||||
// check if processed events has a row and it is a successful response
|
||||
cy.visit(`/events/manage/triggers/${getTriggerName(0, testName)}/processed`);
|
||||
cy.get(getElementFromAlias('trigger-processed-events')).contains('1');
|
||||
};
|
||||
|
||||
export const deleteCTTestTrigger = () => {
|
||||
// Go to the settings section of the trigger
|
||||
cy.visit(`/events/manage/triggers/${getTriggerName(0, testName)}/processed`);
|
||||
// click on settings tab
|
||||
cy.get(getElementFromAlias('trigger-settings')).click();
|
||||
// Click on delete
|
||||
cy.get(getElementFromAlias('delete-trigger')).click();
|
||||
// Confirm
|
||||
cy.on('window:confirm', str => {
|
||||
expect(str === 'Are you sure?').to.be.true;
|
||||
return true;
|
||||
});
|
||||
cy.wait(7000);
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/events/manage/triggers`);
|
||||
// Validate
|
||||
validateCTrigger(getTriggerName(0, testName), 'success');
|
||||
};
|
||||
|
||||
export const deleteCTTestTable = () => {
|
||||
// Go to the modify section of the table
|
||||
cy.visit(`/data/schema/public/tables/${getTableName(0, testName)}/modify`);
|
||||
// Click on delete
|
||||
cy.get(getElementFromAlias('delete-table')).click();
|
||||
// Confirm
|
||||
cy.on('window:confirm', str => {
|
||||
expect(str === 'Are you sure?').to.be.true;
|
||||
return true;
|
||||
});
|
||||
cy.wait(7000);
|
||||
// Match the URL
|
||||
cy.url().should('eq', `${baseUrl}/data/schema/public`);
|
||||
// Validate
|
||||
validateCT(getTableName(0, testName), 'failure');
|
||||
};
|
||||
|
||||
export const setValidationMetaData = () => {
|
||||
setMetaData();
|
||||
};
|
51
console/cypress/integration/events/create-trigger/test.js
Normal file
51
console/cypress/integration/events/create-trigger/test.js
Normal file
@ -0,0 +1,51 @@
|
||||
/* eslint no-unused-vars: 0 */
|
||||
/* eslint import/prefer-default-export: 0 */
|
||||
import { testMode } from '../../../helpers/common';
|
||||
import { setMetaData } from '../../validators/validators';
|
||||
|
||||
import {
|
||||
passPTCreateTable,
|
||||
visitEventsManagePage,
|
||||
checkCreateTriggerRoute,
|
||||
failCTWithoutData,
|
||||
passCT,
|
||||
failCTDuplicateTrigger,
|
||||
failAddExistingTrigger,
|
||||
insertTableRow,
|
||||
deleteCTTestTrigger,
|
||||
deleteCTTestTable,
|
||||
} from './spec';
|
||||
|
||||
const setup = () => {
|
||||
describe('Check Data Tab', () => {
|
||||
it('Clicking on Data tab opens the correct route', () => {
|
||||
// Visit the index route
|
||||
cy.visit('/data/schema/public');
|
||||
cy.wait(7000);
|
||||
// Get and set validation metadata
|
||||
setMetaData();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const runCreateTriggerTests = () => {
|
||||
describe('Create Trigger', () => {
|
||||
it('Create table to use in triggers', passPTCreateTable);
|
||||
it('Visit events manage page', visitEventsManagePage);
|
||||
it(
|
||||
'Create trigger button opens the correct route',
|
||||
checkCreateTriggerRoute
|
||||
);
|
||||
it('Fails to create trigger without data', failCTWithoutData);
|
||||
it('Successfuly creates trigger', passCT);
|
||||
it('Fails to create duplicate trigger', failCTDuplicateTrigger);
|
||||
it('Insert a row and invoke trigger', insertTableRow);
|
||||
it('Delete off the test trigger', deleteCTTestTrigger);
|
||||
it('Delete off the test table', deleteCTTestTable);
|
||||
});
|
||||
};
|
||||
|
||||
if (testMode !== 'cli') {
|
||||
setup();
|
||||
runCreateTriggerTests();
|
||||
}
|
@ -10,6 +10,8 @@ import { runViewsTest } from './data/views/test';
|
||||
import { runRawSQLTests } from './data/raw-sql/test';
|
||||
import { run404Test } from './data/404/test';
|
||||
|
||||
import { runCreateTriggerTests } from './events/create-trigger/test';
|
||||
|
||||
import { runApiExplorerTests } from './api-explorer/graphql/test';
|
||||
|
||||
const setup = () => {
|
||||
@ -28,6 +30,8 @@ describe('Setup route', setup);
|
||||
|
||||
runMigrationModeTests();
|
||||
|
||||
runCreateTriggerTests();
|
||||
|
||||
runCreateTableTests();
|
||||
|
||||
runInsertBrowseTests();
|
||||
|
@ -216,3 +216,24 @@ export const validateMigrationMode = mode => {
|
||||
expect(response.body.migration_mode == mode.toString()).to.be.true; // eslint-disable-line
|
||||
});
|
||||
};
|
||||
|
||||
// ****************** Trigger Validator *********************
|
||||
|
||||
export const validateCTrigger = (triggerName, result) => {
|
||||
const reqBody = {
|
||||
type: 'select',
|
||||
args: {
|
||||
table: { name: 'event_triggers', schema: 'hdb_catalog' },
|
||||
columns: ['table_name'],
|
||||
where: { name: triggerName },
|
||||
},
|
||||
};
|
||||
const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody);
|
||||
cy.request(requestOptions).then(response => {
|
||||
if (result === 'success') {
|
||||
expect(response.status === 200).to.be.true;
|
||||
} else {
|
||||
expect(response.status === 200).to.be.false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -466,9 +466,13 @@ input {
|
||||
{
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.add_mar_bottom_mid
|
||||
{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.add_mar_right
|
||||
{
|
||||
margin-right: 20px;
|
||||
margin-right: 20px !important;
|
||||
}
|
||||
.add_mar_right_small
|
||||
{
|
||||
|
@ -163,6 +163,27 @@ class Main extends React.Component {
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger placement="right" overlay={tooltip.events}>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === 'events'
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + '/events'}
|
||||
>
|
||||
<div className={styles.iconCenter}>
|
||||
<i
|
||||
title="Events"
|
||||
className="fa fa-cloud"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p>Events</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.clusterInfoWrapper}>{accessKeyHtml}</div>
|
||||
|
@ -9,6 +9,10 @@ export const apiexplorer = (
|
||||
<Tooltip id="tooltip-api-explorer">Test the GraphQL APIs</Tooltip>
|
||||
);
|
||||
|
||||
export const events = (
|
||||
<Tooltip id="tooltip-events">Manage Event Triggers</Tooltip>
|
||||
);
|
||||
|
||||
export const secureEndpoint = (
|
||||
<Tooltip id="tooltip-secure-endpoint">
|
||||
This graphql endpoint is public and you should add an access key
|
||||
|
@ -257,7 +257,7 @@ class AddTable extends Component {
|
||||
if ('default' in column) {
|
||||
defValue = column.default.value;
|
||||
}
|
||||
let defPlaceholder = '';
|
||||
let defPlaceholder = 'default_value';
|
||||
if (column.type === 'timestamptz') {
|
||||
defPlaceholder = 'example: now()';
|
||||
} else if (column.type === 'date') {
|
||||
@ -281,7 +281,9 @@ class AddTable extends Component {
|
||||
/>
|
||||
<select
|
||||
value={column.type}
|
||||
className={`${styles.select} form-control ${styles.add_pad_left}`}
|
||||
className={`${styles.select} ${styles.selectWidth} form-control ${
|
||||
styles.add_pad_left
|
||||
}`}
|
||||
onChange={e => {
|
||||
dispatch(
|
||||
setColType(e.target.value, i, this.refs[`nullable${i}`].checked)
|
||||
@ -324,6 +326,24 @@ class AddTable extends Component {
|
||||
</span>
|
||||
) : null}
|
||||
*/}
|
||||
<input
|
||||
placeholder={defPlaceholder}
|
||||
type="text"
|
||||
value={defValue}
|
||||
className={`${styles.inputDefault} ${
|
||||
styles.defaultWidth
|
||||
} form-control ${styles.add_pad_left}`}
|
||||
onChange={e => {
|
||||
dispatch(
|
||||
setColDefault(
|
||||
e.target.value,
|
||||
i,
|
||||
this.refs[`nullable${i}`].checked
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test={`col-default-${i}`}
|
||||
/>{' '}
|
||||
<input
|
||||
className={`${styles.inputCheckbox} form-control `}
|
||||
checked={columns[i].nullable}
|
||||
@ -346,25 +366,6 @@ class AddTable extends Component {
|
||||
data-test={`unique-${i.toString()}`}
|
||||
/>{' '}
|
||||
<label>Unique</label>
|
||||
<input
|
||||
placeholder={defPlaceholder}
|
||||
type="text"
|
||||
value={defValue}
|
||||
className={`${styles.inputDefault} form-control ${
|
||||
styles.add_pad_left
|
||||
}`}
|
||||
onChange={e => {
|
||||
dispatch(
|
||||
setColDefault(
|
||||
e.target.value,
|
||||
i,
|
||||
this.refs[`nullable${i}`].checked
|
||||
)
|
||||
);
|
||||
}}
|
||||
data-test={`col-default-${i}`}
|
||||
/>{' '}
|
||||
<label>Default</label>
|
||||
{removeIcon}
|
||||
</div>
|
||||
);
|
||||
@ -407,42 +408,14 @@ class AddTable extends Component {
|
||||
</div>
|
||||
);
|
||||
});
|
||||
let alert = null;
|
||||
let createBtnText = 'Create';
|
||||
if (ongoingRequest) {
|
||||
alert = (
|
||||
<div className="hidden col-xs-8">
|
||||
<div className="alert alert-warning" role="alert">
|
||||
Creating...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
createBtnText = 'Creating...';
|
||||
} else if (lastError) {
|
||||
alert = (
|
||||
<div className="hidden col-xs-8">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
Error: {JSON.stringify(lastError)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
createBtnText = 'Creating Failed. Try again';
|
||||
} else if (internalError) {
|
||||
alert = (
|
||||
<div className="hidden col-xs-8">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
Validation Error: {internalError}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
createBtnText = 'Creating Failed. Try again';
|
||||
} else if (lastSuccess) {
|
||||
alert = (
|
||||
<div className="hidden col-xs-8">
|
||||
<div className="alert alert-success" role="alert">
|
||||
Created! Redirecting...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
createBtnText = 'Created! Redirecting...';
|
||||
}
|
||||
|
||||
@ -459,7 +432,6 @@ class AddTable extends Component {
|
||||
</div>
|
||||
<br />
|
||||
<div className={`container-fluid ${styles.padd_left_remove}`}>
|
||||
{alert}
|
||||
<div
|
||||
className={`${styles.addCol} col-xs-12 ${styles.padd_left_remove}`}
|
||||
>
|
||||
|
@ -129,6 +129,13 @@ a.expanded {
|
||||
width: 300px;
|
||||
height: 34px;
|
||||
}
|
||||
.selectWidth {
|
||||
width: 200px;
|
||||
}
|
||||
.defaultWidth {
|
||||
width: 200px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #B85C27;
|
||||
|
296
console/src/components/Services/EventTrigger/Add/AddActions.js
Normal file
296
console/src/components/Services/EventTrigger/Add/AddActions.js
Normal file
@ -0,0 +1,296 @@
|
||||
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import dataHeaders from '../Common/Headers';
|
||||
import requestAction from '../../../../utils/requestAction';
|
||||
import defaultState from './AddState';
|
||||
import _push from '../push';
|
||||
import {
|
||||
loadTriggers,
|
||||
makeMigrationCall,
|
||||
setTrigger,
|
||||
loadProcessedEvents,
|
||||
} from '../EventActions';
|
||||
import { showSuccessNotification } from '../Notification';
|
||||
import { UPDATE_MIGRATION_STATUS_ERROR } from '../../../Main/Actions';
|
||||
|
||||
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 MAKING_REQUEST = 'AddTrigger/MAKING_REQUEST';
|
||||
const REQUEST_SUCCESS = 'AddTrigger/REQUEST_SUCCESS';
|
||||
const REQUEST_ERROR = 'AddTrigger/REQUEST_ERROR';
|
||||
const VALIDATION_ERROR = 'AddTrigger/VALIDATION_ERROR';
|
||||
const UPDATE_TABLE_LIST = 'AddTrigger/UPDATE_TABLE_LIST';
|
||||
const TOGGLE_COLUMNS = 'AddTrigger/TOGGLE_COLUMNS';
|
||||
const TOGGLE_QUERY_TYPE_SELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_SELECTED';
|
||||
const TOGGLE_QUERY_TYPE_DESELECTED = 'AddTrigger/TOGGLE_QUERY_TYPE_DESELECTED';
|
||||
|
||||
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 setDefaults = () => ({ type: SET_DEFAULTS });
|
||||
// General error during validation.
|
||||
// const validationError = (error) => ({type: VALIDATION_ERROR, error: error});
|
||||
const validationError = error => {
|
||||
alert(error);
|
||||
return { type: VALIDATION_ERROR, error };
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// apply migrations
|
||||
const migrationName = 'create_trigger_' + triggerName.trim();
|
||||
const payload = {
|
||||
type: 'create_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
table: { name: tableName, schema: currentSchema },
|
||||
webhook: webhook,
|
||||
},
|
||||
};
|
||||
const downPayload = {
|
||||
type: 'delete_event_trigger',
|
||||
args: {
|
||||
name: triggerName,
|
||||
},
|
||||
};
|
||||
// operation definition
|
||||
if (currentState.selectedOperations.insert) {
|
||||
payload.args.insert = { columns: currentState.operations.insert };
|
||||
}
|
||||
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;
|
||||
}
|
||||
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({ type: REQUEST_SUCCESS });
|
||||
|
||||
dispatch(setTrigger(triggerName.trim()));
|
||||
dispatch(loadTriggers()).then(() => {
|
||||
dispatch(loadProcessedEvents()).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
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const fetchTableListBySchema = schemaName => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'hdb_table',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*.*'],
|
||||
where: { table_schema: schemaName },
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: UPDATE_TABLE_LIST, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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_COLUMNS, cols: columns, op: 'insert' });
|
||||
dispatch({ type: TOGGLE_COLUMNS, cols: columns, op: 'update' });
|
||||
dispatch({ type: TOGGLE_COLUMNS, cols: columns, op: 'delete' });
|
||||
};
|
||||
};
|
||||
|
||||
const setOperationSelection = (type, isChecked) => {
|
||||
return dispatch => {
|
||||
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 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: parseInt(action.value, 10),
|
||||
},
|
||||
};
|
||||
case SET_RETRY_INTERVAL:
|
||||
return {
|
||||
...state,
|
||||
retryConf: {
|
||||
...state.retryConf,
|
||||
interval_sec: parseInt(action.value, 10),
|
||||
},
|
||||
};
|
||||
case SET_TABLENAME:
|
||||
return { ...state, tableName: action.value };
|
||||
case SET_SCHEMANAME:
|
||||
return { ...state, schemaName: action.value };
|
||||
case UPDATE_TABLE_LIST:
|
||||
return { ...state, tableListBySchema: action.data };
|
||||
case TOGGLE_COLUMNS:
|
||||
const operations = state.operations;
|
||||
operations[action.op] = action.cols;
|
||||
return { ...state, operations: { ...operations } };
|
||||
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 } };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default addTriggerReducer;
|
||||
export {
|
||||
setTriggerName,
|
||||
setTableName,
|
||||
setSchemaName,
|
||||
setWebhookURL,
|
||||
setRetryNum,
|
||||
setRetryInterval,
|
||||
createTrigger,
|
||||
fetchTableListBySchema,
|
||||
operationToggleColumn,
|
||||
operationToggleAllColumns,
|
||||
setOperationSelection,
|
||||
setDefaults,
|
||||
};
|
||||
export { validationError };
|
16
console/src/components/Services/EventTrigger/Add/AddState.js
Normal file
16
console/src/components/Services/EventTrigger/Add/AddState.js
Normal file
@ -0,0 +1,16 @@
|
||||
const defaultState = {
|
||||
triggerName: '',
|
||||
tableName: '',
|
||||
schemaName: 'public',
|
||||
tableListBySchema: [],
|
||||
operations: { insert: [], update: [], delete: [] },
|
||||
selectedOperations: { insert: false, update: false, delete: false },
|
||||
webhookURL: '',
|
||||
retryConf: null,
|
||||
ongoingRequest: false,
|
||||
lastError: null,
|
||||
internalError: null,
|
||||
lastSuccess: null,
|
||||
};
|
||||
|
||||
export default defaultState;
|
523
console/src/components/Services/EventTrigger/Add/AddTrigger.js
Normal file
523
console/src/components/Services/EventTrigger/Add/AddTrigger.js
Normal file
@ -0,0 +1,523 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import {
|
||||
setTriggerName,
|
||||
setTableName,
|
||||
setSchemaName,
|
||||
setWebhookURL,
|
||||
setRetryNum,
|
||||
setRetryInterval,
|
||||
operationToggleColumn,
|
||||
operationToggleAllColumns,
|
||||
setOperationSelection,
|
||||
setDefaults,
|
||||
} from './AddActions';
|
||||
import { showErrorNotification } from '../Notification';
|
||||
import { createTrigger } from './AddActions';
|
||||
import { fetchTableListBySchema } from './AddActions';
|
||||
|
||||
class AddTrigger extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props.dispatch(fetchTableListBySchema('public'));
|
||||
this.state = { advancedExpanded: false };
|
||||
}
|
||||
componentDidMount() {
|
||||
// set defaults
|
||||
this.props.dispatch(setDefaults());
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// set defaults
|
||||
this.props.dispatch(setDefaults());
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isNaN(parseInt(this.props.retryConf.num_retries, 10))) {
|
||||
isValid = false;
|
||||
errorMsg = 'Number of retries is not valid';
|
||||
customMsg = 'Numer of retries cannot be empty and can only be numbers';
|
||||
}
|
||||
if (isNaN(parseInt(this.props.retryConf.interval_sec, 10))) {
|
||||
isValid = false;
|
||||
errorMsg = 'Retry interval is not valid';
|
||||
customMsg = 'Retry interval cannot be empty and can only be numbers';
|
||||
}
|
||||
} 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';
|
||||
}
|
||||
}
|
||||
if (isValid) {
|
||||
this.props.dispatch(createTrigger());
|
||||
} else {
|
||||
this.props.dispatch(
|
||||
showErrorNotification('Error creating trigger!', errorMsg, '', {
|
||||
custom: customMsg,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
toggleAdvanced() {
|
||||
this.setState({ advancedExpanded: !this.state.advancedExpanded });
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
tableName,
|
||||
tableListBySchema,
|
||||
schemaName,
|
||||
schemaList,
|
||||
selectedOperations,
|
||||
operations,
|
||||
dispatch,
|
||||
ongoingRequest,
|
||||
lastError,
|
||||
lastSuccess,
|
||||
internalError,
|
||||
} = this.props;
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
let createBtnText = 'Create';
|
||||
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 updateTableList = e => {
|
||||
dispatch(setSchemaName(e.target.value));
|
||||
dispatch(fetchTableListBySchema(e.target.value));
|
||||
};
|
||||
|
||||
const updateTableSelection = e => {
|
||||
dispatch(setTableName(e.target.value));
|
||||
const tableSchema = tableListBySchema.find(
|
||||
t => t.table_name === e.target.value
|
||||
);
|
||||
|
||||
const columns = [];
|
||||
if (tableSchema) {
|
||||
tableSchema.columns.map(colObj => {
|
||||
const column = colObj.column_name;
|
||||
columns.push(column);
|
||||
});
|
||||
}
|
||||
dispatch(operationToggleAllColumns(columns));
|
||||
};
|
||||
|
||||
const handleOperationSelection = e => {
|
||||
dispatch(setOperationSelection(e.target.value, e.target.checked));
|
||||
};
|
||||
|
||||
const getColumnList = type => {
|
||||
const dispatchToggleColumn = e => {
|
||||
const column = e.target.value;
|
||||
dispatch(operationToggleColumn(column, type));
|
||||
};
|
||||
const tableSchema = tableListBySchema.find(
|
||||
t => t.table_name === tableName
|
||||
);
|
||||
|
||||
if (tableSchema) {
|
||||
return tableSchema.columns.map((colObj, i) => {
|
||||
const column = colObj.column_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.display_inline + ' ' + styles.add_mar_right}
|
||||
>
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
{inputHtml}
|
||||
{column}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.addTablesBody} ${styles.main_wrapper} ${
|
||||
styles.padd_left
|
||||
}`}
|
||||
>
|
||||
<Helmet title="Add Trigger - Events | Hasura" />
|
||||
<div className={styles.subHeader}>
|
||||
<h2 className={styles.heading_text}>Add a new 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
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
data-test="trigger-name"
|
||||
placeholder="trigger_name"
|
||||
required
|
||||
pattern="^\w+$"
|
||||
className={`${styles.tableNameInput} form-control`}
|
||||
onChange={e => {
|
||||
dispatch(setTriggerName(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
<h4 className={styles.subheading_text}>
|
||||
Schema/Table
|
||||
</h4>
|
||||
<select
|
||||
onChange={updateTableList}
|
||||
data-test="select-schema"
|
||||
className={styles.selectTrigger + ' form-control'}
|
||||
>
|
||||
{schemaList.map(s => {
|
||||
if (s.schema_name === schemaName) {
|
||||
return (
|
||||
<option
|
||||
value={s.schema_name}
|
||||
key={s.schema_name}
|
||||
selected="selected"
|
||||
>
|
||||
{s.schema_name}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<option value={s.schema_name} key={s.schema_name}>
|
||||
{s.schema_name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<select
|
||||
onChange={updateTableSelection}
|
||||
data-test="select-table"
|
||||
required
|
||||
className={
|
||||
styles.selectTrigger + ' form-control ' + styles.add_mar_left
|
||||
}
|
||||
>
|
||||
<option value="">Select table</option>
|
||||
{tableListBySchema.map(t => {
|
||||
if (t.detail.table_type === 'BASE TABLE') {
|
||||
return (
|
||||
<option key={t.table_name} value={t.table_name}>
|
||||
{t.table_name}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</select>
|
||||
<hr />
|
||||
<div
|
||||
className={
|
||||
styles.add_mar_bottom + ' ' + styles.selectOperations
|
||||
}
|
||||
>
|
||||
<h4 className={styles.subheading_text}>Operations</h4>
|
||||
<div className={styles.display_inline}>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
data-test="insert-operation"
|
||||
className={
|
||||
styles.display_inline + ' ' + styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="insert"
|
||||
checked={selectedOperations.insert}
|
||||
/>
|
||||
Insert
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.add_mar_left}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
data-test="update-operation"
|
||||
className={
|
||||
styles.display_inline + ' ' + styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="update"
|
||||
checked={selectedOperations.update}
|
||||
/>
|
||||
Update
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.add_mar_left}
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
data-test="delete-operation"
|
||||
className={
|
||||
styles.display_inline + ' ' + styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="delete"
|
||||
checked={selectedOperations.delete}
|
||||
/>
|
||||
Delete
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div className={styles.add_mar_bottom}>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Webhook URL
|
||||
</h4>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
data-test="webhook"
|
||||
placeholder="webhook url"
|
||||
className={`${styles.tableNameInput} form-control`}
|
||||
onChange={e => {
|
||||
dispatch(setWebhookURL(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<button
|
||||
onClick={this.toggleAdvanced.bind(this)}
|
||||
data-test="advanced-settings"
|
||||
type="button"
|
||||
className={'btn btn-default ' + styles.advancedToggleBtn}
|
||||
>
|
||||
Advanced Settings
|
||||
{this.state.advancedExpanded ? (
|
||||
<i className={'fa fa-arrow-up'} />
|
||||
) : (
|
||||
<i className={'fa fa-arrow-down'} />
|
||||
)}
|
||||
</button>
|
||||
{this.state.advancedExpanded ? (
|
||||
<div
|
||||
className={
|
||||
styles.advancedOperations +
|
||||
' ' +
|
||||
styles.add_mar_bottom +
|
||||
' ' +
|
||||
styles.add_mar_top
|
||||
}
|
||||
>
|
||||
{tableName ? (
|
||||
<div>
|
||||
<h4 className={styles.subheading_text}>
|
||||
Advanced - Operation/Columns
|
||||
</h4>
|
||||
<div>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
className={
|
||||
styles.display_inline +
|
||||
' ' +
|
||||
styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="insert"
|
||||
checked={selectedOperations.insert}
|
||||
/>
|
||||
Insert
|
||||
</label>
|
||||
</div>
|
||||
{getColumnList('insert')}
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
className={
|
||||
styles.display_inline +
|
||||
' ' +
|
||||
styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="update"
|
||||
checked={selectedOperations.update}
|
||||
/>
|
||||
Update
|
||||
</label>
|
||||
</div>
|
||||
{getColumnList('update')}
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
onChange={handleOperationSelection}
|
||||
className={
|
||||
styles.display_inline +
|
||||
' ' +
|
||||
styles.add_mar_right
|
||||
}
|
||||
type="checkbox"
|
||||
value="delete"
|
||||
checked={selectedOperations.delete}
|
||||
/>
|
||||
Delete
|
||||
</label>
|
||||
</div>
|
||||
{getColumnList('delete')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={styles.add_mar_bottom + ' ' + styles.add_mar_top}
|
||||
>
|
||||
<h4 className={styles.subheading_text}>Retry Logic</h4>
|
||||
<div
|
||||
className={
|
||||
styles.display_inline + ' ' + styles.retrySection
|
||||
}
|
||||
>
|
||||
<label
|
||||
className={
|
||||
styles.add_mar_right + ' ' + styles.retryLabel
|
||||
}
|
||||
>
|
||||
Number of retries
|
||||
</label>
|
||||
<input
|
||||
onChange={e => {
|
||||
dispatch(setRetryNum(e.target.value));
|
||||
}}
|
||||
data-test="no-of-retries"
|
||||
className={styles.display_inline + ' form-control'}
|
||||
type="text"
|
||||
placeholder="no of retries"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
styles.display_inline + ' ' + styles.retrySection
|
||||
}
|
||||
>
|
||||
<label
|
||||
className={
|
||||
styles.add_mar_right + ' ' + styles.retryLabel
|
||||
}
|
||||
>
|
||||
Retry Interval in seconds
|
||||
</label>
|
||||
<input
|
||||
onChange={e => {
|
||||
dispatch(setRetryInterval(e.target.value));
|
||||
}}
|
||||
data-test="interval-seconds"
|
||||
className={styles.display_inline + ' form-control'}
|
||||
type="text"
|
||||
placeholder="interval time in seconds"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<hr />
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn ${styles.yellow_button}`}
|
||||
data-test="trigger-create"
|
||||
>
|
||||
{createBtnText}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddTrigger.propTypes = {
|
||||
triggerName: PropTypes.string,
|
||||
tableName: PropTypes.string,
|
||||
schemaName: PropTypes.string,
|
||||
schemaList: PropTypes.array,
|
||||
tableListBySchema: PropTypes.array,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
const addTriggerConnector = connect => connect(mapStateToProps)(AddTrigger);
|
||||
|
||||
export default addTriggerConnector;
|
82
console/src/components/Services/EventTrigger/Common/DataTypes.js
Executable file
82
console/src/components/Services/EventTrigger/Common/DataTypes.js
Executable file
@ -0,0 +1,82 @@
|
||||
const dataTypes = [
|
||||
{
|
||||
name: 'Integer',
|
||||
value: 'integer',
|
||||
description: 'signed four-byte integer',
|
||||
hasuraDatatype: 'integer',
|
||||
},
|
||||
{
|
||||
name: 'Integer (auto-increment)',
|
||||
value: 'serial',
|
||||
description: 'autoincrementing four-byte integer',
|
||||
hasuraDatatype: null,
|
||||
},
|
||||
{
|
||||
name: 'UUID',
|
||||
value: 'uuid',
|
||||
description: 'universal unique identifier',
|
||||
hasuraDatatype: 'uuid',
|
||||
},
|
||||
{
|
||||
name: 'Big Integer',
|
||||
value: 'bigint',
|
||||
description: 'signed eight-byte integer',
|
||||
hasuraDatatype: 'bigint',
|
||||
},
|
||||
{
|
||||
name: 'Big Integer (auto-increment)',
|
||||
value: 'bigserial',
|
||||
description: 'autoincrementing eight-byte integer',
|
||||
hasuraDatatype: null,
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: 'variable-length character string',
|
||||
hasuraDatatype: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Numeric',
|
||||
value: 'numeric',
|
||||
description: 'exact numeric of selected precision',
|
||||
hasuraDatatype: 'numeric',
|
||||
},
|
||||
{
|
||||
name: 'Date',
|
||||
value: 'date',
|
||||
description: 'calendar date (year, month, day)',
|
||||
hasuraDatatype: 'date',
|
||||
},
|
||||
{
|
||||
name: 'Timestamp',
|
||||
value: 'timestamptz',
|
||||
description: 'date and time, including time zone',
|
||||
hasuraDatatype: 'timestamp with time zone',
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
value: 'timetz',
|
||||
description: 'time of day (no time zone)',
|
||||
hasuraDatatype: 'time with time zone',
|
||||
},
|
||||
{
|
||||
name: 'Boolean',
|
||||
value: 'boolean',
|
||||
description: 'logical Boolean (true/false)',
|
||||
hasuraDatatype: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
value: 'json',
|
||||
description: 'textual JSON data',
|
||||
hasuraDatatype: 'json',
|
||||
},
|
||||
{
|
||||
name: 'JSONB',
|
||||
value: 'jsonb',
|
||||
description: 'binary format JSON data',
|
||||
hasuraDatatype: 'jsonb',
|
||||
},
|
||||
];
|
||||
|
||||
export default dataTypes;
|
@ -0,0 +1,34 @@
|
||||
const gqlPattern = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
||||
|
||||
const gqlTableErrorNotif = [
|
||||
'Error creating table!',
|
||||
'Table name cannot contain special characters',
|
||||
'',
|
||||
{
|
||||
custom:
|
||||
'Table name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
|
||||
},
|
||||
];
|
||||
|
||||
const gqlColumnErrorNotif = [
|
||||
'Error adding column!',
|
||||
'Column name cannot contain special characters',
|
||||
'',
|
||||
{
|
||||
custom:
|
||||
'Column name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
|
||||
},
|
||||
];
|
||||
|
||||
const gqlRelErrorNotif = [
|
||||
'Error adding relationship!',
|
||||
'Relationship name cannot contain special characters',
|
||||
'',
|
||||
{
|
||||
custom:
|
||||
'Relationship name cannot contain special characters. It can have alphabets, numbers (cannot start with numbers) and _ (can start with _)',
|
||||
},
|
||||
];
|
||||
|
||||
export default gqlPattern;
|
||||
export { gqlTableErrorNotif, gqlColumnErrorNotif, gqlRelErrorNotif };
|
@ -0,0 +1,4 @@
|
||||
const dataHeaders = currentState => {
|
||||
return currentState().tables.dataHeaders;
|
||||
};
|
||||
export default dataHeaders;
|
@ -0,0 +1,18 @@
|
||||
import Endpoints from '../../../../Endpoints';
|
||||
import globals from '../../../../Globals';
|
||||
|
||||
const returnMigrateUrl = mode => {
|
||||
if (globals.consoleMode === 'cli') {
|
||||
return mode ? Endpoints.hasuractlMigrate : Endpoints.hasuractlMetadata;
|
||||
} else if (globals.consoleMode === 'hasuradb') {
|
||||
let finalUrl;
|
||||
if (globals.nodeEnv === 'development') {
|
||||
finalUrl = globals.devDataApiUrl + '/v1/query';
|
||||
} else {
|
||||
finalUrl = Endpoints.query;
|
||||
}
|
||||
return finalUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export default returnMigrateUrl;
|
490
console/src/components/Services/EventTrigger/EventActions.js
Normal file
490
console/src/components/Services/EventTrigger/EventActions.js
Normal file
@ -0,0 +1,490 @@
|
||||
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 { showErrorNotification, showSuccessNotification } from './Notification';
|
||||
import dataHeaders from './Common/Headers';
|
||||
import { loadMigrationStatus } from '../../Main/Actions';
|
||||
import returnMigrateUrl from './Common/getMigrateUrl';
|
||||
import globals from '../../../Globals';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
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 ACCESS_KEY_ERROR = 'Event/ACCESS_KEY_ERROR';
|
||||
const UPDATE_DATA_HEADERS = 'Event/UPDATE_DATA_HEADERS';
|
||||
const LISTING_TRIGGER = 'Event/LISTING_TRIGGER';
|
||||
const LOAD_EVENT_LOGS = 'Event/LOAD_EVENT_LOGS';
|
||||
|
||||
const MAKE_REQUEST = 'Event/MAKE_REQUEST';
|
||||
const REQUEST_SUCCESS = 'Event/REQUEST_SUCCESS';
|
||||
const REQUEST_ERROR = 'Event/REQUEST_ERROR';
|
||||
|
||||
/* ************ action creators *********************** */
|
||||
const loadTriggers = () => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: ['*'],
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_TRIGGER_LIST, triggerList: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadProcessedEvents = () => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
type: 'select',
|
||||
args: {
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: {
|
||||
$or: [{ delivered: { $eq: true } }, { error: { $eq: true } }],
|
||||
},
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_PROCESSED_EVENTS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const loadPendingEvents = () => (dispatch, getState) => {
|
||||
const url = Endpoints.getSchema;
|
||||
const options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
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 },
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
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 options = {
|
||||
credentials: globalCookiePolicy,
|
||||
method: 'POST',
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify({
|
||||
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 } },
|
||||
order_by: ['-created_at'],
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
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 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: { trigger_name: triggerName } },
|
||||
order_by: ['-created_at'],
|
||||
limit: 20,
|
||||
},
|
||||
}),
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch({ type: LOAD_EVENT_LOGS, data: data });
|
||||
},
|
||||
error => {
|
||||
console.error('Failed to load triggers' + JSON.stringify(error));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setTrigger = triggerName => ({ type: SET_TRIGGER, triggerName });
|
||||
|
||||
/* **********Shared functions between table actions********* */
|
||||
|
||||
const handleMigrationErrors = (title, errorMsg) => dispatch => {
|
||||
const requestMsg = title;
|
||||
if (globals.consoleMode === 'hasuradb') {
|
||||
// handle errors for run_sql based workflow
|
||||
dispatch(showErrorNotification(title, errorMsg.code, requestMsg, errorMsg));
|
||||
} else if (errorMsg.code === 'migration_failed') {
|
||||
dispatch(
|
||||
showErrorNotification(title, 'Migration Failed', requestMsg, errorMsg)
|
||||
);
|
||||
} else if (errorMsg.code === 'data_api_error') {
|
||||
const parsedErrorMsg = errorMsg;
|
||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
||||
dispatch(
|
||||
showErrorNotification(
|
||||
title,
|
||||
parsedErrorMsg.message.error,
|
||||
requestMsg,
|
||||
parsedErrorMsg
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// any other unhandled codes
|
||||
const parsedErrorMsg = errorMsg;
|
||||
parsedErrorMsg.message = JSON.parse(errorMsg.message);
|
||||
dispatch(
|
||||
showErrorNotification(title, errorMsg.code, requestMsg, parsedErrorMsg)
|
||||
);
|
||||
}
|
||||
// dispatch(showErrorNotification(msg, firstDisplay, request, response));
|
||||
};
|
||||
|
||||
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 === 'hasuradb') {
|
||||
finalReqBody = upQuery;
|
||||
} else if (globals.consoleMode === 'cli') {
|
||||
finalReqBody = migrationBody;
|
||||
}
|
||||
const url = migrateUrl;
|
||||
const options = {
|
||||
method: 'POST',
|
||||
credentials: globalCookiePolicy,
|
||||
headers: dataHeaders(getState),
|
||||
body: JSON.stringify(finalReqBody),
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
if (globals.consoleMode === 'cli') {
|
||||
dispatch(loadMigrationStatus()); // don't call for hasuradb mode
|
||||
}
|
||||
dispatch(loadTriggers());
|
||||
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 triggerList = getState().triggers.triggerList;
|
||||
const currentTriggerInfo = triggerList.filter(
|
||||
t => t.name === triggerName
|
||||
)[0];
|
||||
console.log(currentTriggerInfo);
|
||||
// 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.schema_name,
|
||||
},
|
||||
webhook: currentTriggerInfo.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(loadTriggers()).then(() => dispatch(push('/events/manage')));
|
||||
return;
|
||||
};
|
||||
const customOnError = () => {
|
||||
dispatch({ type: REQUEST_ERROR, data: errorMsg });
|
||||
return;
|
||||
};
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
upQuery.args,
|
||||
downQuery.args,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/* ******************************************************* */
|
||||
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 ACCESS_KEY_ERROR:
|
||||
return { ...state, accessKeyError: action.data };
|
||||
case UPDATE_DATA_HEADERS:
|
||||
return { ...state, dataHeaders: action.data };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default eventReducer;
|
||||
export {
|
||||
setTrigger,
|
||||
loadTriggers,
|
||||
deleteTrigger,
|
||||
loadProcessedEvents,
|
||||
loadPendingEvents,
|
||||
loadRunningEvents,
|
||||
loadEventLogs,
|
||||
handleMigrationErrors,
|
||||
makeMigrationCall,
|
||||
ACCESS_KEY_ERROR,
|
||||
UPDATE_DATA_HEADERS,
|
||||
LISTING_TRIGGER,
|
||||
};
|
71
console/src/components/Services/EventTrigger/EventHeader.js
Normal file
71
console/src/components/Services/EventTrigger/EventHeader.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
import PageContainer from './PageContainer/PageContainer';
|
||||
|
||||
const appPrefix = '/events';
|
||||
|
||||
const EventHeader = ({
|
||||
schema,
|
||||
currentSchema,
|
||||
children,
|
||||
location,
|
||||
dispatch,
|
||||
}) => {
|
||||
const styles = require('../Data/TableCommon/Table.scss');
|
||||
const currentLocation = location.pathname;
|
||||
return (
|
||||
<div>
|
||||
<Helmet title={'Events | Hasura'} />
|
||||
<div className={styles.wd20 + ' ' + styles.align_left}>
|
||||
<div
|
||||
className={styles.pageSidebar + ' col-xs-12 ' + styles.padd_remove}
|
||||
>
|
||||
<div>
|
||||
<ul>
|
||||
<li
|
||||
role="presentation"
|
||||
className={
|
||||
currentLocation.indexOf('schema') !== -1 ? styles.active : ''
|
||||
}
|
||||
>
|
||||
<div className={styles.schemaWrapper}>
|
||||
<div
|
||||
className={styles.schemaSidebarSection}
|
||||
data-test="schema"
|
||||
>
|
||||
<Link
|
||||
className={styles.schemaBorder}
|
||||
to={appPrefix + '/manage'}
|
||||
>
|
||||
Manage
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer
|
||||
location={location}
|
||||
schema={schema}
|
||||
currentSchema={currentSchema}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.wd80}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
schema: state.tables.allSchemas,
|
||||
schemaList: state.tables.schemaList,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
};
|
||||
};
|
||||
|
||||
const eventHeaderConnector = connect => connect(mapStateToProps)(EventHeader);
|
||||
|
||||
export default eventHeaderConnector;
|
@ -0,0 +1,9 @@
|
||||
import eventTriggerReducer from './EventActions';
|
||||
import addTriggerReducer from './Add/AddActions';
|
||||
|
||||
const eventReducer = {
|
||||
triggers: eventTriggerReducer,
|
||||
addTrigger: addTriggerReducer,
|
||||
};
|
||||
|
||||
export default eventReducer;
|
192
console/src/components/Services/EventTrigger/EventRouter.js
Normal file
192
console/src/components/Services/EventTrigger/EventRouter.js
Normal file
@ -0,0 +1,192 @@
|
||||
import React from 'react';
|
||||
// import {push} fropm 'react-router-redux';
|
||||
import { Route, IndexRedirect } from 'react-router';
|
||||
import globals from '../../../Globals';
|
||||
|
||||
import {
|
||||
schemaConnector,
|
||||
schemaContainerConnector,
|
||||
addTriggerConnector,
|
||||
processedEventsConnector,
|
||||
pendingEventsConnector,
|
||||
runningEventsConnector,
|
||||
eventHeaderConnector,
|
||||
settingsConnector,
|
||||
streamingLogsConnector,
|
||||
} from '.';
|
||||
|
||||
import {
|
||||
loadTriggers,
|
||||
loadProcessedEvents,
|
||||
loadPendingEvents,
|
||||
loadRunningEvents,
|
||||
} from '../EventTrigger/EventActions';
|
||||
|
||||
const makeEventRouter = (
|
||||
connect,
|
||||
store,
|
||||
composeOnEnterHooks,
|
||||
requireSchema,
|
||||
requireProcessedEvents,
|
||||
requirePendingEvents,
|
||||
requireRunningEvents,
|
||||
migrationRedirects
|
||||
) => {
|
||||
return (
|
||||
<Route
|
||||
path="events"
|
||||
component={eventHeaderConnector(connect)}
|
||||
onEnter={composeOnEnterHooks([requireSchema])}
|
||||
>
|
||||
<IndexRedirect to="manage" />
|
||||
<Route path="manage" component={schemaContainerConnector(connect)}>
|
||||
<IndexRedirect to="triggers" />
|
||||
<Route path="triggers" component={schemaConnector(connect)} />
|
||||
<Route
|
||||
path="triggers/:trigger/processed"
|
||||
component={processedEventsConnector(connect)}
|
||||
onEnter={composeOnEnterHooks([requireProcessedEvents])}
|
||||
/>
|
||||
<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/settings"
|
||||
component={settingsConnector(connect)}
|
||||
/>
|
||||
<Route
|
||||
path="triggers/:trigger/logs"
|
||||
component={streamingLogsConnector(connect)}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="manage/triggers/add"
|
||||
onEnter={composeOnEnterHooks([migrationRedirects])}
|
||||
component={addTriggerConnector(connect)}
|
||||
/>
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
|
||||
const eventRouter = (connect, store, composeOnEnterHooks) => {
|
||||
const requireSchema = (nextState, replaceState, cb) => {
|
||||
// check if access key is available in localstorage. if so use that.
|
||||
// if localstorage access key didn't work, redirect to login (meaning value has changed)
|
||||
// if access key is not available in localstorage, check if cli is giving it via window.__env
|
||||
// if access key is not available in localstorage and cli, make a api call to data without access key.
|
||||
// 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 requireProcessedEvents = (nextState, replaceState, cb) => {
|
||||
const {
|
||||
triggers: { processedEvents },
|
||||
} = store.getState();
|
||||
if (processedEvents.length) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
Promise.all([store.dispatch(loadProcessedEvents())]).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();
|
||||
}
|
||||
);
|
||||
};
|
||||
const migrationRedirects = (nextState, replaceState, cb) => {
|
||||
const state = store.getState();
|
||||
if (!state.main.migrationMode) {
|
||||
replaceState(globals.urlPrefix + '/events/manage');
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
};
|
||||
const consoleModeRedirects = (nextState, replaceState, cb) => {
|
||||
if (globals.consoleMode === 'hasuradb') {
|
||||
replaceState(globals.urlPrefix + '/events/manage');
|
||||
cb();
|
||||
}
|
||||
cb();
|
||||
};
|
||||
return {
|
||||
makeEventRouter: makeEventRouter(
|
||||
connect,
|
||||
store,
|
||||
composeOnEnterHooks,
|
||||
requireSchema,
|
||||
requireProcessedEvents,
|
||||
requirePendingEvents,
|
||||
requireRunningEvents,
|
||||
migrationRedirects,
|
||||
consoleModeRedirects
|
||||
),
|
||||
requireSchema,
|
||||
migrationRedirects,
|
||||
};
|
||||
};
|
||||
|
||||
export default eventRouter;
|
75
console/src/components/Services/EventTrigger/EventState.js
Normal file
75
console/src/components/Services/EventTrigger/EventState.js
Normal file
@ -0,0 +1,75 @@
|
||||
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: 20,
|
||||
offset: 0,
|
||||
order_by: ['-created_at'],
|
||||
},
|
||||
rows: [],
|
||||
expandedRow: '',
|
||||
count: 0,
|
||||
curFilter: defaultCurFilter,
|
||||
activePath: [],
|
||||
ongoingRequest: false,
|
||||
lastError: {},
|
||||
lastSuccess: {},
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
currentTrigger: null,
|
||||
view: { ...defaultViewState },
|
||||
log: { ...defaultLogState },
|
||||
triggerList: [],
|
||||
listingTrigger: [],
|
||||
processedEvents: [],
|
||||
pendingEvents: [],
|
||||
runningEvents: [],
|
||||
eventLogs: [],
|
||||
schemaList: ['public'],
|
||||
currentSchema: 'public',
|
||||
accessKeyError: false,
|
||||
dataHeaders: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultState;
|
||||
export { defaultViewState, defaultLogState, defaultCurFilter };
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Helmet from 'react-helmet';
|
||||
import 'brace/mode/sql';
|
||||
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import Toggle from 'react-toggle';
|
||||
import { updateMigrationModeStatus } from '../../../Main/Actions';
|
||||
import './ReactToggle.css';
|
||||
|
||||
const migrationTip = (
|
||||
<Tooltip id="tooltip-migration">
|
||||
Modifications to the underlying postgres schema should be tracked as
|
||||
migrations.
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const MigrationsHome = ({ dispatch, migrationMode }) => {
|
||||
const styles = require('./Styles.scss');
|
||||
const handleMigrationModeToggle = () => {
|
||||
const isConfirm = window.confirm('Are you sure?');
|
||||
if (isConfirm) {
|
||||
dispatch(updateMigrationModeStatus());
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={'container-fluid'}>
|
||||
<Helmet title="Migrations - Data | Hasura" />
|
||||
<div className={styles.add_mar_top}>
|
||||
<OverlayTrigger placement="right" overlay={migrationTip}>
|
||||
<i className={'fa fa-info-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
<div className={styles.migration_mode}>
|
||||
<label>
|
||||
<span> Allow postgres schema changes </span>
|
||||
<Toggle
|
||||
checked={migrationMode}
|
||||
icons={false}
|
||||
onChange={handleMigrationModeToggle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.add_mar_top}>
|
||||
<b>Note</b>
|
||||
<ul className={styles.remove_ul_left + ' ' + styles.add_mar_top_small}>
|
||||
<li>
|
||||
Recommend that you turn this off if you're working with an existing
|
||||
app or database.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MigrationsHome.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state.rawSQL,
|
||||
migrationMode: state.main.migrationMode,
|
||||
});
|
||||
|
||||
const migrationsConnector = connect => connect(mapStateToProps)(MigrationsHome);
|
||||
|
||||
export default migrationsConnector;
|
@ -0,0 +1,13 @@
|
||||
.react-toggle-track {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.react-toggle--checked .react-toggle-thumb {
|
||||
left: 21px;
|
||||
}
|
||||
|
||||
.react-toggle-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
@import "../../../Common/Common.scss";
|
||||
.migration_mode {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
span {
|
||||
margin-right: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
160
console/src/components/Services/EventTrigger/Notification.js
Normal file
160
console/src/components/Services/EventTrigger/Notification.js
Normal file
@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import { showNotification, showTempNotification } from '../../App/Actions';
|
||||
import { notifExpand, notifMsg } from '../../App/Actions';
|
||||
|
||||
const styles = require('./TableCommon/Table.scss');
|
||||
|
||||
const showErrorNotification = (title, message, reqBody, error) => {
|
||||
let modMessage;
|
||||
let refreshBtn;
|
||||
if (
|
||||
error &&
|
||||
error.message &&
|
||||
(error.message.error === 'postgres query error' ||
|
||||
error.message.error === 'query execution failed')
|
||||
) {
|
||||
if (error.message.internal) {
|
||||
modMessage =
|
||||
error.message.code + ': ' + error.message.internal.error.message;
|
||||
} else {
|
||||
modMessage = error.code + ': ' + error.message.error;
|
||||
}
|
||||
} else if (error && 'info' in error) {
|
||||
modMessage = error.info;
|
||||
} else if (error && 'message' in error) {
|
||||
if (error.code) {
|
||||
if (error.message.error) {
|
||||
modMessage = error.message.error.message;
|
||||
} else {
|
||||
modMessage = error.message;
|
||||
}
|
||||
} else if (error && error.message && 'code' in error.message) {
|
||||
modMessage = error.message.code + ' : ' + message;
|
||||
} else {
|
||||
modMessage = error.code;
|
||||
}
|
||||
} else if (error && 'internal' in error) {
|
||||
modMessage = error.code + ' : ' + error.internal.error.message;
|
||||
} else if (error && 'custom' in error) {
|
||||
modMessage = error.custom;
|
||||
} else if (error && 'code' in error && 'error' in error && 'path' in error) {
|
||||
// Data API error
|
||||
modMessage = error.error;
|
||||
} else {
|
||||
modMessage = error ? error : message;
|
||||
}
|
||||
let finalJson = error ? error.message : '{}';
|
||||
if (error && 'action' in error) {
|
||||
refreshBtn = (
|
||||
<button
|
||||
className={styles.yellow_button + ' ' + styles.add_mar_top_small}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Refresh Console
|
||||
</button>
|
||||
);
|
||||
finalJson = error.action;
|
||||
} else if (error && 'internal' in error) {
|
||||
finalJson = error.internal;
|
||||
}
|
||||
return dispatch => {
|
||||
const expandClicked = finalMsg => {
|
||||
// trigger a modal with a bigger view
|
||||
dispatch(notifExpand(true));
|
||||
dispatch(notifMsg(JSON.stringify(finalMsg, null, 4)));
|
||||
};
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'error',
|
||||
title,
|
||||
message: modMessage,
|
||||
action: reqBody
|
||||
? {
|
||||
label: 'Details',
|
||||
callback: () => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'error',
|
||||
title,
|
||||
message: modMessage,
|
||||
dismissible: 'button',
|
||||
children: [
|
||||
<div className={styles.aceBlock}>
|
||||
<i
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
expandClicked(finalJson);
|
||||
}}
|
||||
className={styles.aceBlockExpand + ' fa fa-expand'}
|
||||
/>
|
||||
<AceEditor
|
||||
readOnly
|
||||
showPrintMargin={false}
|
||||
mode="json"
|
||||
showGutter={false}
|
||||
theme="github"
|
||||
name="notification-response"
|
||||
value={JSON.stringify(finalJson, null, 4)}
|
||||
minLines={1}
|
||||
maxLines={15}
|
||||
width="100%"
|
||||
/>
|
||||
{refreshBtn}
|
||||
</div>,
|
||||
],
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
: null,
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showSuccessNotification = (title, message) => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
level: 'success',
|
||||
title,
|
||||
message: message ? message : null,
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showTempErrorNotification = (title, message) => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showTempNotification({
|
||||
level: 'error',
|
||||
title,
|
||||
message: message ? message : null,
|
||||
autoDismiss: 3,
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const showInfoNotification = title => {
|
||||
return dispatch => {
|
||||
dispatch(
|
||||
showNotification({
|
||||
title,
|
||||
autoDismiss: 0,
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
showInfoNotification,
|
||||
showTempErrorNotification,
|
||||
};
|
17
console/src/components/Services/EventTrigger/Operators.js
Normal file
17
console/src/components/Services/EventTrigger/Operators.js
Normal file
@ -0,0 +1,17 @@
|
||||
const Operators = [
|
||||
{ name: 'equals', value: '$eq' },
|
||||
{ name: 'not equals', value: '$ne' },
|
||||
{ name: 'in', value: '$in' },
|
||||
{ name: 'not in', value: '$nin' },
|
||||
{ name: '>', value: '$gt' },
|
||||
{ name: '<', value: '$lt' },
|
||||
{ name: '>=', value: '$gte' },
|
||||
{ name: '<=', value: '$lte' },
|
||||
{ name: 'like', value: '$like' },
|
||||
{ name: 'not like', value: '$nlike' },
|
||||
{ name: 'ilike', value: '$ilike' },
|
||||
{ name: 'not ilike', value: '$nilike' },
|
||||
{ name: 'similar', value: '$similar' },
|
||||
{ name: 'not similar', value: '$nsimilar' },
|
||||
];
|
||||
export default Operators;
|
@ -0,0 +1,32 @@
|
||||
/* State
|
||||
|
||||
{
|
||||
ongoingRequest : false, //true if request is going on
|
||||
lastError : null OR <string>
|
||||
lastSuccess: null OR <string>
|
||||
}
|
||||
|
||||
*/
|
||||
import defaultState from './State';
|
||||
|
||||
const SET_USERNAME = 'PageContainer/SET_USERNAME';
|
||||
|
||||
// HTML Component defines what state it needs
|
||||
// HTML Component should be able to emit actions
|
||||
// When an action happens, the state is modified (using the reducer function)
|
||||
// When the state is modified, anybody dependent on the state is asked to update
|
||||
// HTML Component is listening to state, hence re-renders
|
||||
|
||||
const homeReducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case SET_USERNAME:
|
||||
return { username: action.data };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const setUsername = username => ({ type: SET_USERNAME, data: username });
|
||||
|
||||
export default homeReducer;
|
||||
export { setUsername };
|
@ -0,0 +1,144 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import globals from '../../../../Globals';
|
||||
|
||||
import { LISTING_TRIGGER } from '../EventActions';
|
||||
|
||||
const appPrefix = '/events';
|
||||
|
||||
const PageContainer = ({
|
||||
currentTrigger,
|
||||
triggerList,
|
||||
listingTrigger,
|
||||
migrationMode,
|
||||
children,
|
||||
dispatch,
|
||||
location,
|
||||
}) => {
|
||||
const styles = require('./PageContainer.scss');
|
||||
// Now schema might be null or an empty array
|
||||
let triggerLinks = (
|
||||
<li className={styles.noTables}>
|
||||
<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.activeTable;
|
||||
}
|
||||
return (
|
||||
<li className={activeTableClass} key={i}>
|
||||
<Link
|
||||
to={appPrefix + '/manage/triggers/' + trigger + '/processed'}
|
||||
data-test={trigger}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{trigger}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.schemaTableList}>
|
||||
<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" />
|
||||
<input
|
||||
type="text"
|
||||
onChange={triggerSearch.bind(this)}
|
||||
className="form-control"
|
||||
placeholder="search triggers"
|
||||
data-test="search-triggers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.sidebarHeadingWrapper}>
|
||||
<div
|
||||
className={
|
||||
'col-xs-8 ' +
|
||||
styles.sidebarHeading +
|
||||
' ' +
|
||||
styles.padd_left_remove
|
||||
}
|
||||
>
|
||||
Triggers ({triggerList.length})
|
||||
</div>
|
||||
{migrationMode ? (
|
||||
<div
|
||||
className={
|
||||
'col-xs-4 text-center ' +
|
||||
styles.padd_remove +
|
||||
' ' +
|
||||
styles.sidebarCreateTable
|
||||
}
|
||||
>
|
||||
<Link
|
||||
className={styles.padd_remove_full}
|
||||
to={'/events/manage/triggers/add'}
|
||||
>
|
||||
<button
|
||||
className={styles.add_mar_right + ' btn btn-xs btn-default'}
|
||||
data-test="sidebar-add-table"
|
||||
>
|
||||
Add Trigger
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<ul className={styles.schemaListUl} data-test="table-links">
|
||||
{triggerLinks}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentTrigger: state.triggers.currentTrigger,
|
||||
triggerList: state.triggers.triggerList,
|
||||
listingTrigger: state.triggers.listingTrigger,
|
||||
migrationMode: state.main.migrationMode,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(PageContainer);
|
@ -0,0 +1,169 @@
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
@import "../../../Common/Common.scss";
|
||||
.container {
|
||||
}
|
||||
.displayFlexContainer
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
.flexRow {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.add_btn {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 20px 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.changeSchema {
|
||||
margin-left: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: calc(100vh - 26px);
|
||||
overflow: auto;
|
||||
// background: #444;
|
||||
// color: $navbar-inverse-color;
|
||||
color: #333;
|
||||
border: 1px solid #E5E5E5;
|
||||
background-color: #F8F8F8;
|
||||
/*
|
||||
a,a:visited {
|
||||
color: $navbar-inverse-link-color;
|
||||
}
|
||||
a:hover {
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
}
|
||||
*/
|
||||
hr {
|
||||
margin: 0;
|
||||
border-color: $navbar-inverse-color;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-top: 10px;
|
||||
padding-left: 7px;
|
||||
li {
|
||||
padding: 7px 0;
|
||||
transition: color 0.5s;
|
||||
/*
|
||||
a,a:visited {
|
||||
color: $navbar-inverse-link-color;
|
||||
}
|
||||
a:hover {
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
}
|
||||
*/
|
||||
|
||||
a
|
||||
{
|
||||
color: #767E93;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
li:hover {
|
||||
padding: 7px 0;
|
||||
// color: $navbar-inverse-link-hover-color;
|
||||
transition: color 0.5s;
|
||||
pointer: cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0;
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebarSearch {
|
||||
margin-right: 20px;
|
||||
padding: 10px 0px;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
i
|
||||
{
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
padding-left: 8px;
|
||||
color: #979797;
|
||||
}
|
||||
input
|
||||
{
|
||||
padding-left: 25px;
|
||||
}
|
||||
}
|
||||
.sidebarHeadingWrapper
|
||||
{
|
||||
width: 100%;
|
||||
float: left;
|
||||
padding-bottom: 10px;
|
||||
.sidebarHeading {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
color: #767E93;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.schemaTableList {
|
||||
// max-height: 300px;
|
||||
// overflow-y: auto;
|
||||
overflow: auto;
|
||||
padding-left: 20px;
|
||||
max-height: calc(100vh - 275px);
|
||||
}
|
||||
.schemaListUl {
|
||||
padding-left: 5px;
|
||||
padding-bottom: 10px;
|
||||
li {
|
||||
border-bottom: 0px !important;
|
||||
padding: 0 0 !important;
|
||||
a {
|
||||
background: transparent !important;
|
||||
padding: 5px 0px !important;
|
||||
font-weight: 400 !important;
|
||||
padding-left: 5px !important;
|
||||
.tableIcon {
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.noTables {
|
||||
font-weight: 400 !important;
|
||||
padding-bottom: 10px !important;
|
||||
color: #767E93 !important;
|
||||
}
|
||||
li:first-child {
|
||||
padding-top: 15px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.heading_tooltip {
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.addAllBtn {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.activeTable {
|
||||
a
|
||||
{
|
||||
// border-left: 4px solid #FFC627;
|
||||
color: #FD9540!important;
|
||||
}
|
||||
}
|
||||
|
||||
.floatRight {
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
const defaultState = {
|
||||
username: 'Guest User',
|
||||
};
|
||||
|
||||
export default defaultState;
|
@ -0,0 +1,246 @@
|
||||
import { defaultCurFilter } from '../EventState';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const LOADING = 'PendingEvents/FilterQuery/LOADING';
|
||||
|
||||
const SET_DEFQUERY = 'PendingEvents/FilterQuery/SET_DEFQUERY';
|
||||
const SET_FILTERCOL = 'PendingEvents/FilterQuery/SET_FILTERCOL';
|
||||
const SET_FILTEROP = 'PendingEvents/FilterQuery/SET_FILTEROP';
|
||||
const SET_FILTERVAL = 'PendingEvents/FilterQuery/SET_FILTERVAL';
|
||||
const ADD_FILTER = 'PendingEvents/FilterQuery/ADD_FILTER';
|
||||
const REMOVE_FILTER = 'PendingEvents/FilterQuery/REMOVE_FILTER';
|
||||
|
||||
const SET_ORDERCOL = 'PendingEvents/FilterQuery/SET_ORDERCOL';
|
||||
const SET_ORDERTYPE = 'PendingEvents/FilterQuery/SET_ORDERTYPE';
|
||||
const ADD_ORDER = 'PendingEvents/FilterQuery/ADD_ORDER';
|
||||
const REMOVE_ORDER = 'PendingEvents/FilterQuery/REMOVE_ORDER';
|
||||
const SET_LIMIT = 'PendingEvents/FilterQuery/SET_LIMIT';
|
||||
const SET_OFFSET = 'PendingEvents/FilterQuery/SET_OFFSET';
|
||||
const SET_NEXTPAGE = 'PendingEvents/FilterQuery/SET_NEXTPAGE';
|
||||
const SET_PREVPAGE = 'PendingEvents/FilterQuery/SET_PREVPAGE';
|
||||
|
||||
const setLoading = () => ({ type: LOADING, data: true });
|
||||
const unsetLoading = () => ({ type: LOADING, data: false });
|
||||
const setDefaultQuery = curQuery => ({ type: SET_DEFQUERY, curQuery });
|
||||
const setFilterCol = (name, index) => ({ type: SET_FILTERCOL, name, index });
|
||||
const setFilterOp = (opName, index) => ({ type: SET_FILTEROP, opName, index });
|
||||
const setFilterVal = (val, index) => ({ type: SET_FILTERVAL, val, index });
|
||||
const addFilter = () => ({ type: ADD_FILTER });
|
||||
const removeFilter = index => ({ type: REMOVE_FILTER, index });
|
||||
|
||||
const setOrderCol = (name, index) => ({ type: SET_ORDERCOL, name, index });
|
||||
const setOrderType = (order, index) => ({ type: SET_ORDERTYPE, order, index });
|
||||
const addOrder = () => ({ type: ADD_ORDER });
|
||||
const removeOrder = index => ({ type: REMOVE_ORDER, index });
|
||||
|
||||
const setLimit = limit => ({ type: SET_LIMIT, limit });
|
||||
const setOffset = offset => ({ type: SET_OFFSET, offset });
|
||||
const setNextPage = () => ({ type: SET_NEXTPAGE });
|
||||
const setPrevPage = () => ({ type: SET_PREVPAGE });
|
||||
|
||||
const runQuery = triggerSchema => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState().triggers.view.curFilter;
|
||||
const finalWhereClauses = state.where.$and.filter(w => {
|
||||
const colName = Object.keys(w)[0].trim();
|
||||
if (colName === '') {
|
||||
return false;
|
||||
}
|
||||
const opName = Object.keys(w[colName])[0].trim();
|
||||
if (opName === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const newQuery = {
|
||||
where: { $and: finalWhereClauses },
|
||||
limit: state.limit,
|
||||
offset: state.offset,
|
||||
order_by: state.order_by.filter(w => w.column.trim() !== ''),
|
||||
};
|
||||
if (newQuery.where.$and.length === 0) {
|
||||
delete newQuery.where;
|
||||
}
|
||||
if (newQuery.order_by.length === 0) {
|
||||
delete newQuery.order_by;
|
||||
}
|
||||
dispatch({ type: 'PendingEvents/V_SET_QUERY_OPTS', queryStuff: newQuery });
|
||||
dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
|
||||
const pendingFilterReducer = (state = defaultCurFilter, action) => {
|
||||
const i = action.index;
|
||||
const newFilter = {};
|
||||
switch (action.type) {
|
||||
case SET_DEFQUERY:
|
||||
const q = action.curQuery;
|
||||
if (
|
||||
'order_by' in q ||
|
||||
'limit' in q ||
|
||||
'offset' in q ||
|
||||
('where' in q && '$and' in q.where)
|
||||
) {
|
||||
const newCurFilterQ = {};
|
||||
newCurFilterQ.where =
|
||||
'where' in q && '$and' in q.where
|
||||
? { $and: [...q.where.$and, { '': { '': '' } }] }
|
||||
: { ...defaultCurFilter.where };
|
||||
newCurFilterQ.order_by =
|
||||
'order_by' in q
|
||||
? [...q.order_by, ...defaultCurFilter.order_by]
|
||||
: [...defaultCurFilter.order_by];
|
||||
newCurFilterQ.limit = 'limit' in q ? q.limit : defaultCurFilter.limit;
|
||||
newCurFilterQ.offset =
|
||||
'offset' in q ? q.offset : defaultCurFilter.offset;
|
||||
return newCurFilterQ;
|
||||
}
|
||||
return defaultCurFilter;
|
||||
case SET_FILTERCOL:
|
||||
const oldColName = Object.keys(state.where.$and[i])[0];
|
||||
newFilter[action.name] = { ...state.where.$and[i][oldColName] };
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTEROP:
|
||||
const colName = Object.keys(state.where.$and[i])[0];
|
||||
const oldOp = Object.keys(state.where.$and[i][colName])[0];
|
||||
newFilter[colName] = {};
|
||||
newFilter[colName][action.opName] = state.where.$and[i][colName][oldOp];
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTERVAL:
|
||||
const colName1 = Object.keys(state.where.$and[i])[0];
|
||||
const opName = Object.keys(state.where.$and[i][colName1])[0];
|
||||
newFilter[colName1] = {};
|
||||
newFilter[colName1][opName] = action.val;
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case ADD_FILTER:
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [...state.where.$and, { '': { '': '' } }],
|
||||
},
|
||||
};
|
||||
case REMOVE_FILTER:
|
||||
const newFilters = [
|
||||
...state.where.$and.slice(0, i),
|
||||
...state.where.$and.slice(i + 1),
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
where: { $and: newFilters },
|
||||
};
|
||||
|
||||
case SET_ORDERCOL:
|
||||
const oldOrder = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder, column: action.name },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case SET_ORDERTYPE:
|
||||
const oldOrder1 = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder1, type: action.order },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case REMOVE_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case ADD_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by,
|
||||
{ column: '', type: 'asc', nulls: 'last' },
|
||||
],
|
||||
};
|
||||
|
||||
case SET_LIMIT:
|
||||
return {
|
||||
...state,
|
||||
limit: action.limit,
|
||||
};
|
||||
case SET_OFFSET:
|
||||
return {
|
||||
...state,
|
||||
offset: action.offset,
|
||||
};
|
||||
case SET_NEXTPAGE:
|
||||
return {
|
||||
...state,
|
||||
offset: state.offset + state.limit,
|
||||
};
|
||||
case SET_PREVPAGE:
|
||||
const newOffset = state.offset - state.limit;
|
||||
return {
|
||||
...state,
|
||||
offset: newOffset < 0 ? 0 : newOffset,
|
||||
};
|
||||
case LOADING:
|
||||
return {
|
||||
...state,
|
||||
loading: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default pendingFilterReducer;
|
||||
export {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
setLimit,
|
||||
setOffset,
|
||||
setNextPage,
|
||||
setPrevPage,
|
||||
setDefaultQuery,
|
||||
setLoading,
|
||||
unsetLoading,
|
||||
runQuery,
|
||||
};
|
@ -0,0 +1,265 @@
|
||||
/*
|
||||
Use state exactly the way columns in create table do.
|
||||
dispatch actions using a given function,
|
||||
but don't listen to state.
|
||||
derive everything through viewtable as much as possible.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Operators from '../Operators';
|
||||
import {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
} from './FilterActions.js';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
} from './FilterActions.js';
|
||||
import { setDefaultQuery, runQuery } from './FilterActions';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const renderCols = (colName, triggerSchema, onChange, usage, key) => {
|
||||
const columns = ['id', 'delivered', 'created_at'];
|
||||
return (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={colName.trim()}
|
||||
data-test={
|
||||
usage === 'sort' ? `sort-column-${key}` : `filter-column-${key}`
|
||||
}
|
||||
>
|
||||
{colName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{columns.map((c, i) => (
|
||||
<option key={i} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOps = (opName, onChange, key) => (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={opName.trim()}
|
||||
data-test={`filter-op-${key}`}
|
||||
>
|
||||
{opName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{Operators.map((o, i) => (
|
||||
<option key={i} value={o.value}>
|
||||
{o.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const renderWheres = (whereAnd, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return whereAnd.map((clause, i) => {
|
||||
const colName = Object.keys(clause)[0];
|
||||
const opName = Object.keys(clause[colName])[0];
|
||||
const dSetFilterCol = e => {
|
||||
dispatch(setFilterCol(e.target.value, i));
|
||||
};
|
||||
const dSetFilterOp = e => {
|
||||
dispatch(setFilterOp(e.target.value, i));
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < whereAnd.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeFilter(i));
|
||||
}}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-4">
|
||||
{renderCols(colName, triggerSchema, dSetFilterCol, 'filter', i)}
|
||||
</div>
|
||||
<div className="col-xs-3">{renderOps(opName, dSetFilterOp, i)}</div>
|
||||
<div className="col-xs-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="-- value --"
|
||||
value={clause[colName][opName]}
|
||||
onChange={e => {
|
||||
dispatch(setFilterVal(e.target.value, i));
|
||||
if (i + 1 === whereAnd.length) {
|
||||
dispatch(addFilter());
|
||||
}
|
||||
}}
|
||||
data-test={`filter-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderSorts = (orderBy, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return orderBy.map((c, i) => {
|
||||
const dSetOrderCol = e => {
|
||||
dispatch(setOrderCol(e.target.value, i));
|
||||
if (i + 1 === orderBy.length) {
|
||||
dispatch(addOrder());
|
||||
}
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < orderBy.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeOrder(i));
|
||||
}}
|
||||
data-test={`clear-sorts-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-6">
|
||||
{renderCols(c.column, triggerSchema, dSetOrderCol, 'sort', i)}
|
||||
</div>
|
||||
<div className="col-xs-5">
|
||||
<select
|
||||
value={c.type}
|
||||
className="form-control"
|
||||
onChange={e => {
|
||||
dispatch(setOrderType(e.target.value, i));
|
||||
}}
|
||||
data-test={`sort-order-${i}`}
|
||||
>
|
||||
<option value="asc">Asc</option>
|
||||
<option value="desc">Desc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-1 text-center">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
class FilterQuery extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isWatching: false, intervalId: null };
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(setDefaultQuery(this.props.curQuery));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
|
||||
watchChanges() {
|
||||
// set state on watch
|
||||
this.setState({ isWatching: !this.state.isWatching });
|
||||
if (this.state.isWatching) {
|
||||
clearInterval(this.state.intervalId);
|
||||
} else {
|
||||
const intervalId = setInterval(this.refreshData, 2000);
|
||||
this.setState({ intervalId: intervalId });
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.props.dispatch(vMakeRequest());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, whereAnd, triggerSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return (
|
||||
<div className={styles.filterOptions}>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<span className={styles.subheading_text}>Filter</span>
|
||||
{renderWheres(whereAnd, triggerSchema, dispatch)}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<b className={styles.subheading_text}>Sort</b>
|
||||
{renderSorts(orderBy, triggerSchema, dispatch)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.padd_right} ${styles.clear_fix}`}>
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn ${styles.yellow_button}`}
|
||||
data-test="run-query"
|
||||
>
|
||||
Run query
|
||||
</button>
|
||||
<button
|
||||
onClick={this.watchChanges.bind(this)}
|
||||
className={styles.add_mar_left + ' btn btn-default'}
|
||||
data-test="run-query"
|
||||
>
|
||||
{this.state.isWatching ? (
|
||||
<span>
|
||||
Watching <i className={'fa fa-spinner fa-spin'} />
|
||||
</span>
|
||||
) : (
|
||||
'Watch'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuery.propTypes = {
|
||||
curQuery: PropTypes.object.isRequired,
|
||||
triggerSchema: PropTypes.object.isRequired,
|
||||
whereAnd: PropTypes.array.isRequired,
|
||||
orderBy: PropTypes.array.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
count: PropTypes.number,
|
||||
triggerName: PropTypes.string,
|
||||
offset: PropTypes.number.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
@ -0,0 +1,95 @@
|
||||
@import "../../../Common/Common.scss";
|
||||
$bgColor: #f9f9f9;
|
||||
.container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline-block;
|
||||
max-height: 34px;
|
||||
padding: 5px 20px;
|
||||
margin-left: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.queryBox {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-height: 30px;
|
||||
.inputRow {
|
||||
margin: 20px 0;
|
||||
div[class^=col-xs-] {
|
||||
padding-left: 0;
|
||||
padding-right: 2.5px;
|
||||
}
|
||||
:global(.form-control) {
|
||||
height: 35px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.fa) {
|
||||
padding-top: 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
}
|
||||
.descending {
|
||||
padding-left: 10px;
|
||||
input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.runQuery {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 20px;
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.input-group) {
|
||||
margin-top: -24px;
|
||||
margin-right: 10px;
|
||||
input {
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
margin-top: -24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterOptions {
|
||||
margin-top: 10px;
|
||||
background: $bgColor;
|
||||
// padding: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boxHeading {
|
||||
top: -0.55em;
|
||||
line-height: 1.1em;
|
||||
background: $bgColor;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
padding: 0 10px;
|
||||
color: #929292;
|
||||
font-weight: normal;
|
||||
}
|
@ -0,0 +1,473 @@
|
||||
import { defaultViewState } from '../EventState';
|
||||
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import requestAction from 'utils/requestAction';
|
||||
import pendingFilterReducer from './FilterActions';
|
||||
import { findTableFromRel } from '../utils';
|
||||
import dataHeaders from '../Common/Headers';
|
||||
|
||||
/* ****************** View actions *************/
|
||||
const V_SET_DEFAULTS = 'PendingEvents/V_SET_DEFAULTS';
|
||||
const V_REQUEST_SUCCESS = 'PendingEvents/V_REQUEST_SUCCESS';
|
||||
const V_REQUEST_ERROR = 'PendingEvents/V_REQUEST_ERROR';
|
||||
const V_EXPAND_REL = 'PendingEvents/V_EXPAND_REL';
|
||||
const V_CLOSE_REL = 'PendingEvents/V_CLOSE_REL';
|
||||
const V_SET_ACTIVE = 'PendingEvents/V_SET_ACTIVE';
|
||||
const V_SET_QUERY_OPTS = 'PendingEvents/V_SET_QUERY_OPTS';
|
||||
const V_REQUEST_PROGRESS = 'PendingEvents/V_REQUEST_PROGRESS';
|
||||
const V_EXPAND_ROW = 'PendingEvents/V_EXPAND_ROW';
|
||||
const V_COLLAPSE_ROW = 'PendingEvents/V_COLLAPSE_ROW';
|
||||
|
||||
/* ****************** action creators *************/
|
||||
|
||||
const vExpandRow = rowKey => ({
|
||||
type: V_EXPAND_ROW,
|
||||
data: rowKey,
|
||||
});
|
||||
|
||||
const vCollapseRow = () => ({
|
||||
type: V_COLLAPSE_ROW,
|
||||
});
|
||||
|
||||
const vSetDefaults = () => ({ type: V_SET_DEFAULTS });
|
||||
|
||||
const vMakeRequest = () => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const url = Endpoints.query;
|
||||
const originalTrigger = getState().triggers.currentTrigger;
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: true });
|
||||
const currentQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
// count query
|
||||
const countQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
countQuery.columns = ['id'];
|
||||
|
||||
// delivered = false and error = false
|
||||
// where clause for relationship
|
||||
const currentWhereClause = state.triggers.view.query.where;
|
||||
if (currentWhereClause && currentWhereClause.$and) {
|
||||
// make filter for events
|
||||
const finalAndClause = currentQuery.where.$and;
|
||||
finalAndClause.push({ delivered: false });
|
||||
finalAndClause.push({ error: false });
|
||||
finalAndClause.push({ tries: 0 });
|
||||
currentQuery.columns[1].where = { $and: finalAndClause };
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where.$and.push({
|
||||
trigger_name: state.triggers.currentTrigger,
|
||||
});
|
||||
} else {
|
||||
// reset where for events
|
||||
if (currentQuery.columns[1]) {
|
||||
currentQuery.columns[1].where = {
|
||||
delivered: false,
|
||||
error: false,
|
||||
tries: 0,
|
||||
};
|
||||
}
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where = {
|
||||
trigger_name: state.triggers.currentTrigger,
|
||||
delivered: false,
|
||||
error: false,
|
||||
tries: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// order_by for relationship
|
||||
const currentOrderBy = state.triggers.view.query.order_by;
|
||||
if (currentOrderBy) {
|
||||
currentQuery.columns[1].order_by = currentOrderBy;
|
||||
// reset order_by
|
||||
delete currentQuery.order_by;
|
||||
} else {
|
||||
// reset order by for events
|
||||
if (currentQuery.columns[1]) {
|
||||
delete currentQuery.columns[1].order_by;
|
||||
currentQuery.columns[1].order_by = ['-created_at'];
|
||||
}
|
||||
delete currentQuery.order_by;
|
||||
}
|
||||
|
||||
// limit and offset for relationship
|
||||
const currentLimit = state.triggers.view.query.limit;
|
||||
const currentOffset = state.triggers.view.query.offset;
|
||||
currentQuery.columns[1].limit = currentLimit;
|
||||
currentQuery.columns[1].offset = currentOffset;
|
||||
|
||||
// reset limit and offset for parent
|
||||
delete currentQuery.limit;
|
||||
delete currentQuery.offset;
|
||||
delete countQuery.limit;
|
||||
delete countQuery.offset;
|
||||
|
||||
const requestBody = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
args: {
|
||||
...currentQuery,
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'count',
|
||||
args: {
|
||||
...countQuery,
|
||||
table: {
|
||||
name: 'event_log',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: dataHeaders(getState),
|
||||
credentials: globalCookiePolicy,
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
const currentTrigger = getState().triggers.currentTrigger;
|
||||
if (originalTrigger === currentTrigger) {
|
||||
Promise.all([
|
||||
dispatch({
|
||||
type: V_REQUEST_SUCCESS,
|
||||
data: data[0],
|
||||
count: data[1].count,
|
||||
}),
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: false }),
|
||||
]);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch({ type: V_REQUEST_ERROR, data: error });
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const vExpandRel = (path, relname, pk) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_EXPAND_REL, path, relname, pk });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
const vCloseRel = (path, relname) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_CLOSE_REL, path, relname });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
/* ************ helpers ************************/
|
||||
const defaultSubQuery = (relname, tableSchema) => {
|
||||
return {
|
||||
name: relname,
|
||||
columns: tableSchema.columns.map(c => c.column_name),
|
||||
};
|
||||
};
|
||||
|
||||
const expandQuery = (
|
||||
curQuery,
|
||||
curTable,
|
||||
pk,
|
||||
curPath,
|
||||
relname,
|
||||
schemas,
|
||||
isObjRel = false
|
||||
) => {
|
||||
if (curPath.length === 0) {
|
||||
const rel = curTable.relationships.find(r => r.rel_name === relname);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, rel);
|
||||
|
||||
const newColumns = [
|
||||
...curQuery.columns,
|
||||
defaultSubQuery(relname, childTableSchema),
|
||||
];
|
||||
if (isObjRel) {
|
||||
return { ...curQuery, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's already oldStuff then don't reset it
|
||||
if ('oldStuff' in curQuery) {
|
||||
return { ...curQuery, where: pk, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's no oldStuff then set it
|
||||
const oldStuff = {};
|
||||
['where', 'limit', 'offset'].map(k => {
|
||||
if (k in curQuery) {
|
||||
oldStuff[k] = curQuery[k];
|
||||
}
|
||||
});
|
||||
return { name: curQuery.name, where: pk, columns: newColumns, oldStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
expandQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
pk,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas,
|
||||
curRel.rel_type === 'object'
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const closeQuery = (curQuery, curTable, curPath, relname, schemas) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
if (curPath.length === 0) {
|
||||
const expandedIndex = curQuery.columns.findIndex(c => c.name === relname);
|
||||
const newColumns = [
|
||||
...curQuery.columns.slice(0, expandedIndex),
|
||||
...curQuery.columns.slice(expandedIndex + 1),
|
||||
];
|
||||
const newStuff = {};
|
||||
newStuff.columns = newColumns;
|
||||
if ('name' in curQuery) {
|
||||
newStuff.name = curQuery.name;
|
||||
}
|
||||
// If no other expanded columns are left
|
||||
if (!newColumns.find(c => typeof c === 'object')) {
|
||||
if (curQuery.oldStuff) {
|
||||
['where', 'limit', 'order_by', 'offset'].map(k => {
|
||||
if (k in curQuery.oldStuff) {
|
||||
newStuff[k] = curQuery.oldStuff[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
return { ...newStuff };
|
||||
}
|
||||
return { ...curQuery, ...newStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
closeQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const setActivePath = (activePath, curPath, relname, query) => {
|
||||
const basePath = relname
|
||||
? [activePath[0], ...curPath, relname]
|
||||
: [activePath[0], ...curPath];
|
||||
|
||||
// Now check if there are any more children on this path.
|
||||
// If there are, then we should expand them by default
|
||||
let subQuery = query;
|
||||
let subBase = basePath.slice(1);
|
||||
|
||||
while (subBase.length > 0) {
|
||||
subQuery = subQuery.columns.find(c => c.name === subBase[0]); // eslint-disable-line no-loop-func
|
||||
subBase = subBase.slice(1);
|
||||
}
|
||||
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
while (subQuery) {
|
||||
basePath.push(subQuery.name);
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
}
|
||||
|
||||
return basePath;
|
||||
};
|
||||
const updateActivePathOnClose = (
|
||||
activePath,
|
||||
tableName,
|
||||
curPath,
|
||||
relname,
|
||||
query
|
||||
) => {
|
||||
const basePath = [tableName, ...curPath, relname];
|
||||
let subBase = [...basePath];
|
||||
let subActive = [...activePath];
|
||||
let matchingFound = false;
|
||||
let commonIndex = 0;
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
|
||||
while (subActive.length > 0) {
|
||||
if (subBase[0] === subActive[0]) {
|
||||
matchingFound = true;
|
||||
break;
|
||||
}
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
commonIndex += 1;
|
||||
}
|
||||
|
||||
if (matchingFound) {
|
||||
const newActivePath = activePath.slice(0, commonIndex + 1);
|
||||
return setActivePath(
|
||||
newActivePath,
|
||||
newActivePath.slice(1, -1),
|
||||
null,
|
||||
query
|
||||
);
|
||||
}
|
||||
return [...activePath];
|
||||
};
|
||||
const addQueryOptsActivePath = (query, queryStuff, activePath) => {
|
||||
let curPath = activePath.slice(1);
|
||||
const newQuery = { ...query };
|
||||
let curQuery = newQuery;
|
||||
while (curPath.length > 0) {
|
||||
curQuery = curQuery.columns.find(c => c.name === curPath[0]); // eslint-disable-line no-loop-func
|
||||
curPath = curPath.slice(1);
|
||||
}
|
||||
|
||||
['where', 'order_by', 'limit', 'offset'].map(k => {
|
||||
delete curQuery[k];
|
||||
});
|
||||
|
||||
for (const k in queryStuff) {
|
||||
if (queryStuff.hasOwnProperty(k)) {
|
||||
curQuery[k] = queryStuff[k];
|
||||
}
|
||||
}
|
||||
return newQuery;
|
||||
};
|
||||
/* ****************** reducer ******************/
|
||||
const pendingEventsReducer = (triggerName, triggerList, viewState, action) => {
|
||||
if (action.type.indexOf('PendingEvents/FilterQuery/') === 0) {
|
||||
return {
|
||||
...viewState,
|
||||
curFilter: pendingFilterReducer(viewState.curFilter, action),
|
||||
};
|
||||
}
|
||||
const tableSchema = triggerList.find(x => x.name === triggerName);
|
||||
switch (action.type) {
|
||||
case V_SET_DEFAULTS:
|
||||
return {
|
||||
...defaultViewState,
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: { delivered: false, error: false },
|
||||
},
|
||||
],
|
||||
limit: 10,
|
||||
},
|
||||
activePath: [triggerName],
|
||||
rows: [],
|
||||
count: null,
|
||||
};
|
||||
case V_SET_QUERY_OPTS:
|
||||
return {
|
||||
...viewState,
|
||||
query: addQueryOptsActivePath(
|
||||
viewState.query,
|
||||
action.queryStuff,
|
||||
viewState.activePath
|
||||
),
|
||||
};
|
||||
case V_EXPAND_REL:
|
||||
return {
|
||||
...viewState,
|
||||
query: expandQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.pk,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
),
|
||||
activePath: [...viewState.activePath, action.relname],
|
||||
};
|
||||
case V_CLOSE_REL:
|
||||
const _query = closeQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
);
|
||||
return {
|
||||
...viewState,
|
||||
query: _query,
|
||||
activePath: updateActivePathOnClose(
|
||||
viewState.activePath,
|
||||
triggerName,
|
||||
action.path,
|
||||
action.relname,
|
||||
_query
|
||||
),
|
||||
};
|
||||
case V_SET_ACTIVE:
|
||||
return {
|
||||
...viewState,
|
||||
activePath: setActivePath(
|
||||
viewState.activePath,
|
||||
action.path,
|
||||
action.relname,
|
||||
viewState.query
|
||||
),
|
||||
};
|
||||
case V_REQUEST_SUCCESS:
|
||||
return { ...viewState, rows: action.data, count: action.count };
|
||||
case V_REQUEST_PROGRESS:
|
||||
return { ...viewState, isProgressing: action.data };
|
||||
case V_EXPAND_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: action.data,
|
||||
};
|
||||
case V_COLLAPSE_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: '',
|
||||
};
|
||||
default:
|
||||
return viewState;
|
||||
}
|
||||
};
|
||||
|
||||
export default pendingEventsReducer;
|
||||
export {
|
||||
vSetDefaults,
|
||||
vMakeRequest,
|
||||
vExpandRel,
|
||||
vCloseRel,
|
||||
vExpandRow,
|
||||
vCollapseRow,
|
||||
V_SET_ACTIVE,
|
||||
};
|
@ -0,0 +1,403 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/json';
|
||||
import Tabs from 'react-bootstrap/lib/Tabs';
|
||||
import Tab from 'react-bootstrap/lib/Tab';
|
||||
import 'react-table/react-table.css';
|
||||
import { deleteItem, vExpandRow, vCollapseRow } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import FilterQuery from './FilterQuery';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
removeOrder,
|
||||
runQuery,
|
||||
setOffset,
|
||||
setLimit,
|
||||
addOrder,
|
||||
} from './FilterActions';
|
||||
import { ordinalColSort } from '../utils';
|
||||
import Spinner from '../../../Common/Spinner/Spinner';
|
||||
import '../TableCommon/ReactTableFix.css';
|
||||
|
||||
const ViewRows = ({
|
||||
curTriggerName,
|
||||
curQuery,
|
||||
curFilter,
|
||||
curRows,
|
||||
curPath,
|
||||
curDepth,
|
||||
activePath,
|
||||
triggerList,
|
||||
dispatch,
|
||||
isProgressing,
|
||||
isView,
|
||||
count,
|
||||
expandedRow,
|
||||
}) => {
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
const triggerSchema = triggerList.find(x => x.name === curTriggerName);
|
||||
const curRelName = curPath.length > 0 ? curPath.slice(-1)[0] : null;
|
||||
|
||||
// Am I a single row display
|
||||
const isSingleRow = false;
|
||||
|
||||
// Get the headings
|
||||
const tableHeadings = [];
|
||||
const gridHeadings = [];
|
||||
const eventLogColumns = ['id', 'delivered', 'created_at'];
|
||||
const sortedColumns = eventLogColumns.sort(ordinalColSort);
|
||||
|
||||
sortedColumns.map((column, i) => {
|
||||
tableHeadings.push(<th key={i}>{column}</th>);
|
||||
gridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
|
||||
const hasPrimaryKeys = true;
|
||||
/*
|
||||
let editButton;
|
||||
let deleteButton;
|
||||
*/
|
||||
|
||||
const newCurRows = [];
|
||||
if (curRows && curRows[0] && curRows[0].events) {
|
||||
curRows[0].events.forEach((row, rowIndex) => {
|
||||
const newRow = {};
|
||||
const pkClause = {};
|
||||
if (!isView && hasPrimaryKeys) {
|
||||
pkClause.id = row.id;
|
||||
} else {
|
||||
triggerSchema.map(k => {
|
||||
pkClause[k] = row[k];
|
||||
});
|
||||
}
|
||||
/*
|
||||
if (!isSingleRow && !isView && hasPrimaryKeys) {
|
||||
deleteButton = (
|
||||
<button
|
||||
className={`${styles.add_mar_right_small} btn btn-xs btn-default`}
|
||||
onClick={() => {
|
||||
dispatch(deleteItem(pkClause));
|
||||
}}
|
||||
data-test={`row-delete-button-${rowIndex}`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const buttonsDiv = (
|
||||
<div className={styles.tableCellCenterAligned}>
|
||||
{editButton}
|
||||
{deleteButton}
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
// Insert Edit, Delete, Clone in a cell
|
||||
// newRow.actions = buttonsDiv;
|
||||
// Insert cells corresponding to all rows
|
||||
sortedColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (row[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let content = row[col] === undefined ? 'NULL' : row[col].toString();
|
||||
if (col === 'created_at') {
|
||||
content = new Date(row[col]).toUTCString();
|
||||
}
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
newCurRows.push(newRow);
|
||||
});
|
||||
}
|
||||
|
||||
// Is this ViewRows visible
|
||||
let isVisible = false;
|
||||
if (!curRelName) {
|
||||
isVisible = true;
|
||||
} else if (curRelName === activePath[curDepth]) {
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
let filterQuery = null;
|
||||
if (!isSingleRow) {
|
||||
if (curRelName === activePath[curDepth] || curDepth === 0) {
|
||||
// Rendering only if this is the activePath or this is the root
|
||||
|
||||
let wheres = [{ '': { '': '' } }];
|
||||
if ('where' in curFilter && '$and' in curFilter.where) {
|
||||
wheres = [...curFilter.where.$and];
|
||||
}
|
||||
|
||||
let orderBy = [{ column: '', type: 'asc', nulls: 'last' }];
|
||||
if ('order_by' in curFilter) {
|
||||
orderBy = [...curFilter.order_by];
|
||||
}
|
||||
const limit = 'limit' in curFilter ? curFilter.limit : 10;
|
||||
const offset = 'offset' in curFilter ? curFilter.offset : 0;
|
||||
|
||||
filterQuery = (
|
||||
<FilterQuery
|
||||
curQuery={curQuery}
|
||||
whereAnd={wheres}
|
||||
triggerSchema={triggerSchema}
|
||||
orderBy={orderBy}
|
||||
limit={limit}
|
||||
dispatch={dispatch}
|
||||
count={count}
|
||||
triggerName={curTriggerName}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortByColumn = col => {
|
||||
// Remove all the existing order_bys
|
||||
const numOfOrderBys = curFilter.order_by.length;
|
||||
for (let i = 0; i < numOfOrderBys - 1; i++) {
|
||||
dispatch(removeOrder(1));
|
||||
}
|
||||
// Go back to the first page
|
||||
dispatch(setOffset(0));
|
||||
// Set the filter and run query
|
||||
dispatch(setOrderCol(col, 0));
|
||||
if (
|
||||
curFilter.order_by.length !== 0 &&
|
||||
curFilter.order_by[0].column === col &&
|
||||
curFilter.order_by[0].type === 'asc'
|
||||
) {
|
||||
dispatch(setOrderType('desc', 0));
|
||||
} else {
|
||||
dispatch(setOrderType('asc', 0));
|
||||
}
|
||||
dispatch(runQuery(triggerSchema));
|
||||
// Add a new empty filter
|
||||
dispatch(addOrder());
|
||||
};
|
||||
|
||||
const changePage = page => {
|
||||
if (curFilter.offset !== page * curFilter.limit) {
|
||||
dispatch(setOffset(page * curFilter.limit));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const changePageSize = size => {
|
||||
if (curFilter.size !== size) {
|
||||
dispatch(setLimit(size));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const renderTableBody = () => {
|
||||
if (isProgressing) {
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<Spinner />{' '}
|
||||
</div>
|
||||
);
|
||||
} else if (count === 0) {
|
||||
return <div> No rows found. </div>;
|
||||
}
|
||||
let shouldSortColumn = true;
|
||||
const invocationColumns = ['status', 'id', 'created_at'];
|
||||
const invocationGridHeadings = [];
|
||||
invocationColumns.map(column => {
|
||||
invocationGridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
return (
|
||||
<ReactTable
|
||||
className="-highlight"
|
||||
data={newCurRows}
|
||||
columns={gridHeadings}
|
||||
resizable
|
||||
manual
|
||||
sortable={false}
|
||||
minRows={0}
|
||||
getTheadThProps={(finalState, some, column) => ({
|
||||
onClick: () => {
|
||||
if (
|
||||
column.Header &&
|
||||
shouldSortColumn &&
|
||||
column.Header !== 'Actions'
|
||||
) {
|
||||
sortByColumn(column.Header);
|
||||
}
|
||||
shouldSortColumn = true;
|
||||
},
|
||||
})}
|
||||
getResizerProps={(finalState, none, column, ctx) => ({
|
||||
onMouseDown: e => {
|
||||
shouldSortColumn = false;
|
||||
ctx.resizeColumnStart(e, column, false);
|
||||
},
|
||||
})}
|
||||
showPagination={count > curFilter.limit}
|
||||
defaultPageSize={Math.min(curFilter.limit, count)}
|
||||
pages={Math.ceil(count / curFilter.limit)}
|
||||
onPageChange={changePage}
|
||||
onPageSizeChange={changePageSize}
|
||||
page={Math.floor(curFilter.offset / curFilter.limit)}
|
||||
SubComponent={row => {
|
||||
const currentIndex = row.index;
|
||||
const currentRow = curRows[0].events[currentIndex];
|
||||
const invocationRowsData = [];
|
||||
currentRow.logs.map((r, rowIndex) => {
|
||||
const newRow = {};
|
||||
const status =
|
||||
r.status === 200 ? (
|
||||
<i className={styles.invocationSuccess + ' fa fa-check'} />
|
||||
) : (
|
||||
<i className={styles.invocationFailure + ' fa fa-times'} />
|
||||
);
|
||||
|
||||
// Insert cells corresponding to all rows
|
||||
invocationColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (r[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (col === 'status') {
|
||||
return status;
|
||||
}
|
||||
if (col === 'created_at') {
|
||||
const formattedDate = new Date(r.created_at).toUTCString();
|
||||
return formattedDate;
|
||||
}
|
||||
const content =
|
||||
r[col] === undefined ? 'NULL' : r[col].toString();
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
invocationRowsData.push(newRow);
|
||||
});
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<em>Recent Invocations</em>
|
||||
<div
|
||||
className={styles.invocationsSection + ' invocationsSection'}
|
||||
>
|
||||
{invocationRowsData.length ? (
|
||||
<ReactTable
|
||||
data={invocationRowsData}
|
||||
columns={invocationGridHeadings}
|
||||
defaultPageSize={currentRow.logs.length}
|
||||
showPagination={false}
|
||||
SubComponent={logRow => {
|
||||
const finalIndex = logRow.index;
|
||||
const currentPayload = JSON.stringify(
|
||||
currentRow.payload,
|
||||
null,
|
||||
4
|
||||
);
|
||||
const finalRow = currentRow.logs[finalIndex];
|
||||
const finalResponse = JSON.parse(
|
||||
JSON.stringify(finalRow.response, null, 4)
|
||||
);
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Tabs
|
||||
animation={false}
|
||||
defaultActiveKey={1}
|
||||
id="requestResponseTab"
|
||||
>
|
||||
<Tab eventKey={1} title="Request">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Request
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="payload"
|
||||
value={currentPayload}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey={2} title="Response">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Response
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="response"
|
||||
value={finalResponse}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.add_mar_top}>No data available</div>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={isVisible ? '' : 'hide '}>
|
||||
{filterQuery}
|
||||
<hr />
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className={styles.tableContainer + ' eventsTableBody'}>
|
||||
{renderTableBody()}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewRows;
|
@ -0,0 +1,214 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { vSetDefaults, vMakeRequest, vExpandHeading } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import { setTrigger } from '../EventActions';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import ViewRows from './ViewRows';
|
||||
import { replace } from 'react-router-redux';
|
||||
|
||||
const genHeadings = headings => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [heading, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const headingName =
|
||||
heading.type === 'obj_rel' ? heading.lcol : heading.relname;
|
||||
return [
|
||||
{ name: headingName, type: heading.type },
|
||||
...genHeadings(headings.slice(1)),
|
||||
];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subheadings = genHeadings(heading.headings).map(h => {
|
||||
if (typeof h === 'string') {
|
||||
return heading.relname + '.' + h;
|
||||
}
|
||||
return heading.relname + '.' + h.name;
|
||||
});
|
||||
return [...subheadings, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
const genRow = (row, headings) => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [row[heading], ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const rowVal = heading.type === 'obj_rel' ? row[heading.lcol] : '[...]';
|
||||
return [rowVal, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subrow = genRow(row[heading.relname], heading.headings);
|
||||
return [...subrow, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
class ViewTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize this table
|
||||
this.state = {
|
||||
dispatch: props.dispatch,
|
||||
triggerName: props.triggerName,
|
||||
};
|
||||
// this.state.dispatch = props.dispatch;
|
||||
// this.state.triggerName = props.triggerName;
|
||||
const dispatch = this.props.dispatch;
|
||||
Promise.all([
|
||||
dispatch(setTrigger(this.props.triggerName)),
|
||||
dispatch(vSetDefaults(this.props.triggerName)),
|
||||
dispatch(vMakeRequest()),
|
||||
]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const dispatch = this.props.dispatch;
|
||||
if (nextProps.triggerName !== this.props.triggerName) {
|
||||
dispatch(setTrigger(nextProps.triggerName));
|
||||
dispatch(vSetDefaults(nextProps.triggerName));
|
||||
dispatch(vMakeRequest());
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.triggerName === null ||
|
||||
nextProps.triggerName === this.props.triggerName
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
this.shouldScrollBottom =
|
||||
window.innerHeight ===
|
||||
document.body.offsetHeight - document.body.scrollTop;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.shouldScrollBottom) {
|
||||
document.body.scrollTop = document.body.offsetHeight - window.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove state data beloging to this table
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(vSetDefaults(this.props.triggerName));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
triggerName,
|
||||
triggerList,
|
||||
query,
|
||||
curFilter,
|
||||
rows,
|
||||
count, // eslint-disable-line no-unused-vars
|
||||
activePath,
|
||||
migrationMode,
|
||||
ongoingRequest,
|
||||
isProgressing,
|
||||
lastError,
|
||||
lastSuccess,
|
||||
dispatch,
|
||||
expandedRow,
|
||||
} = this.props; // eslint-disable-line no-unused-vars
|
||||
|
||||
// check if table exists
|
||||
const currentTrigger = triggerList.find(s => s.name === triggerName);
|
||||
if (!currentTrigger) {
|
||||
// dispatch a 404 route
|
||||
dispatch(replace('/404'));
|
||||
}
|
||||
// Is this a view
|
||||
const isView = false;
|
||||
|
||||
// Are there any expanded columns
|
||||
const viewRows = (
|
||||
<ViewRows
|
||||
curTriggerName={triggerName}
|
||||
curQuery={query}
|
||||
curFilter={curFilter}
|
||||
curPath={[]}
|
||||
curRows={rows}
|
||||
isView={isView}
|
||||
parentTableName={null}
|
||||
activePath={activePath}
|
||||
ongoingRequest={ongoingRequest}
|
||||
isProgressing={isProgressing}
|
||||
lastError={lastError}
|
||||
lastSuccess={lastSuccess}
|
||||
triggerList={triggerList}
|
||||
curDepth={0}
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
expandedRow={expandedRow}
|
||||
/>
|
||||
);
|
||||
|
||||
// Choose the right nav bar header thing
|
||||
const header = (
|
||||
<TableHeader
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
triggerName={triggerName}
|
||||
tabName="pending"
|
||||
migrationMode={migrationMode}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<div>{viewRows}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewTable.propTypes = {
|
||||
triggerName: PropTypes.string.isRequired,
|
||||
triggerList: PropTypes.array.isRequired,
|
||||
activePath: PropTypes.array.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
curFilter: PropTypes.object.isRequired,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
ongoingRequest: PropTypes.bool.isRequired,
|
||||
isProgressing: PropTypes.bool.isRequired,
|
||||
rows: PropTypes.array.isRequired,
|
||||
expandedRow: PropTypes.string.isRequired,
|
||||
count: PropTypes.number,
|
||||
lastError: PropTypes.object.isRequired,
|
||||
lastSuccess: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
triggerName: ownProps.params.trigger,
|
||||
triggerList: state.triggers.pendingEvents,
|
||||
migrationMode: state.main.migrationMode,
|
||||
...state.triggers.view,
|
||||
};
|
||||
};
|
||||
|
||||
const pendingEventsConnector = connect => connect(mapStateToProps)(ViewTable);
|
||||
|
||||
export default pendingEventsConnector;
|
@ -0,0 +1,246 @@
|
||||
import { defaultCurFilter } from '../EventState';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const LOADING = 'ProcessedEvents/FilterQuery/LOADING';
|
||||
|
||||
const SET_DEFQUERY = 'ProcessedEvents/FilterQuery/SET_DEFQUERY';
|
||||
const SET_FILTERCOL = 'ProcessedEvents/FilterQuery/SET_FILTERCOL';
|
||||
const SET_FILTEROP = 'ProcessedEvents/FilterQuery/SET_FILTEROP';
|
||||
const SET_FILTERVAL = 'ProcessedEvents/FilterQuery/SET_FILTERVAL';
|
||||
const ADD_FILTER = 'ProcessedEvents/FilterQuery/ADD_FILTER';
|
||||
const REMOVE_FILTER = 'ProcessedEvents/FilterQuery/REMOVE_FILTER';
|
||||
|
||||
const SET_ORDERCOL = 'ProcessedEvents/FilterQuery/SET_ORDERCOL';
|
||||
const SET_ORDERTYPE = 'ProcessedEvents/FilterQuery/SET_ORDERTYPE';
|
||||
const ADD_ORDER = 'ProcessedEvents/FilterQuery/ADD_ORDER';
|
||||
const REMOVE_ORDER = 'ProcessedEvents/FilterQuery/REMOVE_ORDER';
|
||||
const SET_LIMIT = 'ProcessedEvents/FilterQuery/SET_LIMIT';
|
||||
const SET_OFFSET = 'ProcessedEvents/FilterQuery/SET_OFFSET';
|
||||
const SET_NEXTPAGE = 'ProcessedEvents/FilterQuery/SET_NEXTPAGE';
|
||||
const SET_PREVPAGE = 'ProcessedEvents/FilterQuery/SET_PREVPAGE';
|
||||
|
||||
const setLoading = () => ({ type: LOADING, data: true });
|
||||
const unsetLoading = () => ({ type: LOADING, data: false });
|
||||
const setDefaultQuery = curQuery => ({ type: SET_DEFQUERY, curQuery });
|
||||
const setFilterCol = (name, index) => ({ type: SET_FILTERCOL, name, index });
|
||||
const setFilterOp = (opName, index) => ({ type: SET_FILTEROP, opName, index });
|
||||
const setFilterVal = (val, index) => ({ type: SET_FILTERVAL, val, index });
|
||||
const addFilter = () => ({ type: ADD_FILTER });
|
||||
const removeFilter = index => ({ type: REMOVE_FILTER, index });
|
||||
|
||||
const setOrderCol = (name, index) => ({ type: SET_ORDERCOL, name, index });
|
||||
const setOrderType = (order, index) => ({ type: SET_ORDERTYPE, order, index });
|
||||
const addOrder = () => ({ type: ADD_ORDER });
|
||||
const removeOrder = index => ({ type: REMOVE_ORDER, index });
|
||||
|
||||
const setLimit = limit => ({ type: SET_LIMIT, limit });
|
||||
const setOffset = offset => ({ type: SET_OFFSET, offset });
|
||||
const setNextPage = () => ({ type: SET_NEXTPAGE });
|
||||
const setPrevPage = () => ({ type: SET_PREVPAGE });
|
||||
|
||||
const runQuery = triggerSchema => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState().triggers.view.curFilter;
|
||||
const finalWhereClauses = state.where.$and.filter(w => {
|
||||
const colName = Object.keys(w)[0].trim();
|
||||
if (colName === '') {
|
||||
return false;
|
||||
}
|
||||
const opName = Object.keys(w[colName])[0].trim();
|
||||
if (opName === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const newQuery = {
|
||||
where: { $and: finalWhereClauses },
|
||||
limit: state.limit,
|
||||
offset: state.offset,
|
||||
order_by: state.order_by.filter(w => w.column.trim() !== ''),
|
||||
};
|
||||
if (newQuery.where.$and.length === 0) {
|
||||
delete newQuery.where;
|
||||
}
|
||||
if (newQuery.order_by.length === 0) {
|
||||
delete newQuery.order_by;
|
||||
}
|
||||
dispatch({ type: 'ProcessedEvents/V_SET_QUERY_OPTS', queryStuff: newQuery });
|
||||
dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
|
||||
const processedFilterReducer = (state = defaultCurFilter, action) => {
|
||||
const i = action.index;
|
||||
const newFilter = {};
|
||||
switch (action.type) {
|
||||
case SET_DEFQUERY:
|
||||
const q = action.curQuery;
|
||||
if (
|
||||
'order_by' in q ||
|
||||
'limit' in q ||
|
||||
'offset' in q ||
|
||||
('where' in q && '$and' in q.where)
|
||||
) {
|
||||
const newCurFilterQ = {};
|
||||
newCurFilterQ.where =
|
||||
'where' in q && '$and' in q.where
|
||||
? { $and: [...q.where.$and, { '': { '': '' } }] }
|
||||
: { ...defaultCurFilter.where };
|
||||
newCurFilterQ.order_by =
|
||||
'order_by' in q
|
||||
? [...q.order_by, ...defaultCurFilter.order_by]
|
||||
: [...defaultCurFilter.order_by];
|
||||
newCurFilterQ.limit = 'limit' in q ? q.limit : defaultCurFilter.limit;
|
||||
newCurFilterQ.offset =
|
||||
'offset' in q ? q.offset : defaultCurFilter.offset;
|
||||
return newCurFilterQ;
|
||||
}
|
||||
return defaultCurFilter;
|
||||
case SET_FILTERCOL:
|
||||
const oldColName = Object.keys(state.where.$and[i])[0];
|
||||
newFilter[action.name] = { ...state.where.$and[i][oldColName] };
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTEROP:
|
||||
const colName = Object.keys(state.where.$and[i])[0];
|
||||
const oldOp = Object.keys(state.where.$and[i][colName])[0];
|
||||
newFilter[colName] = {};
|
||||
newFilter[colName][action.opName] = state.where.$and[i][colName][oldOp];
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTERVAL:
|
||||
const colName1 = Object.keys(state.where.$and[i])[0];
|
||||
const opName = Object.keys(state.where.$and[i][colName1])[0];
|
||||
newFilter[colName1] = {};
|
||||
newFilter[colName1][opName] = action.val;
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case ADD_FILTER:
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [...state.where.$and, { '': { '': '' } }],
|
||||
},
|
||||
};
|
||||
case REMOVE_FILTER:
|
||||
const newFilters = [
|
||||
...state.where.$and.slice(0, i),
|
||||
...state.where.$and.slice(i + 1),
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
where: { $and: newFilters },
|
||||
};
|
||||
|
||||
case SET_ORDERCOL:
|
||||
const oldOrder = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder, column: action.name },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case SET_ORDERTYPE:
|
||||
const oldOrder1 = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder1, type: action.order },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case REMOVE_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case ADD_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by,
|
||||
{ column: '', type: 'asc', nulls: 'last' },
|
||||
],
|
||||
};
|
||||
|
||||
case SET_LIMIT:
|
||||
return {
|
||||
...state,
|
||||
limit: action.limit,
|
||||
};
|
||||
case SET_OFFSET:
|
||||
return {
|
||||
...state,
|
||||
offset: action.offset,
|
||||
};
|
||||
case SET_NEXTPAGE:
|
||||
return {
|
||||
...state,
|
||||
offset: state.offset + state.limit,
|
||||
};
|
||||
case SET_PREVPAGE:
|
||||
const newOffset = state.offset - state.limit;
|
||||
return {
|
||||
...state,
|
||||
offset: newOffset < 0 ? 0 : newOffset,
|
||||
};
|
||||
case LOADING:
|
||||
return {
|
||||
...state,
|
||||
loading: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default processedFilterReducer;
|
||||
export {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
setLimit,
|
||||
setOffset,
|
||||
setNextPage,
|
||||
setPrevPage,
|
||||
setDefaultQuery,
|
||||
setLoading,
|
||||
unsetLoading,
|
||||
runQuery,
|
||||
};
|
@ -0,0 +1,265 @@
|
||||
/*
|
||||
Use state exactly the way columns in create table do.
|
||||
dispatch actions using a given function,
|
||||
but don't listen to state.
|
||||
derive everything through viewtable as much as possible.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Operators from '../Operators';
|
||||
import {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
} from './FilterActions.js';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
} from './FilterActions.js';
|
||||
import { setDefaultQuery, runQuery } from './FilterActions';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const renderCols = (colName, triggerSchema, onChange, usage, key) => {
|
||||
const columns = ['id', 'delivered', 'created_at'];
|
||||
return (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={colName.trim()}
|
||||
data-test={
|
||||
usage === 'sort' ? `sort-column-${key}` : `filter-column-${key}`
|
||||
}
|
||||
>
|
||||
{colName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{columns.map((c, i) => (
|
||||
<option key={i} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOps = (opName, onChange, key) => (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={opName.trim()}
|
||||
data-test={`filter-op-${key}`}
|
||||
>
|
||||
{opName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{Operators.map((o, i) => (
|
||||
<option key={i} value={o.value}>
|
||||
{o.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const renderWheres = (whereAnd, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return whereAnd.map((clause, i) => {
|
||||
const colName = Object.keys(clause)[0];
|
||||
const opName = Object.keys(clause[colName])[0];
|
||||
const dSetFilterCol = e => {
|
||||
dispatch(setFilterCol(e.target.value, i));
|
||||
};
|
||||
const dSetFilterOp = e => {
|
||||
dispatch(setFilterOp(e.target.value, i));
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < whereAnd.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeFilter(i));
|
||||
}}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-4">
|
||||
{renderCols(colName, triggerSchema, dSetFilterCol, 'filter', i)}
|
||||
</div>
|
||||
<div className="col-xs-3">{renderOps(opName, dSetFilterOp, i)}</div>
|
||||
<div className="col-xs-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="-- value --"
|
||||
value={clause[colName][opName]}
|
||||
onChange={e => {
|
||||
dispatch(setFilterVal(e.target.value, i));
|
||||
if (i + 1 === whereAnd.length) {
|
||||
dispatch(addFilter());
|
||||
}
|
||||
}}
|
||||
data-test={`filter-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderSorts = (orderBy, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return orderBy.map((c, i) => {
|
||||
const dSetOrderCol = e => {
|
||||
dispatch(setOrderCol(e.target.value, i));
|
||||
if (i + 1 === orderBy.length) {
|
||||
dispatch(addOrder());
|
||||
}
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < orderBy.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeOrder(i));
|
||||
}}
|
||||
data-test={`clear-sorts-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-6">
|
||||
{renderCols(c.column, triggerSchema, dSetOrderCol, 'sort', i)}
|
||||
</div>
|
||||
<div className="col-xs-5">
|
||||
<select
|
||||
value={c.type}
|
||||
className="form-control"
|
||||
onChange={e => {
|
||||
dispatch(setOrderType(e.target.value, i));
|
||||
}}
|
||||
data-test={`sort-order-${i}`}
|
||||
>
|
||||
<option value="asc">Asc</option>
|
||||
<option value="desc">Desc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-1 text-center">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
class FilterQuery extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isWatching: false, intervalId: null };
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(setDefaultQuery(this.props.curQuery));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
|
||||
watchChanges() {
|
||||
// set state on watch
|
||||
this.setState({ isWatching: !this.state.isWatching });
|
||||
if (this.state.isWatching) {
|
||||
clearInterval(this.state.intervalId);
|
||||
} else {
|
||||
const intervalId = setInterval(this.refreshData, 2000);
|
||||
this.setState({ intervalId: intervalId });
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.props.dispatch(vMakeRequest());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, whereAnd, triggerSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return (
|
||||
<div className={styles.filterOptions}>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<span className={styles.subheading_text}>Filter</span>
|
||||
{renderWheres(whereAnd, triggerSchema, dispatch)}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<b className={styles.subheading_text}>Sort</b>
|
||||
{renderSorts(orderBy, triggerSchema, dispatch)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.padd_right} ${styles.clear_fix}`}>
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn ${styles.yellow_button}`}
|
||||
data-test="run-query"
|
||||
>
|
||||
Run query
|
||||
</button>
|
||||
<button
|
||||
onClick={this.watchChanges.bind(this)}
|
||||
className={styles.add_mar_left + ' btn btn-default'}
|
||||
data-test="run-query"
|
||||
>
|
||||
{this.state.isWatching ? (
|
||||
<span>
|
||||
Watching <i className={'fa fa-spinner fa-spin'} />
|
||||
</span>
|
||||
) : (
|
||||
'Watch'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuery.propTypes = {
|
||||
curQuery: PropTypes.object.isRequired,
|
||||
triggerSchema: PropTypes.object.isRequired,
|
||||
whereAnd: PropTypes.array.isRequired,
|
||||
orderBy: PropTypes.array.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
count: PropTypes.number,
|
||||
triggerName: PropTypes.string,
|
||||
offset: PropTypes.number.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
@ -0,0 +1,95 @@
|
||||
@import "../../../Common/Common.scss";
|
||||
$bgColor: #f9f9f9;
|
||||
.container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline-block;
|
||||
max-height: 34px;
|
||||
padding: 5px 20px;
|
||||
margin-left: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.queryBox {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-height: 30px;
|
||||
.inputRow {
|
||||
margin: 20px 0;
|
||||
div[class^=col-xs-] {
|
||||
padding-left: 0;
|
||||
padding-right: 2.5px;
|
||||
}
|
||||
:global(.form-control) {
|
||||
height: 35px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.fa) {
|
||||
padding-top: 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
}
|
||||
.descending {
|
||||
padding-left: 10px;
|
||||
input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.runQuery {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 20px;
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.input-group) {
|
||||
margin-top: -24px;
|
||||
margin-right: 10px;
|
||||
input {
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
margin-top: -24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterOptions {
|
||||
margin-top: 10px;
|
||||
background: $bgColor;
|
||||
// padding: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boxHeading {
|
||||
top: -0.55em;
|
||||
line-height: 1.1em;
|
||||
background: $bgColor;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
padding: 0 10px;
|
||||
color: #929292;
|
||||
font-weight: normal;
|
||||
}
|
@ -0,0 +1,540 @@
|
||||
import { defaultViewState } from '../EventState';
|
||||
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import requestAction from 'utils/requestAction';
|
||||
import processedFilterReducer from './FilterActions';
|
||||
import { findTableFromRel } from '../utils';
|
||||
import {
|
||||
showSuccessNotification,
|
||||
showErrorNotification,
|
||||
} from '../Notification';
|
||||
import dataHeaders from '../Common/Headers';
|
||||
|
||||
/* ****************** View actions *************/
|
||||
const V_SET_DEFAULTS = 'ProcessedEvents/V_SET_DEFAULTS';
|
||||
const V_REQUEST_SUCCESS = 'ProcessedEvents/V_REQUEST_SUCCESS';
|
||||
const V_REQUEST_ERROR = 'ProcessedEvents/V_REQUEST_ERROR';
|
||||
const V_EXPAND_REL = 'ProcessedEvents/V_EXPAND_REL';
|
||||
const V_CLOSE_REL = 'ProcessedEvents/V_CLOSE_REL';
|
||||
const V_SET_ACTIVE = 'ProcessedEvents/V_SET_ACTIVE';
|
||||
const V_SET_QUERY_OPTS = 'ProcessedEvents/V_SET_QUERY_OPTS';
|
||||
const V_REQUEST_PROGRESS = 'ProcessedEvents/V_REQUEST_PROGRESS';
|
||||
const V_EXPAND_ROW = 'ProcessedEvents/V_EXPAND_ROW';
|
||||
const V_COLLAPSE_ROW = 'ProcessedEvents/V_COLLAPSE_ROW';
|
||||
// const V_ADD_WHERE;
|
||||
// const V_REMOVE_WHERE;
|
||||
// const V_SET_LIMIT;
|
||||
// const V_SET_OFFSET;
|
||||
// const V_ADD_SORT;
|
||||
// const V_REMOVE_SORT;
|
||||
|
||||
/* ****************** action creators *************/
|
||||
|
||||
const vExpandRow = rowKey => ({
|
||||
type: V_EXPAND_ROW,
|
||||
data: rowKey,
|
||||
});
|
||||
|
||||
const vCollapseRow = () => ({
|
||||
type: V_COLLAPSE_ROW,
|
||||
});
|
||||
|
||||
const vSetDefaults = () => ({ type: V_SET_DEFAULTS });
|
||||
|
||||
const vMakeRequest = () => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const url = Endpoints.query;
|
||||
const originalTrigger = getState().triggers.currentTrigger;
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: true });
|
||||
const currentQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
// count query
|
||||
const countQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
countQuery.columns = ['id'];
|
||||
|
||||
// delivered = true || error = true
|
||||
// where clause for relationship
|
||||
const currentWhereClause = state.triggers.view.query.where;
|
||||
if (currentWhereClause && currentWhereClause.$and) {
|
||||
// make filter for events
|
||||
const finalAndClause = currentQuery.where.$and;
|
||||
finalAndClause.push({
|
||||
$or: [{ delivered: { $eq: true } }, { error: { $eq: true } }],
|
||||
});
|
||||
currentQuery.columns[1].where = { $and: finalAndClause };
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where.$and.push({
|
||||
trigger_name: state.triggers.currentTrigger,
|
||||
});
|
||||
} else {
|
||||
// reset where for events
|
||||
if (currentQuery.columns[1]) {
|
||||
currentQuery.columns[1].where = {
|
||||
$or: [{ delivered: { $eq: true } }, { error: { $eq: true } }],
|
||||
};
|
||||
}
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where = {
|
||||
$and: [
|
||||
{ trigger_name: state.triggers.currentTrigger },
|
||||
{ $or: [{ delivered: { $eq: true } }, { error: { $eq: true } }] },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// order_by for relationship
|
||||
const currentOrderBy = state.triggers.view.query.order_by;
|
||||
if (currentOrderBy) {
|
||||
currentQuery.columns[1].order_by = currentOrderBy;
|
||||
// reset order_by
|
||||
delete currentQuery.order_by;
|
||||
} else {
|
||||
// reset order by for events
|
||||
if (currentQuery.columns[1]) {
|
||||
delete currentQuery.columns[1].order_by;
|
||||
currentQuery.columns[1].order_by = ['-created_at'];
|
||||
}
|
||||
delete currentQuery.order_by;
|
||||
}
|
||||
|
||||
// limit and offset for relationship
|
||||
const currentLimit = state.triggers.view.query.limit;
|
||||
const currentOffset = state.triggers.view.query.offset;
|
||||
currentQuery.columns[1].limit = currentLimit;
|
||||
currentQuery.columns[1].offset = currentOffset;
|
||||
|
||||
// reset limit and offset for parent
|
||||
delete currentQuery.limit;
|
||||
delete currentQuery.offset;
|
||||
delete countQuery.limit;
|
||||
delete countQuery.offset;
|
||||
|
||||
const requestBody = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
args: {
|
||||
...currentQuery,
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'count',
|
||||
args: {
|
||||
...countQuery,
|
||||
table: {
|
||||
name: 'event_log',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: dataHeaders(getState),
|
||||
credentials: globalCookiePolicy,
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
const currentTrigger = getState().triggers.currentTrigger;
|
||||
if (originalTrigger === currentTrigger) {
|
||||
Promise.all([
|
||||
dispatch({
|
||||
type: V_REQUEST_SUCCESS,
|
||||
data: data[0],
|
||||
count: data[1].count,
|
||||
}),
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: false }),
|
||||
]);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch({ type: V_REQUEST_ERROR, data: error });
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const deleteItem = pkClause => {
|
||||
return (dispatch, getState) => {
|
||||
const isOk = confirm('Permanently delete this row?');
|
||||
if (!isOk) {
|
||||
return;
|
||||
}
|
||||
const state = getState();
|
||||
const url = Endpoints.query;
|
||||
const reqBody = {
|
||||
type: 'delete',
|
||||
args: {
|
||||
table: {
|
||||
name: state.triggers.currentTrigger,
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
where: pkClause,
|
||||
},
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(reqBody),
|
||||
headers: dataHeaders(getState),
|
||||
credentials: globalCookiePolicy,
|
||||
};
|
||||
dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
dispatch(vMakeRequest());
|
||||
dispatch(
|
||||
showSuccessNotification(
|
||||
'Row deleted!',
|
||||
'Affected rows: ' + data.affected_rows
|
||||
)
|
||||
);
|
||||
},
|
||||
err => {
|
||||
dispatch(
|
||||
showErrorNotification('Deleting row failed!', err.error, reqBody, err)
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const vExpandRel = (path, relname, pk) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_EXPAND_REL, path, relname, pk });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
const vCloseRel = (path, relname) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_CLOSE_REL, path, relname });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
/* ************ helpers ************************/
|
||||
const defaultSubQuery = (relname, tableSchema) => {
|
||||
return {
|
||||
name: relname,
|
||||
columns: tableSchema.columns.map(c => c.column_name),
|
||||
};
|
||||
};
|
||||
|
||||
const expandQuery = (
|
||||
curQuery,
|
||||
curTable,
|
||||
pk,
|
||||
curPath,
|
||||
relname,
|
||||
schemas,
|
||||
isObjRel = false
|
||||
) => {
|
||||
if (curPath.length === 0) {
|
||||
const rel = curTable.relationships.find(r => r.rel_name === relname);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, rel);
|
||||
|
||||
const newColumns = [
|
||||
...curQuery.columns,
|
||||
defaultSubQuery(relname, childTableSchema),
|
||||
];
|
||||
if (isObjRel) {
|
||||
return { ...curQuery, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's already oldStuff then don't reset it
|
||||
if ('oldStuff' in curQuery) {
|
||||
return { ...curQuery, where: pk, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's no oldStuff then set it
|
||||
const oldStuff = {};
|
||||
['where', 'limit', 'offset'].map(k => {
|
||||
if (k in curQuery) {
|
||||
oldStuff[k] = curQuery[k];
|
||||
}
|
||||
});
|
||||
return { name: curQuery.name, where: pk, columns: newColumns, oldStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
expandQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
pk,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas,
|
||||
curRel.rel_type === 'object'
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const closeQuery = (curQuery, curTable, curPath, relname, schemas) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
if (curPath.length === 0) {
|
||||
const expandedIndex = curQuery.columns.findIndex(c => c.name === relname);
|
||||
const newColumns = [
|
||||
...curQuery.columns.slice(0, expandedIndex),
|
||||
...curQuery.columns.slice(expandedIndex + 1),
|
||||
];
|
||||
const newStuff = {};
|
||||
newStuff.columns = newColumns;
|
||||
if ('name' in curQuery) {
|
||||
newStuff.name = curQuery.name;
|
||||
}
|
||||
// If no other expanded columns are left
|
||||
if (!newColumns.find(c => typeof c === 'object')) {
|
||||
if (curQuery.oldStuff) {
|
||||
['where', 'limit', 'order_by', 'offset'].map(k => {
|
||||
if (k in curQuery.oldStuff) {
|
||||
newStuff[k] = curQuery.oldStuff[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
return { ...newStuff };
|
||||
}
|
||||
return { ...curQuery, ...newStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
closeQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const setActivePath = (activePath, curPath, relname, query) => {
|
||||
const basePath = relname
|
||||
? [activePath[0], ...curPath, relname]
|
||||
: [activePath[0], ...curPath];
|
||||
|
||||
// Now check if there are any more children on this path.
|
||||
// If there are, then we should expand them by default
|
||||
let subQuery = query;
|
||||
let subBase = basePath.slice(1);
|
||||
|
||||
while (subBase.length > 0) {
|
||||
subQuery = subQuery.columns.find(c => c.name === subBase[0]); // eslint-disable-line no-loop-func
|
||||
subBase = subBase.slice(1);
|
||||
}
|
||||
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
while (subQuery) {
|
||||
basePath.push(subQuery.name);
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
}
|
||||
|
||||
return basePath;
|
||||
};
|
||||
const updateActivePathOnClose = (
|
||||
activePath,
|
||||
tableName,
|
||||
curPath,
|
||||
relname,
|
||||
query
|
||||
) => {
|
||||
const basePath = [tableName, ...curPath, relname];
|
||||
let subBase = [...basePath];
|
||||
let subActive = [...activePath];
|
||||
let matchingFound = false;
|
||||
let commonIndex = 0;
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
|
||||
while (subActive.length > 0) {
|
||||
if (subBase[0] === subActive[0]) {
|
||||
matchingFound = true;
|
||||
break;
|
||||
}
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
commonIndex += 1;
|
||||
}
|
||||
|
||||
if (matchingFound) {
|
||||
const newActivePath = activePath.slice(0, commonIndex + 1);
|
||||
return setActivePath(
|
||||
newActivePath,
|
||||
newActivePath.slice(1, -1),
|
||||
null,
|
||||
query
|
||||
);
|
||||
}
|
||||
return [...activePath];
|
||||
};
|
||||
const addQueryOptsActivePath = (query, queryStuff, activePath) => {
|
||||
let curPath = activePath.slice(1);
|
||||
const newQuery = { ...query };
|
||||
let curQuery = newQuery;
|
||||
while (curPath.length > 0) {
|
||||
curQuery = curQuery.columns.find(c => c.name === curPath[0]); // eslint-disable-line no-loop-func
|
||||
curPath = curPath.slice(1);
|
||||
}
|
||||
|
||||
['where', 'order_by', 'limit', 'offset'].map(k => {
|
||||
delete curQuery[k];
|
||||
});
|
||||
|
||||
for (const k in queryStuff) {
|
||||
if (queryStuff.hasOwnProperty(k)) {
|
||||
curQuery[k] = queryStuff[k];
|
||||
}
|
||||
}
|
||||
return newQuery;
|
||||
};
|
||||
/* ****************** reducer ******************/
|
||||
const processedEventsReducer = (
|
||||
triggerName,
|
||||
triggerList,
|
||||
viewState,
|
||||
action
|
||||
) => {
|
||||
if (action.type.indexOf('ProcessedEvents/FilterQuery/') === 0) {
|
||||
return {
|
||||
...viewState,
|
||||
curFilter: processedFilterReducer(viewState.curFilter, action),
|
||||
};
|
||||
}
|
||||
const tableSchema = triggerList.find(x => x.name === triggerName);
|
||||
switch (action.type) {
|
||||
case V_SET_DEFAULTS:
|
||||
// check if table exists and then process.
|
||||
/*
|
||||
const currentTrigger = triggerList.find(t => t.name === triggerName);
|
||||
let currentColumns = [];
|
||||
if (currentTrigger) {
|
||||
currentColumns = currentTrigger.map(c => c.column_name);
|
||||
}
|
||||
*/
|
||||
return {
|
||||
...defaultViewState,
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: {
|
||||
$or: [{ delivered: { $eq: true } }, { error: { $eq: true } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
limit: 10,
|
||||
},
|
||||
activePath: [triggerName],
|
||||
rows: [],
|
||||
count: null,
|
||||
};
|
||||
case V_SET_QUERY_OPTS:
|
||||
return {
|
||||
...viewState,
|
||||
query: addQueryOptsActivePath(
|
||||
viewState.query,
|
||||
action.queryStuff,
|
||||
viewState.activePath
|
||||
),
|
||||
};
|
||||
case V_EXPAND_REL:
|
||||
return {
|
||||
...viewState,
|
||||
query: expandQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.pk,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
),
|
||||
activePath: [...viewState.activePath, action.relname],
|
||||
};
|
||||
case V_CLOSE_REL:
|
||||
const _query = closeQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
);
|
||||
return {
|
||||
...viewState,
|
||||
query: _query,
|
||||
activePath: updateActivePathOnClose(
|
||||
viewState.activePath,
|
||||
triggerName,
|
||||
action.path,
|
||||
action.relname,
|
||||
_query
|
||||
),
|
||||
};
|
||||
case V_SET_ACTIVE:
|
||||
return {
|
||||
...viewState,
|
||||
activePath: setActivePath(
|
||||
viewState.activePath,
|
||||
action.path,
|
||||
action.relname,
|
||||
viewState.query
|
||||
),
|
||||
};
|
||||
case V_REQUEST_SUCCESS:
|
||||
return { ...viewState, rows: action.data, count: action.count };
|
||||
case V_REQUEST_PROGRESS:
|
||||
return { ...viewState, isProgressing: action.data };
|
||||
case V_EXPAND_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: action.data,
|
||||
};
|
||||
case V_COLLAPSE_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: '',
|
||||
};
|
||||
default:
|
||||
return viewState;
|
||||
}
|
||||
};
|
||||
|
||||
export default processedEventsReducer;
|
||||
export {
|
||||
vSetDefaults,
|
||||
vMakeRequest,
|
||||
vExpandRel,
|
||||
vCloseRel,
|
||||
vExpandRow,
|
||||
vCollapseRow,
|
||||
V_SET_ACTIVE,
|
||||
deleteItem,
|
||||
};
|
@ -0,0 +1,407 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import AceEditor from 'react-ace';
|
||||
import Tabs from 'react-bootstrap/lib/Tabs';
|
||||
import Tab from 'react-bootstrap/lib/Tab';
|
||||
import 'brace/mode/json';
|
||||
import 'react-table/react-table.css';
|
||||
import { deleteItem, vExpandRow, vCollapseRow } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import FilterQuery from './FilterQuery';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
removeOrder,
|
||||
runQuery,
|
||||
setOffset,
|
||||
setLimit,
|
||||
addOrder,
|
||||
} from './FilterActions';
|
||||
import { ordinalColSort } from '../utils';
|
||||
import Spinner from '../../../Common/Spinner/Spinner';
|
||||
import '../TableCommon/ReactTableFix.css';
|
||||
|
||||
const ViewRows = ({
|
||||
curTriggerName,
|
||||
curQuery,
|
||||
curFilter,
|
||||
curRows,
|
||||
curPath,
|
||||
curDepth,
|
||||
activePath,
|
||||
triggerList,
|
||||
dispatch,
|
||||
isProgressing,
|
||||
isView,
|
||||
count,
|
||||
expandedRow,
|
||||
}) => {
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
const triggerSchema = triggerList.find(x => x.name === curTriggerName);
|
||||
const curRelName = curPath.length > 0 ? curPath.slice(-1)[0] : null;
|
||||
|
||||
// Am I a single row display
|
||||
const isSingleRow = false;
|
||||
|
||||
// Get the headings
|
||||
const tableHeadings = [];
|
||||
const gridHeadings = [];
|
||||
const eventLogColumns = ['id', 'delivered', 'created_at'];
|
||||
const sortedColumns = eventLogColumns.sort(ordinalColSort);
|
||||
|
||||
sortedColumns.map((column, i) => {
|
||||
tableHeadings.push(<th key={i}>{column}</th>);
|
||||
gridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
|
||||
const hasPrimaryKeys = true;
|
||||
/*
|
||||
let editButton;
|
||||
let deleteButton;
|
||||
*/
|
||||
|
||||
const newCurRows = [];
|
||||
if (curRows && curRows[0] && curRows[0].events) {
|
||||
curRows[0].events.forEach((row, rowIndex) => {
|
||||
const newRow = {};
|
||||
const pkClause = {};
|
||||
if (!isView && hasPrimaryKeys) {
|
||||
pkClause.id = row.id;
|
||||
} else {
|
||||
triggerSchema.map(k => {
|
||||
pkClause[k] = row[k];
|
||||
});
|
||||
}
|
||||
/*
|
||||
if (!isSingleRow && !isView && hasPrimaryKeys) {
|
||||
deleteButton = (
|
||||
<button
|
||||
className={`${styles.add_mar_right_small} btn btn-xs btn-default`}
|
||||
onClick={() => {
|
||||
dispatch(deleteItem(pkClause));
|
||||
}}
|
||||
data-test={`row-delete-button-${rowIndex}`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const buttonsDiv = (
|
||||
<div className={styles.tableCellCenterAligned}>
|
||||
{editButton}
|
||||
{deleteButton}
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
// Insert Edit, Delete, Clone in a cell
|
||||
// newRow.actions = buttonsDiv;
|
||||
// Insert cells corresponding to all rows
|
||||
sortedColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (row[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let content = row[col] === undefined ? 'NULL' : row[col].toString();
|
||||
if (col === 'created_at') {
|
||||
content = new Date(row[col]).toUTCString();
|
||||
}
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
newCurRows.push(newRow);
|
||||
});
|
||||
}
|
||||
|
||||
// Is this ViewRows visible
|
||||
let isVisible = false;
|
||||
if (!curRelName) {
|
||||
isVisible = true;
|
||||
} else if (curRelName === activePath[curDepth]) {
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
let filterQuery = null;
|
||||
if (!isSingleRow) {
|
||||
if (curRelName === activePath[curDepth] || curDepth === 0) {
|
||||
// Rendering only if this is the activePath or this is the root
|
||||
|
||||
let wheres = [{ '': { '': '' } }];
|
||||
if ('where' in curFilter && '$and' in curFilter.where) {
|
||||
wheres = [...curFilter.where.$and];
|
||||
}
|
||||
|
||||
let orderBy = [{ column: '', type: 'asc', nulls: 'last' }];
|
||||
if ('order_by' in curFilter) {
|
||||
orderBy = [...curFilter.order_by];
|
||||
}
|
||||
const limit = 'limit' in curFilter ? curFilter.limit : 10;
|
||||
const offset = 'offset' in curFilter ? curFilter.offset : 0;
|
||||
|
||||
filterQuery = (
|
||||
<FilterQuery
|
||||
curQuery={curQuery}
|
||||
whereAnd={wheres}
|
||||
triggerSchema={triggerSchema}
|
||||
orderBy={orderBy}
|
||||
limit={limit}
|
||||
dispatch={dispatch}
|
||||
count={count}
|
||||
triggerName={curTriggerName}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortByColumn = col => {
|
||||
// Remove all the existing order_bys
|
||||
const numOfOrderBys = curFilter.order_by.length;
|
||||
for (let i = 0; i < numOfOrderBys - 1; i++) {
|
||||
dispatch(removeOrder(1));
|
||||
}
|
||||
// Go back to the first page
|
||||
dispatch(setOffset(0));
|
||||
// Set the filter and run query
|
||||
dispatch(setOrderCol(col, 0));
|
||||
if (
|
||||
curFilter.order_by.length !== 0 &&
|
||||
curFilter.order_by[0].column === col &&
|
||||
curFilter.order_by[0].type === 'asc'
|
||||
) {
|
||||
dispatch(setOrderType('desc', 0));
|
||||
} else {
|
||||
dispatch(setOrderType('asc', 0));
|
||||
}
|
||||
dispatch(runQuery(triggerSchema));
|
||||
// Add a new empty filter
|
||||
dispatch(addOrder());
|
||||
};
|
||||
|
||||
const changePage = page => {
|
||||
if (curFilter.offset !== page * curFilter.limit) {
|
||||
dispatch(setOffset(page * curFilter.limit));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const changePageSize = size => {
|
||||
if (curFilter.size !== size) {
|
||||
dispatch(setLimit(size));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const renderTableBody = () => {
|
||||
if (count === 0) {
|
||||
return <div> No rows found. </div>;
|
||||
}
|
||||
let shouldSortColumn = true;
|
||||
const invocationColumns = ['status', 'id', 'created_at'];
|
||||
const invocationGridHeadings = [];
|
||||
invocationColumns.map(column => {
|
||||
invocationGridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
return (
|
||||
<ReactTable
|
||||
className="-highlight"
|
||||
data={newCurRows}
|
||||
columns={gridHeadings}
|
||||
resizable
|
||||
manual
|
||||
sortable={false}
|
||||
minRows={0}
|
||||
getTheadThProps={(finalState, some, column) => ({
|
||||
onClick: () => {
|
||||
if (
|
||||
column.Header &&
|
||||
shouldSortColumn &&
|
||||
column.Header !== 'Actions'
|
||||
) {
|
||||
sortByColumn(column.Header);
|
||||
}
|
||||
shouldSortColumn = true;
|
||||
},
|
||||
})}
|
||||
getResizerProps={(finalState, none, column, ctx) => ({
|
||||
onMouseDown: e => {
|
||||
shouldSortColumn = false;
|
||||
ctx.resizeColumnStart(e, column, false);
|
||||
},
|
||||
})}
|
||||
showPagination={count > curFilter.limit}
|
||||
defaultPageSize={Math.min(curFilter.limit, count)}
|
||||
pages={Math.ceil(count / curFilter.limit)}
|
||||
onPageChange={changePage}
|
||||
onPageSizeChange={changePageSize}
|
||||
page={Math.floor(curFilter.offset / curFilter.limit)}
|
||||
SubComponent={row => {
|
||||
const currentIndex = row.index;
|
||||
const currentRow = curRows[0].events[currentIndex];
|
||||
const invocationRowsData = [];
|
||||
currentRow.logs.map((r, rowIndex) => {
|
||||
const newRow = {};
|
||||
const status =
|
||||
r.status === 200 ? (
|
||||
<i className={styles.invocationSuccess + ' fa fa-check'} />
|
||||
) : (
|
||||
<i className={styles.invocationFailure + ' fa fa-times'} />
|
||||
);
|
||||
|
||||
// Insert cells corresponding to all rows
|
||||
invocationColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (r[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (col === 'status') {
|
||||
return status;
|
||||
}
|
||||
if (col === 'created_at') {
|
||||
const formattedDate = new Date(r.created_at).toUTCString();
|
||||
return formattedDate;
|
||||
}
|
||||
const content =
|
||||
r[col] === undefined ? 'NULL' : r[col].toString();
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
invocationRowsData.push(newRow);
|
||||
});
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<em>Recent Invocations</em>
|
||||
<div
|
||||
className={styles.invocationsSection + ' invocationsSection'}
|
||||
>
|
||||
{invocationRowsData.length ? (
|
||||
<ReactTable
|
||||
data={invocationRowsData}
|
||||
columns={invocationGridHeadings}
|
||||
defaultPageSize={currentRow.logs.length}
|
||||
showPagination={false}
|
||||
SubComponent={logRow => {
|
||||
const finalIndex = logRow.index;
|
||||
const currentPayload = JSON.stringify(
|
||||
currentRow.payload,
|
||||
null,
|
||||
4
|
||||
);
|
||||
const finalRow = currentRow.logs[finalIndex];
|
||||
// check if response is type JSON
|
||||
let finalResponse = finalRow.response;
|
||||
try {
|
||||
finalResponse = JSON.parse(finalRow.response);
|
||||
finalResponse = JSON.stringify(finalResponse, null, 4);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Tabs
|
||||
animation={false}
|
||||
defaultActiveKey={1}
|
||||
id="requestResponseTab"
|
||||
>
|
||||
<Tab eventKey={1} title="Request">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Request
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="payload"
|
||||
value={currentPayload}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey={2} title="Response">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Response
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="response"
|
||||
value={finalResponse}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.add_mar_top}>No data available</div>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={isVisible ? '' : 'hide '}>
|
||||
{filterQuery}
|
||||
<hr />
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className={styles.tableContainer + ' eventsTableBody'}>
|
||||
{isProgressing ? (
|
||||
<div>
|
||||
{' '}
|
||||
<Spinner />{' '}
|
||||
</div>
|
||||
) : null}
|
||||
{renderTableBody()}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewRows;
|
@ -0,0 +1,227 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { vSetDefaults, vMakeRequest, vExpandHeading } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import { setTrigger } from '../EventActions';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import ViewRows from './ViewRows';
|
||||
import { replace } from 'react-router-redux';
|
||||
|
||||
const genHeadings = headings => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [heading, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const headingName =
|
||||
heading.type === 'obj_rel' ? heading.lcol : heading.relname;
|
||||
return [
|
||||
{ name: headingName, type: heading.type },
|
||||
...genHeadings(headings.slice(1)),
|
||||
];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subheadings = genHeadings(heading.headings).map(h => {
|
||||
if (typeof h === 'string') {
|
||||
return heading.relname + '.' + h;
|
||||
}
|
||||
return heading.relname + '.' + h.name;
|
||||
});
|
||||
return [...subheadings, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
const genRow = (row, headings) => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [row[heading], ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const rowVal = heading.type === 'obj_rel' ? row[heading.lcol] : '[...]';
|
||||
return [rowVal, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subrow = genRow(row[heading.relname], heading.headings);
|
||||
return [...subrow, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
class ViewTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize this table
|
||||
this.state = {
|
||||
dispatch: props.dispatch,
|
||||
triggerName: props.triggerName,
|
||||
};
|
||||
// this.state.dispatch = props.dispatch;
|
||||
// this.state.triggerName = props.triggerName;
|
||||
const dispatch = this.props.dispatch;
|
||||
Promise.all([
|
||||
dispatch(setTrigger(this.props.triggerName)),
|
||||
dispatch(vSetDefaults(this.props.triggerName)),
|
||||
dispatch(vMakeRequest()),
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const dispatch = this.props.dispatch;
|
||||
Promise.all([
|
||||
dispatch(setTrigger(this.props.triggerName)),
|
||||
dispatch(vSetDefaults(this.props.triggerName)),
|
||||
dispatch(vMakeRequest()),
|
||||
]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const dispatch = this.props.dispatch;
|
||||
if (nextProps.triggerName !== this.props.triggerName) {
|
||||
Promise.all([
|
||||
dispatch(setTrigger(nextProps.triggerName)),
|
||||
dispatch(vSetDefaults(nextProps.triggerName)),
|
||||
dispatch(vMakeRequest()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.triggerName === null ||
|
||||
nextProps.triggerName === this.props.triggerName
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
this.shouldScrollBottom =
|
||||
window.innerHeight ===
|
||||
document.body.offsetHeight - document.body.scrollTop;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.shouldScrollBottom) {
|
||||
document.body.scrollTop = document.body.offsetHeight - window.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove state data beloging to this table
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(vSetDefaults(this.props.triggerName));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
triggerName,
|
||||
triggerList,
|
||||
query,
|
||||
curFilter,
|
||||
rows,
|
||||
count, // eslint-disable-line no-unused-vars
|
||||
activePath,
|
||||
migrationMode,
|
||||
ongoingRequest,
|
||||
isProgressing,
|
||||
lastError,
|
||||
lastSuccess,
|
||||
dispatch,
|
||||
expandedRow,
|
||||
} = this.props; // eslint-disable-line no-unused-vars
|
||||
|
||||
// check if table exists
|
||||
const currentTrigger = triggerList.find(s => s.name === triggerName);
|
||||
if (!currentTrigger) {
|
||||
// dispatch a 404 route
|
||||
console.log(triggerName);
|
||||
console.log(triggerList);
|
||||
dispatch(replace('/404'));
|
||||
}
|
||||
// Is this a view
|
||||
const isView = false;
|
||||
|
||||
// Are there any expanded columns
|
||||
const viewRows = (
|
||||
<ViewRows
|
||||
curTriggerName={triggerName}
|
||||
curQuery={query}
|
||||
curFilter={curFilter}
|
||||
curPath={[]}
|
||||
curRows={rows}
|
||||
isView={isView}
|
||||
parentTableName={null}
|
||||
activePath={activePath}
|
||||
ongoingRequest={ongoingRequest}
|
||||
isProgressing={isProgressing}
|
||||
lastError={lastError}
|
||||
lastSuccess={lastSuccess}
|
||||
triggerList={triggerList}
|
||||
curDepth={0}
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
expandedRow={expandedRow}
|
||||
/>
|
||||
);
|
||||
|
||||
// Choose the right nav bar header thing
|
||||
const header = (
|
||||
<TableHeader
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
triggerName={triggerName}
|
||||
tabName="processed"
|
||||
migrationMode={migrationMode}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<div>{viewRows}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewTable.propTypes = {
|
||||
triggerName: PropTypes.string.isRequired,
|
||||
triggerList: PropTypes.array.isRequired,
|
||||
activePath: PropTypes.array.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
curFilter: PropTypes.object.isRequired,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
ongoingRequest: PropTypes.bool.isRequired,
|
||||
isProgressing: PropTypes.bool.isRequired,
|
||||
rows: PropTypes.array.isRequired,
|
||||
expandedRow: PropTypes.string.isRequired,
|
||||
count: PropTypes.number,
|
||||
lastError: PropTypes.object.isRequired,
|
||||
lastSuccess: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
triggerName: ownProps.params.trigger,
|
||||
triggerList: state.triggers.processedEvents,
|
||||
migrationMode: state.main.migrationMode,
|
||||
...state.triggers.view,
|
||||
};
|
||||
};
|
||||
|
||||
const processedEventsConnector = connect => connect(mapStateToProps)(ViewTable);
|
||||
|
||||
export default processedEventsConnector;
|
@ -0,0 +1,246 @@
|
||||
import { defaultCurFilter } from '../EventState';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const LOADING = 'RunningEvents/FilterQuery/LOADING';
|
||||
|
||||
const SET_DEFQUERY = 'RunningEvents/FilterQuery/SET_DEFQUERY';
|
||||
const SET_FILTERCOL = 'RunningEvents/FilterQuery/SET_FILTERCOL';
|
||||
const SET_FILTEROP = 'RunningEvents/FilterQuery/SET_FILTEROP';
|
||||
const SET_FILTERVAL = 'RunningEvents/FilterQuery/SET_FILTERVAL';
|
||||
const ADD_FILTER = 'RunningEvents/FilterQuery/ADD_FILTER';
|
||||
const REMOVE_FILTER = 'RunningEvents/FilterQuery/REMOVE_FILTER';
|
||||
|
||||
const SET_ORDERCOL = 'RunningEvents/FilterQuery/SET_ORDERCOL';
|
||||
const SET_ORDERTYPE = 'RunningEvents/FilterQuery/SET_ORDERTYPE';
|
||||
const ADD_ORDER = 'RunningEvents/FilterQuery/ADD_ORDER';
|
||||
const REMOVE_ORDER = 'RunningEvents/FilterQuery/REMOVE_ORDER';
|
||||
const SET_LIMIT = 'RunningEvents/FilterQuery/SET_LIMIT';
|
||||
const SET_OFFSET = 'RunningEvents/FilterQuery/SET_OFFSET';
|
||||
const SET_NEXTPAGE = 'RunningEvents/FilterQuery/SET_NEXTPAGE';
|
||||
const SET_PREVPAGE = 'RunningEvents/FilterQuery/SET_PREVPAGE';
|
||||
|
||||
const setLoading = () => ({ type: LOADING, data: true });
|
||||
const unsetLoading = () => ({ type: LOADING, data: false });
|
||||
const setDefaultQuery = curQuery => ({ type: SET_DEFQUERY, curQuery });
|
||||
const setFilterCol = (name, index) => ({ type: SET_FILTERCOL, name, index });
|
||||
const setFilterOp = (opName, index) => ({ type: SET_FILTEROP, opName, index });
|
||||
const setFilterVal = (val, index) => ({ type: SET_FILTERVAL, val, index });
|
||||
const addFilter = () => ({ type: ADD_FILTER });
|
||||
const removeFilter = index => ({ type: REMOVE_FILTER, index });
|
||||
|
||||
const setOrderCol = (name, index) => ({ type: SET_ORDERCOL, name, index });
|
||||
const setOrderType = (order, index) => ({ type: SET_ORDERTYPE, order, index });
|
||||
const addOrder = () => ({ type: ADD_ORDER });
|
||||
const removeOrder = index => ({ type: REMOVE_ORDER, index });
|
||||
|
||||
const setLimit = limit => ({ type: SET_LIMIT, limit });
|
||||
const setOffset = offset => ({ type: SET_OFFSET, offset });
|
||||
const setNextPage = () => ({ type: SET_NEXTPAGE });
|
||||
const setPrevPage = () => ({ type: SET_PREVPAGE });
|
||||
|
||||
const runQuery = triggerSchema => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState().triggers.view.curFilter;
|
||||
const finalWhereClauses = state.where.$and.filter(w => {
|
||||
const colName = Object.keys(w)[0].trim();
|
||||
if (colName === '') {
|
||||
return false;
|
||||
}
|
||||
const opName = Object.keys(w[colName])[0].trim();
|
||||
if (opName === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const newQuery = {
|
||||
where: { $and: finalWhereClauses },
|
||||
limit: state.limit,
|
||||
offset: state.offset,
|
||||
order_by: state.order_by.filter(w => w.column.trim() !== ''),
|
||||
};
|
||||
if (newQuery.where.$and.length === 0) {
|
||||
delete newQuery.where;
|
||||
}
|
||||
if (newQuery.order_by.length === 0) {
|
||||
delete newQuery.order_by;
|
||||
}
|
||||
dispatch({ type: 'RunningEvents/V_SET_QUERY_OPTS', queryStuff: newQuery });
|
||||
dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
|
||||
const pendingFilterReducer = (state = defaultCurFilter, action) => {
|
||||
const i = action.index;
|
||||
const newFilter = {};
|
||||
switch (action.type) {
|
||||
case SET_DEFQUERY:
|
||||
const q = action.curQuery;
|
||||
if (
|
||||
'order_by' in q ||
|
||||
'limit' in q ||
|
||||
'offset' in q ||
|
||||
('where' in q && '$and' in q.where)
|
||||
) {
|
||||
const newCurFilterQ = {};
|
||||
newCurFilterQ.where =
|
||||
'where' in q && '$and' in q.where
|
||||
? { $and: [...q.where.$and, { '': { '': '' } }] }
|
||||
: { ...defaultCurFilter.where };
|
||||
newCurFilterQ.order_by =
|
||||
'order_by' in q
|
||||
? [...q.order_by, ...defaultCurFilter.order_by]
|
||||
: [...defaultCurFilter.order_by];
|
||||
newCurFilterQ.limit = 'limit' in q ? q.limit : defaultCurFilter.limit;
|
||||
newCurFilterQ.offset =
|
||||
'offset' in q ? q.offset : defaultCurFilter.offset;
|
||||
return newCurFilterQ;
|
||||
}
|
||||
return defaultCurFilter;
|
||||
case SET_FILTERCOL:
|
||||
const oldColName = Object.keys(state.where.$and[i])[0];
|
||||
newFilter[action.name] = { ...state.where.$and[i][oldColName] };
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTEROP:
|
||||
const colName = Object.keys(state.where.$and[i])[0];
|
||||
const oldOp = Object.keys(state.where.$and[i][colName])[0];
|
||||
newFilter[colName] = {};
|
||||
newFilter[colName][action.opName] = state.where.$and[i][colName][oldOp];
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case SET_FILTERVAL:
|
||||
const colName1 = Object.keys(state.where.$and[i])[0];
|
||||
const opName = Object.keys(state.where.$and[i][colName1])[0];
|
||||
newFilter[colName1] = {};
|
||||
newFilter[colName1][opName] = action.val;
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [
|
||||
...state.where.$and.slice(0, i),
|
||||
newFilter,
|
||||
...state.where.$and.slice(i + 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
case ADD_FILTER:
|
||||
return {
|
||||
...state,
|
||||
where: {
|
||||
$and: [...state.where.$and, { '': { '': '' } }],
|
||||
},
|
||||
};
|
||||
case REMOVE_FILTER:
|
||||
const newFilters = [
|
||||
...state.where.$and.slice(0, i),
|
||||
...state.where.$and.slice(i + 1),
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
where: { $and: newFilters },
|
||||
};
|
||||
|
||||
case SET_ORDERCOL:
|
||||
const oldOrder = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder, column: action.name },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case SET_ORDERTYPE:
|
||||
const oldOrder1 = state.order_by[i];
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
{ ...oldOrder1, type: action.order },
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case REMOVE_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by.slice(0, i),
|
||||
...state.order_by.slice(i + 1),
|
||||
],
|
||||
};
|
||||
case ADD_ORDER:
|
||||
return {
|
||||
...state,
|
||||
order_by: [
|
||||
...state.order_by,
|
||||
{ column: '', type: 'asc', nulls: 'last' },
|
||||
],
|
||||
};
|
||||
|
||||
case SET_LIMIT:
|
||||
return {
|
||||
...state,
|
||||
limit: action.limit,
|
||||
};
|
||||
case SET_OFFSET:
|
||||
return {
|
||||
...state,
|
||||
offset: action.offset,
|
||||
};
|
||||
case SET_NEXTPAGE:
|
||||
return {
|
||||
...state,
|
||||
offset: state.offset + state.limit,
|
||||
};
|
||||
case SET_PREVPAGE:
|
||||
const newOffset = state.offset - state.limit;
|
||||
return {
|
||||
...state,
|
||||
offset: newOffset < 0 ? 0 : newOffset,
|
||||
};
|
||||
case LOADING:
|
||||
return {
|
||||
...state,
|
||||
loading: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default pendingFilterReducer;
|
||||
export {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
setLimit,
|
||||
setOffset,
|
||||
setNextPage,
|
||||
setPrevPage,
|
||||
setDefaultQuery,
|
||||
setLoading,
|
||||
unsetLoading,
|
||||
runQuery,
|
||||
};
|
@ -0,0 +1,265 @@
|
||||
/*
|
||||
Use state exactly the way columns in create table do.
|
||||
dispatch actions using a given function,
|
||||
but don't listen to state.
|
||||
derive everything through viewtable as much as possible.
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import Operators from '../Operators';
|
||||
import {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
} from './FilterActions.js';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
} from './FilterActions.js';
|
||||
import { setDefaultQuery, runQuery } from './FilterActions';
|
||||
import { vMakeRequest } from './ViewActions';
|
||||
|
||||
const renderCols = (colName, triggerSchema, onChange, usage, key) => {
|
||||
const columns = ['id', 'delivered', 'created_at'];
|
||||
return (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={colName.trim()}
|
||||
data-test={
|
||||
usage === 'sort' ? `sort-column-${key}` : `filter-column-${key}`
|
||||
}
|
||||
>
|
||||
{colName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{columns.map((c, i) => (
|
||||
<option key={i} value={c}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOps = (opName, onChange, key) => (
|
||||
<select
|
||||
className="form-control"
|
||||
onChange={onChange}
|
||||
value={opName.trim()}
|
||||
data-test={`filter-op-${key}`}
|
||||
>
|
||||
{opName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{Operators.map((o, i) => (
|
||||
<option key={i} value={o.value}>
|
||||
{o.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const renderWheres = (whereAnd, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return whereAnd.map((clause, i) => {
|
||||
const colName = Object.keys(clause)[0];
|
||||
const opName = Object.keys(clause[colName])[0];
|
||||
const dSetFilterCol = e => {
|
||||
dispatch(setFilterCol(e.target.value, i));
|
||||
};
|
||||
const dSetFilterOp = e => {
|
||||
dispatch(setFilterOp(e.target.value, i));
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < whereAnd.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeFilter(i));
|
||||
}}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-4">
|
||||
{renderCols(colName, triggerSchema, dSetFilterCol, 'filter', i)}
|
||||
</div>
|
||||
<div className="col-xs-3">{renderOps(opName, dSetFilterOp, i)}</div>
|
||||
<div className="col-xs-4">
|
||||
<input
|
||||
className="form-control"
|
||||
placeholder="-- value --"
|
||||
value={clause[colName][opName]}
|
||||
onChange={e => {
|
||||
dispatch(setFilterVal(e.target.value, i));
|
||||
if (i + 1 === whereAnd.length) {
|
||||
dispatch(addFilter());
|
||||
}
|
||||
}}
|
||||
data-test={`filter-value-${i}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center col-xs-1">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderSorts = (orderBy, triggerSchema, dispatch) => {
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return orderBy.map((c, i) => {
|
||||
const dSetOrderCol = e => {
|
||||
dispatch(setOrderCol(e.target.value, i));
|
||||
if (i + 1 === orderBy.length) {
|
||||
dispatch(addOrder());
|
||||
}
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < orderBy.length) {
|
||||
removeIcon = (
|
||||
<i
|
||||
className="fa fa-times"
|
||||
onClick={() => {
|
||||
dispatch(removeOrder(i));
|
||||
}}
|
||||
data-test={`clear-sorts-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} className={`${styles.inputRow} row`}>
|
||||
<div className="col-xs-6">
|
||||
{renderCols(c.column, triggerSchema, dSetOrderCol, 'sort', i)}
|
||||
</div>
|
||||
<div className="col-xs-5">
|
||||
<select
|
||||
value={c.type}
|
||||
className="form-control"
|
||||
onChange={e => {
|
||||
dispatch(setOrderType(e.target.value, i));
|
||||
}}
|
||||
data-test={`sort-order-${i}`}
|
||||
>
|
||||
<option value="asc">Asc</option>
|
||||
<option value="desc">Desc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-xs-1 text-center">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
class FilterQuery extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isWatching: false, intervalId: null };
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(setDefaultQuery(this.props.curQuery));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.intervalId);
|
||||
}
|
||||
|
||||
watchChanges() {
|
||||
// set state on watch
|
||||
this.setState({ isWatching: !this.state.isWatching });
|
||||
if (this.state.isWatching) {
|
||||
clearInterval(this.state.intervalId);
|
||||
} else {
|
||||
const intervalId = setInterval(this.refreshData, 2000);
|
||||
this.setState({ intervalId: intervalId });
|
||||
}
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.props.dispatch(vMakeRequest());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, whereAnd, triggerSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
|
||||
const styles = require('./FilterQuery.scss');
|
||||
return (
|
||||
<div className={styles.filterOptions}>
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<span className={styles.subheading_text}>Filter</span>
|
||||
{renderWheres(whereAnd, triggerSchema, dispatch)}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.queryBox} col-xs-6 ${
|
||||
styles.padd_left_remove
|
||||
}`}
|
||||
>
|
||||
<b className={styles.subheading_text}>Sort</b>
|
||||
{renderSorts(orderBy, triggerSchema, dispatch)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${styles.padd_right} ${styles.clear_fix}`}>
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn ${styles.yellow_button}`}
|
||||
data-test="run-query"
|
||||
>
|
||||
Run query
|
||||
</button>
|
||||
<button
|
||||
onClick={this.watchChanges.bind(this)}
|
||||
className={styles.add_mar_left + ' btn btn-default'}
|
||||
data-test="run-query"
|
||||
>
|
||||
{this.state.isWatching ? (
|
||||
<span>
|
||||
Watching <i className={'fa fa-spinner fa-spin'} />
|
||||
</span>
|
||||
) : (
|
||||
'Watch'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuery.propTypes = {
|
||||
curQuery: PropTypes.object.isRequired,
|
||||
triggerSchema: PropTypes.object.isRequired,
|
||||
whereAnd: PropTypes.array.isRequired,
|
||||
orderBy: PropTypes.array.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
count: PropTypes.number,
|
||||
triggerName: PropTypes.string,
|
||||
offset: PropTypes.number.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
@ -0,0 +1,95 @@
|
||||
@import "../../../Common/Common.scss";
|
||||
$bgColor: #f9f9f9;
|
||||
.container {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline-block;
|
||||
max-height: 34px;
|
||||
padding: 5px 20px;
|
||||
margin-left: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.queryBox {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-height: 30px;
|
||||
.inputRow {
|
||||
margin: 20px 0;
|
||||
div[class^=col-xs-] {
|
||||
padding-left: 0;
|
||||
padding-right: 2.5px;
|
||||
}
|
||||
:global(.form-control) {
|
||||
height: 35px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.fa) {
|
||||
padding-top: 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
}
|
||||
.descending {
|
||||
padding-left: 10px;
|
||||
input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.runQuery {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 20px;
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
:global(.input-group) {
|
||||
margin-top: -24px;
|
||||
margin-right: 10px;
|
||||
input {
|
||||
max-width: 60px;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
margin-top: -24px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterOptions {
|
||||
margin-top: 10px;
|
||||
background: $bgColor;
|
||||
// padding: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boxHeading {
|
||||
top: -0.55em;
|
||||
line-height: 1.1em;
|
||||
background: $bgColor;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
padding: 0 10px;
|
||||
color: #929292;
|
||||
font-weight: normal;
|
||||
}
|
@ -0,0 +1,472 @@
|
||||
import { defaultViewState } from '../EventState';
|
||||
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import requestAction from 'utils/requestAction';
|
||||
import pendingFilterReducer from './FilterActions';
|
||||
import { findTableFromRel } from '../utils';
|
||||
import dataHeaders from '../Common/Headers';
|
||||
|
||||
/* ****************** View actions *************/
|
||||
const V_SET_DEFAULTS = 'RunningEvents/V_SET_DEFAULTS';
|
||||
const V_REQUEST_SUCCESS = 'RunningEvents/V_REQUEST_SUCCESS';
|
||||
const V_REQUEST_ERROR = 'RunningEvents/V_REQUEST_ERROR';
|
||||
const V_EXPAND_REL = 'RunningEvents/V_EXPAND_REL';
|
||||
const V_CLOSE_REL = 'RunningEvents/V_CLOSE_REL';
|
||||
const V_SET_ACTIVE = 'RunningEvents/V_SET_ACTIVE';
|
||||
const V_SET_QUERY_OPTS = 'RunningEvents/V_SET_QUERY_OPTS';
|
||||
const V_REQUEST_PROGRESS = 'RunningEvents/V_REQUEST_PROGRESS';
|
||||
const V_EXPAND_ROW = 'RunningEvents/V_EXPAND_ROW';
|
||||
const V_COLLAPSE_ROW = 'RunningEvents/V_COLLAPSE_ROW';
|
||||
|
||||
/* ****************** action creators *************/
|
||||
|
||||
const vExpandRow = rowKey => ({
|
||||
type: V_EXPAND_ROW,
|
||||
data: rowKey,
|
||||
});
|
||||
|
||||
const vCollapseRow = () => ({
|
||||
type: V_COLLAPSE_ROW,
|
||||
});
|
||||
|
||||
const vSetDefaults = () => ({ type: V_SET_DEFAULTS });
|
||||
|
||||
const vMakeRequest = () => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const url = Endpoints.query;
|
||||
const originalTrigger = getState().triggers.currentTrigger;
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: true });
|
||||
const currentQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
// count query
|
||||
const countQuery = JSON.parse(JSON.stringify(state.triggers.view.query));
|
||||
countQuery.columns = ['id'];
|
||||
|
||||
// delivered = false and error = false
|
||||
// where clause for relationship
|
||||
const currentWhereClause = state.triggers.view.query.where;
|
||||
if (currentWhereClause && currentWhereClause.$and) {
|
||||
// make filter for events
|
||||
const finalAndClause = currentQuery.where.$and;
|
||||
finalAndClause.push({ delivered: false });
|
||||
finalAndClause.push({ error: false });
|
||||
currentQuery.columns[1].where = { $and: finalAndClause };
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where.$and.push({
|
||||
trigger_name: state.triggers.currentTrigger,
|
||||
});
|
||||
} else {
|
||||
// reset where for events
|
||||
if (currentQuery.columns[1]) {
|
||||
currentQuery.columns[1].where = {
|
||||
delivered: false,
|
||||
error: false,
|
||||
tries: { $gt: 0 },
|
||||
};
|
||||
}
|
||||
currentQuery.where = { name: state.triggers.currentTrigger };
|
||||
countQuery.where = {
|
||||
trigger_name: state.triggers.currentTrigger,
|
||||
delivered: false,
|
||||
error: false,
|
||||
tries: { $gt: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// order_by for relationship
|
||||
const currentOrderBy = state.triggers.view.query.order_by;
|
||||
if (currentOrderBy) {
|
||||
currentQuery.columns[1].order_by = currentOrderBy;
|
||||
// reset order_by
|
||||
delete currentQuery.order_by;
|
||||
} else {
|
||||
// reset order by for events
|
||||
if (currentQuery.columns[1]) {
|
||||
delete currentQuery.columns[1].order_by;
|
||||
currentQuery.columns[1].order_by = ['-created_at'];
|
||||
}
|
||||
delete currentQuery.order_by;
|
||||
}
|
||||
|
||||
// limit and offset for relationship
|
||||
const currentLimit = state.triggers.view.query.limit;
|
||||
const currentOffset = state.triggers.view.query.offset;
|
||||
currentQuery.columns[1].limit = currentLimit;
|
||||
currentQuery.columns[1].offset = currentOffset;
|
||||
|
||||
// reset limit and offset for parent
|
||||
delete currentQuery.limit;
|
||||
delete currentQuery.offset;
|
||||
delete countQuery.limit;
|
||||
delete countQuery.offset;
|
||||
|
||||
const requestBody = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
args: {
|
||||
...currentQuery,
|
||||
table: {
|
||||
name: 'event_triggers',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'count',
|
||||
args: {
|
||||
...countQuery,
|
||||
table: {
|
||||
name: 'event_log',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: dataHeaders(getState),
|
||||
credentials: globalCookiePolicy,
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
const currentTrigger = getState().triggers.currentTrigger;
|
||||
if (originalTrigger === currentTrigger) {
|
||||
Promise.all([
|
||||
dispatch({
|
||||
type: V_REQUEST_SUCCESS,
|
||||
data: data[0],
|
||||
count: data[1].count,
|
||||
}),
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: false }),
|
||||
]);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch({ type: V_REQUEST_ERROR, data: error });
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const vExpandRel = (path, relname, pk) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_EXPAND_REL, path, relname, pk });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
const vCloseRel = (path, relname) => {
|
||||
return dispatch => {
|
||||
// Modify the query (UI will automatically change)
|
||||
dispatch({ type: V_CLOSE_REL, path, relname });
|
||||
// Make a request
|
||||
return dispatch(vMakeRequest());
|
||||
};
|
||||
};
|
||||
/* ************ helpers ************************/
|
||||
const defaultSubQuery = (relname, tableSchema) => {
|
||||
return {
|
||||
name: relname,
|
||||
columns: tableSchema.columns.map(c => c.column_name),
|
||||
};
|
||||
};
|
||||
|
||||
const expandQuery = (
|
||||
curQuery,
|
||||
curTable,
|
||||
pk,
|
||||
curPath,
|
||||
relname,
|
||||
schemas,
|
||||
isObjRel = false
|
||||
) => {
|
||||
if (curPath.length === 0) {
|
||||
const rel = curTable.relationships.find(r => r.rel_name === relname);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, rel);
|
||||
|
||||
const newColumns = [
|
||||
...curQuery.columns,
|
||||
defaultSubQuery(relname, childTableSchema),
|
||||
];
|
||||
if (isObjRel) {
|
||||
return { ...curQuery, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's already oldStuff then don't reset it
|
||||
if ('oldStuff' in curQuery) {
|
||||
return { ...curQuery, where: pk, columns: newColumns };
|
||||
}
|
||||
|
||||
// If there's no oldStuff then set it
|
||||
const oldStuff = {};
|
||||
['where', 'limit', 'offset'].map(k => {
|
||||
if (k in curQuery) {
|
||||
oldStuff[k] = curQuery[k];
|
||||
}
|
||||
});
|
||||
return { name: curQuery.name, where: pk, columns: newColumns, oldStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
expandQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
pk,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas,
|
||||
curRel.rel_type === 'object'
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const closeQuery = (curQuery, curTable, curPath, relname, schemas) => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
if (curPath.length === 0) {
|
||||
const expandedIndex = curQuery.columns.findIndex(c => c.name === relname);
|
||||
const newColumns = [
|
||||
...curQuery.columns.slice(0, expandedIndex),
|
||||
...curQuery.columns.slice(expandedIndex + 1),
|
||||
];
|
||||
const newStuff = {};
|
||||
newStuff.columns = newColumns;
|
||||
if ('name' in curQuery) {
|
||||
newStuff.name = curQuery.name;
|
||||
}
|
||||
// If no other expanded columns are left
|
||||
if (!newColumns.find(c => typeof c === 'object')) {
|
||||
if (curQuery.oldStuff) {
|
||||
['where', 'limit', 'order_by', 'offset'].map(k => {
|
||||
if (k in curQuery.oldStuff) {
|
||||
newStuff[k] = curQuery.oldStuff[k];
|
||||
}
|
||||
});
|
||||
}
|
||||
return { ...newStuff };
|
||||
}
|
||||
return { ...curQuery, ...newStuff };
|
||||
}
|
||||
|
||||
const curRelName = curPath[0];
|
||||
const curRel = curTable.relationships.find(r => r.rel_name === curRelName);
|
||||
const childTableSchema = findTableFromRel(schemas, curTable, curRel);
|
||||
const curRelColIndex = curQuery.columns.findIndex(c => c.name === curRelName);
|
||||
return {
|
||||
...curQuery,
|
||||
columns: [
|
||||
...curQuery.columns.slice(0, curRelColIndex),
|
||||
closeQuery(
|
||||
curQuery.columns[curRelColIndex],
|
||||
childTableSchema,
|
||||
curPath.slice(1),
|
||||
relname,
|
||||
schemas
|
||||
),
|
||||
...curQuery.columns.slice(curRelColIndex + 1),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const setActivePath = (activePath, curPath, relname, query) => {
|
||||
const basePath = relname
|
||||
? [activePath[0], ...curPath, relname]
|
||||
: [activePath[0], ...curPath];
|
||||
|
||||
// Now check if there are any more children on this path.
|
||||
// If there are, then we should expand them by default
|
||||
let subQuery = query;
|
||||
let subBase = basePath.slice(1);
|
||||
|
||||
while (subBase.length > 0) {
|
||||
subQuery = subQuery.columns.find(c => c.name === subBase[0]); // eslint-disable-line no-loop-func
|
||||
subBase = subBase.slice(1);
|
||||
}
|
||||
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
while (subQuery) {
|
||||
basePath.push(subQuery.name);
|
||||
subQuery = subQuery.columns.find(c => typeof c === 'object');
|
||||
}
|
||||
|
||||
return basePath;
|
||||
};
|
||||
const updateActivePathOnClose = (
|
||||
activePath,
|
||||
tableName,
|
||||
curPath,
|
||||
relname,
|
||||
query
|
||||
) => {
|
||||
const basePath = [tableName, ...curPath, relname];
|
||||
let subBase = [...basePath];
|
||||
let subActive = [...activePath];
|
||||
let matchingFound = false;
|
||||
let commonIndex = 0;
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
|
||||
while (subActive.length > 0) {
|
||||
if (subBase[0] === subActive[0]) {
|
||||
matchingFound = true;
|
||||
break;
|
||||
}
|
||||
subBase = subBase.slice(1);
|
||||
subActive = subActive.slice(1);
|
||||
commonIndex += 1;
|
||||
}
|
||||
|
||||
if (matchingFound) {
|
||||
const newActivePath = activePath.slice(0, commonIndex + 1);
|
||||
return setActivePath(
|
||||
newActivePath,
|
||||
newActivePath.slice(1, -1),
|
||||
null,
|
||||
query
|
||||
);
|
||||
}
|
||||
return [...activePath];
|
||||
};
|
||||
const addQueryOptsActivePath = (query, queryStuff, activePath) => {
|
||||
let curPath = activePath.slice(1);
|
||||
const newQuery = { ...query };
|
||||
let curQuery = newQuery;
|
||||
while (curPath.length > 0) {
|
||||
curQuery = curQuery.columns.find(c => c.name === curPath[0]); // eslint-disable-line no-loop-func
|
||||
curPath = curPath.slice(1);
|
||||
}
|
||||
|
||||
['where', 'order_by', 'limit', 'offset'].map(k => {
|
||||
delete curQuery[k];
|
||||
});
|
||||
|
||||
for (const k in queryStuff) {
|
||||
if (queryStuff.hasOwnProperty(k)) {
|
||||
curQuery[k] = queryStuff[k];
|
||||
}
|
||||
}
|
||||
return newQuery;
|
||||
};
|
||||
/* ****************** reducer ******************/
|
||||
const RunningEventsReducer = (triggerName, triggerList, viewState, action) => {
|
||||
if (action.type.indexOf('RunningEvents/FilterQuery/') === 0) {
|
||||
return {
|
||||
...viewState,
|
||||
curFilter: pendingFilterReducer(viewState.curFilter, action),
|
||||
};
|
||||
}
|
||||
const tableSchema = triggerList.find(x => x.name === triggerName);
|
||||
switch (action.type) {
|
||||
case V_SET_DEFAULTS:
|
||||
return {
|
||||
...defaultViewState,
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'events',
|
||||
columns: [
|
||||
'*',
|
||||
{ name: 'logs', columns: ['*'], order_by: ['-created_at'] },
|
||||
],
|
||||
where: { delivered: false, error: false },
|
||||
},
|
||||
],
|
||||
limit: 10,
|
||||
},
|
||||
activePath: [triggerName],
|
||||
rows: [],
|
||||
count: null,
|
||||
};
|
||||
case V_SET_QUERY_OPTS:
|
||||
return {
|
||||
...viewState,
|
||||
query: addQueryOptsActivePath(
|
||||
viewState.query,
|
||||
action.queryStuff,
|
||||
viewState.activePath
|
||||
),
|
||||
};
|
||||
case V_EXPAND_REL:
|
||||
return {
|
||||
...viewState,
|
||||
query: expandQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.pk,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
),
|
||||
activePath: [...viewState.activePath, action.relname],
|
||||
};
|
||||
case V_CLOSE_REL:
|
||||
const _query = closeQuery(
|
||||
viewState.query,
|
||||
tableSchema,
|
||||
action.path,
|
||||
action.relname,
|
||||
triggerList
|
||||
);
|
||||
return {
|
||||
...viewState,
|
||||
query: _query,
|
||||
activePath: updateActivePathOnClose(
|
||||
viewState.activePath,
|
||||
triggerName,
|
||||
action.path,
|
||||
action.relname,
|
||||
_query
|
||||
),
|
||||
};
|
||||
case V_SET_ACTIVE:
|
||||
return {
|
||||
...viewState,
|
||||
activePath: setActivePath(
|
||||
viewState.activePath,
|
||||
action.path,
|
||||
action.relname,
|
||||
viewState.query
|
||||
),
|
||||
};
|
||||
case V_REQUEST_SUCCESS:
|
||||
return { ...viewState, rows: action.data, count: action.count };
|
||||
case V_REQUEST_PROGRESS:
|
||||
return { ...viewState, isProgressing: action.data };
|
||||
case V_EXPAND_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: action.data,
|
||||
};
|
||||
case V_COLLAPSE_ROW:
|
||||
return {
|
||||
...viewState,
|
||||
expandedRow: '',
|
||||
};
|
||||
default:
|
||||
return viewState;
|
||||
}
|
||||
};
|
||||
|
||||
export default RunningEventsReducer;
|
||||
export {
|
||||
vSetDefaults,
|
||||
vMakeRequest,
|
||||
vExpandRel,
|
||||
vCloseRel,
|
||||
vExpandRow,
|
||||
vCollapseRow,
|
||||
V_SET_ACTIVE,
|
||||
};
|
@ -0,0 +1,403 @@
|
||||
import React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/json';
|
||||
import Tabs from 'react-bootstrap/lib/Tabs';
|
||||
import Tab from 'react-bootstrap/lib/Tab';
|
||||
import 'react-table/react-table.css';
|
||||
import { deleteItem, vExpandRow, vCollapseRow } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import FilterQuery from './FilterQuery';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
removeOrder,
|
||||
runQuery,
|
||||
setOffset,
|
||||
setLimit,
|
||||
addOrder,
|
||||
} from './FilterActions';
|
||||
import { ordinalColSort } from '../utils';
|
||||
import Spinner from '../../../Common/Spinner/Spinner';
|
||||
import '../TableCommon/ReactTableFix.css';
|
||||
|
||||
const ViewRows = ({
|
||||
curTriggerName,
|
||||
curQuery,
|
||||
curFilter,
|
||||
curRows,
|
||||
curPath,
|
||||
curDepth,
|
||||
activePath,
|
||||
triggerList,
|
||||
dispatch,
|
||||
isProgressing,
|
||||
isView,
|
||||
count,
|
||||
expandedRow,
|
||||
}) => {
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
const triggerSchema = triggerList.find(x => x.name === curTriggerName);
|
||||
const curRelName = curPath.length > 0 ? curPath.slice(-1)[0] : null;
|
||||
|
||||
// Am I a single row display
|
||||
const isSingleRow = false;
|
||||
|
||||
// Get the headings
|
||||
const tableHeadings = [];
|
||||
const gridHeadings = [];
|
||||
const eventLogColumns = ['id', 'delivered', 'created_at'];
|
||||
const sortedColumns = eventLogColumns.sort(ordinalColSort);
|
||||
|
||||
sortedColumns.map((column, i) => {
|
||||
tableHeadings.push(<th key={i}>{column}</th>);
|
||||
gridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
|
||||
const hasPrimaryKeys = true;
|
||||
/*
|
||||
let editButton;
|
||||
let deleteButton;
|
||||
*/
|
||||
|
||||
const newCurRows = [];
|
||||
if (curRows && curRows[0] && curRows[0].events) {
|
||||
curRows[0].events.forEach((row, rowIndex) => {
|
||||
const newRow = {};
|
||||
const pkClause = {};
|
||||
if (!isView && hasPrimaryKeys) {
|
||||
pkClause.id = row.id;
|
||||
} else {
|
||||
triggerSchema.map(k => {
|
||||
pkClause[k] = row[k];
|
||||
});
|
||||
}
|
||||
/*
|
||||
if (!isSingleRow && !isView && hasPrimaryKeys) {
|
||||
deleteButton = (
|
||||
<button
|
||||
className={`${styles.add_mar_right_small} btn btn-xs btn-default`}
|
||||
onClick={() => {
|
||||
dispatch(deleteItem(pkClause));
|
||||
}}
|
||||
data-test={`row-delete-button-${rowIndex}`}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
);
|
||||
}
|
||||
const buttonsDiv = (
|
||||
<div className={styles.tableCellCenterAligned}>
|
||||
{editButton}
|
||||
{deleteButton}
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
// Insert Edit, Delete, Clone in a cell
|
||||
// newRow.actions = buttonsDiv;
|
||||
// Insert cells corresponding to all rows
|
||||
sortedColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (row[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let content = row[col] === undefined ? 'NULL' : row[col].toString();
|
||||
if (col === 'created_at') {
|
||||
content = new Date(row[col]).toUTCString();
|
||||
}
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
newCurRows.push(newRow);
|
||||
});
|
||||
}
|
||||
|
||||
// Is this ViewRows visible
|
||||
let isVisible = false;
|
||||
if (!curRelName) {
|
||||
isVisible = true;
|
||||
} else if (curRelName === activePath[curDepth]) {
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
let filterQuery = null;
|
||||
if (!isSingleRow) {
|
||||
if (curRelName === activePath[curDepth] || curDepth === 0) {
|
||||
// Rendering only if this is the activePath or this is the root
|
||||
|
||||
let wheres = [{ '': { '': '' } }];
|
||||
if ('where' in curFilter && '$and' in curFilter.where) {
|
||||
wheres = [...curFilter.where.$and];
|
||||
}
|
||||
|
||||
let orderBy = [{ column: '', type: 'asc', nulls: 'last' }];
|
||||
if ('order_by' in curFilter) {
|
||||
orderBy = [...curFilter.order_by];
|
||||
}
|
||||
const limit = 'limit' in curFilter ? curFilter.limit : 10;
|
||||
const offset = 'offset' in curFilter ? curFilter.offset : 0;
|
||||
|
||||
filterQuery = (
|
||||
<FilterQuery
|
||||
curQuery={curQuery}
|
||||
whereAnd={wheres}
|
||||
triggerSchema={triggerSchema}
|
||||
orderBy={orderBy}
|
||||
limit={limit}
|
||||
dispatch={dispatch}
|
||||
count={count}
|
||||
triggerName={curTriggerName}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortByColumn = col => {
|
||||
// Remove all the existing order_bys
|
||||
const numOfOrderBys = curFilter.order_by.length;
|
||||
for (let i = 0; i < numOfOrderBys - 1; i++) {
|
||||
dispatch(removeOrder(1));
|
||||
}
|
||||
// Go back to the first page
|
||||
dispatch(setOffset(0));
|
||||
// Set the filter and run query
|
||||
dispatch(setOrderCol(col, 0));
|
||||
if (
|
||||
curFilter.order_by.length !== 0 &&
|
||||
curFilter.order_by[0].column === col &&
|
||||
curFilter.order_by[0].type === 'asc'
|
||||
) {
|
||||
dispatch(setOrderType('desc', 0));
|
||||
} else {
|
||||
dispatch(setOrderType('asc', 0));
|
||||
}
|
||||
dispatch(runQuery(triggerSchema));
|
||||
// Add a new empty filter
|
||||
dispatch(addOrder());
|
||||
};
|
||||
|
||||
const changePage = page => {
|
||||
if (curFilter.offset !== page * curFilter.limit) {
|
||||
dispatch(setOffset(page * curFilter.limit));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const changePageSize = size => {
|
||||
if (curFilter.size !== size) {
|
||||
dispatch(setLimit(size));
|
||||
dispatch(runQuery(triggerSchema));
|
||||
}
|
||||
};
|
||||
|
||||
const renderTableBody = () => {
|
||||
if (isProgressing) {
|
||||
return (
|
||||
<div>
|
||||
{' '}
|
||||
<Spinner />{' '}
|
||||
</div>
|
||||
);
|
||||
} else if (count === 0) {
|
||||
return <div> No rows found. </div>;
|
||||
}
|
||||
let shouldSortColumn = true;
|
||||
const invocationColumns = ['status', 'id', 'created_at'];
|
||||
const invocationGridHeadings = [];
|
||||
invocationColumns.map(column => {
|
||||
invocationGridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
return (
|
||||
<ReactTable
|
||||
className="-highlight"
|
||||
data={newCurRows}
|
||||
columns={gridHeadings}
|
||||
resizable
|
||||
manual
|
||||
sortable={false}
|
||||
minRows={0}
|
||||
getTheadThProps={(finalState, some, column) => ({
|
||||
onClick: () => {
|
||||
if (
|
||||
column.Header &&
|
||||
shouldSortColumn &&
|
||||
column.Header !== 'Actions'
|
||||
) {
|
||||
sortByColumn(column.Header);
|
||||
}
|
||||
shouldSortColumn = true;
|
||||
},
|
||||
})}
|
||||
getResizerProps={(finalState, none, column, ctx) => ({
|
||||
onMouseDown: e => {
|
||||
shouldSortColumn = false;
|
||||
ctx.resizeColumnStart(e, column, false);
|
||||
},
|
||||
})}
|
||||
showPagination={count > curFilter.limit}
|
||||
defaultPageSize={Math.min(curFilter.limit, count)}
|
||||
pages={Math.ceil(count / curFilter.limit)}
|
||||
onPageChange={changePage}
|
||||
onPageSizeChange={changePageSize}
|
||||
page={Math.floor(curFilter.offset / curFilter.limit)}
|
||||
SubComponent={row => {
|
||||
const currentIndex = row.index;
|
||||
const currentRow = curRows[0].events[currentIndex];
|
||||
const invocationRowsData = [];
|
||||
currentRow.logs.map((r, rowIndex) => {
|
||||
const newRow = {};
|
||||
const status =
|
||||
r.status === 200 ? (
|
||||
<i className={styles.invocationSuccess + ' fa fa-check'} />
|
||||
) : (
|
||||
<i className={styles.invocationFailure + ' fa fa-times'} />
|
||||
);
|
||||
|
||||
// Insert cells corresponding to all rows
|
||||
invocationColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
let conditionalClassname = styles.tableCellCenterAligned;
|
||||
const cellIndex = `${curTriggerName}-${col}-${rowIndex}`;
|
||||
if (expandedRow === cellIndex) {
|
||||
conditionalClassname = styles.tableCellCenterAlignedExpanded;
|
||||
}
|
||||
if (r[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (col === 'status') {
|
||||
return status;
|
||||
}
|
||||
if (col === 'created_at') {
|
||||
const formattedDate = new Date(r.created_at).toUTCString();
|
||||
return formattedDate;
|
||||
}
|
||||
const content =
|
||||
r[col] === undefined ? 'NULL' : r[col].toString();
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
invocationRowsData.push(newRow);
|
||||
});
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<em>Recent Invocations</em>
|
||||
<div
|
||||
className={styles.invocationsSection + ' invocationsSection'}
|
||||
>
|
||||
{invocationRowsData.length ? (
|
||||
<ReactTable
|
||||
data={invocationRowsData}
|
||||
columns={invocationGridHeadings}
|
||||
defaultPageSize={currentRow.logs.length}
|
||||
showPagination={false}
|
||||
SubComponent={logRow => {
|
||||
const finalIndex = logRow.index;
|
||||
const currentPayload = JSON.stringify(
|
||||
currentRow.payload,
|
||||
null,
|
||||
4
|
||||
);
|
||||
const finalRow = currentRow.logs[finalIndex];
|
||||
const finalResponse = JSON.parse(
|
||||
JSON.stringify(finalRow.response, null, 4)
|
||||
);
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Tabs
|
||||
animation={false}
|
||||
defaultActiveKey={1}
|
||||
id="requestResponseTab"
|
||||
>
|
||||
<Tab eventKey={1} title="Request">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Request
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="payload"
|
||||
value={currentPayload}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey={2} title="Response">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>
|
||||
Response
|
||||
</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="response"
|
||||
value={finalResponse}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.add_mar_top}>No data available</div>
|
||||
)}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={isVisible ? '' : 'hide '}>
|
||||
{filterQuery}
|
||||
<hr />
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className={styles.tableContainer + ' eventsTableBody'}>
|
||||
{renderTableBody()}
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewRows;
|
@ -0,0 +1,214 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { vSetDefaults, vMakeRequest, vExpandHeading } from './ViewActions'; // eslint-disable-line no-unused-vars
|
||||
import { setTrigger } from '../EventActions';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import ViewRows from './ViewRows';
|
||||
import { replace } from 'react-router-redux';
|
||||
|
||||
const genHeadings = headings => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [heading, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const headingName =
|
||||
heading.type === 'obj_rel' ? heading.lcol : heading.relname;
|
||||
return [
|
||||
{ name: headingName, type: heading.type },
|
||||
...genHeadings(headings.slice(1)),
|
||||
];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subheadings = genHeadings(heading.headings).map(h => {
|
||||
if (typeof h === 'string') {
|
||||
return heading.relname + '.' + h;
|
||||
}
|
||||
return heading.relname + '.' + h.name;
|
||||
});
|
||||
return [...subheadings, ...genHeadings(headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
const genRow = (row, headings) => {
|
||||
if (headings.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const heading = headings[0];
|
||||
if (typeof heading === 'string') {
|
||||
return [row[heading], ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (typeof heading === 'object') {
|
||||
if (!heading._expanded) {
|
||||
const rowVal = heading.type === 'obj_rel' ? row[heading.lcol] : '[...]';
|
||||
return [rowVal, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
if (heading.type === 'obj_rel') {
|
||||
const subrow = genRow(row[heading.relname], heading.headings);
|
||||
return [...subrow, ...genRow(row, headings.slice(1))];
|
||||
}
|
||||
}
|
||||
|
||||
throw 'Incomplete pattern match'; // eslint-disable-line no-throw-literal
|
||||
};
|
||||
|
||||
class ViewTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize this table
|
||||
this.state = {
|
||||
dispatch: props.dispatch,
|
||||
triggerName: props.triggerName,
|
||||
};
|
||||
// this.state.dispatch = props.dispatch;
|
||||
// this.state.triggerName = props.triggerName;
|
||||
const dispatch = this.props.dispatch;
|
||||
Promise.all([
|
||||
dispatch(setTrigger(this.props.triggerName)),
|
||||
dispatch(vSetDefaults(this.props.triggerName)),
|
||||
dispatch(vMakeRequest()),
|
||||
]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const dispatch = this.props.dispatch;
|
||||
if (nextProps.triggerName !== this.props.triggerName) {
|
||||
dispatch(setTrigger(nextProps.triggerName));
|
||||
dispatch(vSetDefaults(nextProps.triggerName));
|
||||
dispatch(vMakeRequest());
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
this.props.triggerName === null ||
|
||||
nextProps.triggerName === this.props.triggerName
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
this.shouldScrollBottom =
|
||||
window.innerHeight ===
|
||||
document.body.offsetHeight - document.body.scrollTop;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.shouldScrollBottom) {
|
||||
document.body.scrollTop = document.body.offsetHeight - window.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove state data beloging to this table
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(vSetDefaults(this.props.triggerName));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
triggerName,
|
||||
triggerList,
|
||||
query,
|
||||
curFilter,
|
||||
rows,
|
||||
count, // eslint-disable-line no-unused-vars
|
||||
activePath,
|
||||
migrationMode,
|
||||
ongoingRequest,
|
||||
isProgressing,
|
||||
lastError,
|
||||
lastSuccess,
|
||||
dispatch,
|
||||
expandedRow,
|
||||
} = this.props; // eslint-disable-line no-unused-vars
|
||||
|
||||
// check if table exists
|
||||
const currentTrigger = triggerList.find(s => s.name === triggerName);
|
||||
if (!currentTrigger) {
|
||||
// dispatch a 404 route
|
||||
dispatch(replace('/404'));
|
||||
}
|
||||
// Is this a view
|
||||
const isView = false;
|
||||
|
||||
// Are there any expanded columns
|
||||
const viewRows = (
|
||||
<ViewRows
|
||||
curTriggerName={triggerName}
|
||||
curQuery={query}
|
||||
curFilter={curFilter}
|
||||
curPath={[]}
|
||||
curRows={rows}
|
||||
isView={isView}
|
||||
parentTableName={null}
|
||||
activePath={activePath}
|
||||
ongoingRequest={ongoingRequest}
|
||||
isProgressing={isProgressing}
|
||||
lastError={lastError}
|
||||
lastSuccess={lastSuccess}
|
||||
triggerList={triggerList}
|
||||
curDepth={0}
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
expandedRow={expandedRow}
|
||||
/>
|
||||
);
|
||||
|
||||
// Choose the right nav bar header thing
|
||||
const header = (
|
||||
<TableHeader
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
triggerName={triggerName}
|
||||
tabName="running"
|
||||
migrationMode={migrationMode}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<div>{viewRows}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewTable.propTypes = {
|
||||
triggerName: PropTypes.string.isRequired,
|
||||
triggerList: PropTypes.array.isRequired,
|
||||
activePath: PropTypes.array.isRequired,
|
||||
query: PropTypes.object.isRequired,
|
||||
curFilter: PropTypes.object.isRequired,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
ongoingRequest: PropTypes.bool.isRequired,
|
||||
isProgressing: PropTypes.bool.isRequired,
|
||||
rows: PropTypes.array.isRequired,
|
||||
expandedRow: PropTypes.string.isRequired,
|
||||
count: PropTypes.number,
|
||||
lastError: PropTypes.object.isRequired,
|
||||
lastSuccess: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
triggerName: ownProps.params.trigger,
|
||||
triggerList: state.triggers.runningEvents,
|
||||
migrationMode: state.main.migrationMode,
|
||||
...state.triggers.view,
|
||||
};
|
||||
};
|
||||
|
||||
const runningEventsConnector = connect => connect(mapStateToProps)(ViewTable);
|
||||
|
||||
export default runningEventsConnector;
|
@ -0,0 +1,134 @@
|
||||
/* eslint-disable space-infix-ops */
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
autoTrackRelations,
|
||||
autoAddRelName,
|
||||
} from '../TableRelationships/Actions';
|
||||
import { getRelationshipLine } from '../TableRelationships/Relationships';
|
||||
import suggestedRelationshipsRaw from '../TableRelationships/autoRelations';
|
||||
|
||||
class AutoAddRelations extends Component {
|
||||
trackAllRelations = () => {
|
||||
this.props.dispatch(autoTrackRelations());
|
||||
};
|
||||
render() {
|
||||
const { schema, untrackedRelations, dispatch } = this.props;
|
||||
const styles = require('../PageContainer/PageContainer.scss');
|
||||
const handleAutoAddIndivRel = obj => {
|
||||
dispatch(autoAddRelName(obj));
|
||||
};
|
||||
|
||||
if (untrackedRelations.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.padd_bottom}
|
||||
key="no-untracked-rel"
|
||||
>
|
||||
There are no untracked relations
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const untrackedIndivHtml = [];
|
||||
schema.map(table => {
|
||||
const currentTable = table.table_name;
|
||||
const currentTableRel = suggestedRelationshipsRaw(currentTable, schema);
|
||||
currentTableRel.objectRel.map(obj => {
|
||||
untrackedIndivHtml.push(
|
||||
<div
|
||||
className={styles.padd_top_medium}
|
||||
key={'untrackedIndiv' + table.table_name}
|
||||
>
|
||||
<button
|
||||
className={`${styles.display_inline} btn btn-xs btn-default`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
handleAutoAddIndivRel(obj);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<div className={styles.display_inline + ' ' + styles.add_pad_left}>
|
||||
<b>{obj.tableName}</b> -{' '}
|
||||
{getRelationshipLine(
|
||||
obj.isObjRel,
|
||||
obj.lcol,
|
||||
obj.rcol,
|
||||
obj.rTable
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
currentTableRel.arrayRel.map(obj => {
|
||||
untrackedIndivHtml.push(
|
||||
<div
|
||||
className={styles.padd_top_medium}
|
||||
key={'untrackedIndiv' + table.table_name}
|
||||
>
|
||||
<button
|
||||
className={`${styles.display_inline} btn btn-xs btn-default`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
handleAutoAddIndivRel(obj);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<div className={styles.display_inline + ' ' + styles.add_pad_left}>
|
||||
<b>{obj.tableName}</b> -{' '}
|
||||
{getRelationshipLine(
|
||||
obj.isObjRel,
|
||||
obj.lcol,
|
||||
obj.rcol,
|
||||
obj.rTable
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{untrackedRelations.length === 0 ? (
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.padd_bottom}
|
||||
key="no-untracked-rel"
|
||||
>
|
||||
There are no untracked relations
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.padd_bottom}
|
||||
key="untracked-rel"
|
||||
>
|
||||
There are {untrackedRelations.length} untracked relations
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={this.trackAllRelations}
|
||||
className={
|
||||
styles.display_inline +
|
||||
' btn btn-xs btn-default ' +
|
||||
styles.add_mar_left
|
||||
}
|
||||
data-test="track-all-relationships"
|
||||
>
|
||||
Track All Relations
|
||||
</button>
|
||||
<div className={styles.padd_top_small}>{untrackedIndivHtml}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AutoAddRelations.propTypes = {
|
||||
untrackedRelations: PropTypes.array.isRequired,
|
||||
schema: PropTypes.array.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AutoAddRelations;
|
@ -0,0 +1,80 @@
|
||||
/* 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';
|
||||
|
||||
const appPrefix = globals.urlPrefix + '/events';
|
||||
|
||||
class Schema extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Initialize this table
|
||||
const dispatch = this.props.dispatch;
|
||||
dispatch(loadTriggers());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { migrationMode, dispatch } = this.props;
|
||||
|
||||
const styles = require('../PageContainer/PageContainer.scss');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.padd_left_remove} container-fluid ${
|
||||
styles.padd_top
|
||||
}`}
|
||||
>
|
||||
<div className={styles.padd_left}>
|
||||
<Helmet title="Event Triggers | Hasura" />
|
||||
<div>
|
||||
<h2 className={`${styles.heading_text} ${styles.inline_block}`}>
|
||||
{' '}
|
||||
Event Triggers{' '}
|
||||
</h2>
|
||||
{migrationMode ? (
|
||||
<button
|
||||
data-test="data-create-trigger"
|
||||
className={styles.yellow_button}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
dispatch(push(`${appPrefix}/manage/triggers/add`));
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Schema.propTypes = {
|
||||
schema: PropTypes.array.isRequired,
|
||||
untracked: PropTypes.array.isRequired,
|
||||
untrackedRelations: PropTypes.array.isRequired,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
currentSchema: PropTypes.string.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
schema: state.tables.allSchemas,
|
||||
schemaList: state.tables.schemaList,
|
||||
untracked: state.tables.untrackedSchemas,
|
||||
migrationMode: state.main.migrationMode,
|
||||
untrackedRelations: state.tables.untrackedRelations,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
});
|
||||
|
||||
const schemaConnector = connect => connect(mapStateToProps)(Schema);
|
||||
|
||||
export default schemaConnector;
|
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
// import PageContainer from '../PageContainer/PageContainer';
|
||||
|
||||
const SchemaContainer = ({ children }) => {
|
||||
const styles = require('./SchemaContainer.scss');
|
||||
return (
|
||||
<div className={styles.container + ' container-fluid'}>
|
||||
<Helmet title={'Schema | Data | Hasura'} />
|
||||
<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 schemaContainerConnector = connect =>
|
||||
connect(mapStateToProps)(SchemaContainer);
|
||||
|
||||
export default schemaContainerConnector;
|
@ -0,0 +1,63 @@
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/variables";
|
||||
@import "../../../Common/Common.scss";
|
||||
|
||||
.flexRow {
|
||||
display: flex;
|
||||
}
|
||||
.padd_left_remove
|
||||
{
|
||||
padding-left: 0;
|
||||
}
|
||||
.add_btn {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.account {
|
||||
padding: 20px 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
background: #444;
|
||||
color: $navbar-inverse-color;
|
||||
hr {
|
||||
margin: 0;
|
||||
border-color: $navbar-inverse-color;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-top: 10px;
|
||||
padding-left: 7px;
|
||||
li {
|
||||
padding: 7px 0;
|
||||
transition: color 0.5s;
|
||||
a,a:visited {
|
||||
color: $navbar-inverse-link-color;
|
||||
}
|
||||
a:hover {
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
li:hover {
|
||||
padding: 7px 0;
|
||||
color: $navbar-inverse-link-hover-color;
|
||||
transition: color 0.5s;
|
||||
pointer: cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0;
|
||||
padding-left: 15px;
|
||||
height: $mainContainerHeight;
|
||||
overflow: auto;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.rightBar {
|
||||
padding-left: 15px;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
|
||||
export const dataAPI = (
|
||||
<Tooltip id="tooltip-data-service">
|
||||
To use Data APIs for complex joins/queries, create a view and then add the
|
||||
view here.
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const untrackedTip = (
|
||||
<Tooltip id="tooltip-data-service">
|
||||
Tables or views that are not exposed over GraphQL
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const untrackedRelTip = (
|
||||
<Tooltip id="tooltip-data-rel-service">
|
||||
Foreign keys between tracked tables that are not relationships
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const quickDefaultPublic = (
|
||||
<Tooltip id="tooltip-permission-public">
|
||||
The selected role can perform select, insert, update and delete on all rows
|
||||
of the table.
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const quickDefaultReadOnly = (
|
||||
<Tooltip id="tooltip-permission-read">
|
||||
The selected role can perform select on all rows of the table.
|
||||
</Tooltip>
|
||||
);
|
@ -0,0 +1,110 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import { deleteTrigger } from '../EventActions';
|
||||
|
||||
class Settings extends Component {
|
||||
render() {
|
||||
const {
|
||||
triggerName,
|
||||
triggerList,
|
||||
migrationMode,
|
||||
count,
|
||||
dispatch,
|
||||
} = this.props;
|
||||
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
let triggerSchema = triggerList.filter(
|
||||
elem => elem.name === triggerName
|
||||
)[0];
|
||||
triggerSchema = triggerSchema ? triggerSchema : {};
|
||||
|
||||
const handleDeleteTrigger = () => {
|
||||
dispatch(deleteTrigger(triggerName));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container + ' container-fluid'}>
|
||||
<TableHeader
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
triggerName={triggerName}
|
||||
tabName="settings"
|
||||
migrationMode={migrationMode}
|
||||
/>
|
||||
<br />
|
||||
<div>
|
||||
<div className={styles.settingsSection}>
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Webhook URL</td>
|
||||
<td>{triggerSchema.webhook}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Table</td>
|
||||
<td>{triggerSchema.table_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Schema</td>
|
||||
<td>{triggerSchema.schema_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Event Type</td>
|
||||
<td>{triggerSchema.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number of Retries</td>
|
||||
<td>{triggerSchema.num_retries}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Retry Interval</td>
|
||||
<td>
|
||||
{triggerSchema.retry_interval}{' '}
|
||||
{triggerSchema.retry_interval > 1 ? 'seconds' : 'second'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operation / Columns</td>
|
||||
<td>{JSON.stringify(triggerSchema.definition, null, 4)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.add_mar_bottom}>
|
||||
<button
|
||||
onClick={handleDeleteTrigger}
|
||||
className={'btn btn-sm btn-danger'}
|
||||
data-test="delete-trigger"
|
||||
>
|
||||
Delete Trigger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
tableName: PropTypes.string.isRequired,
|
||||
triggerList: PropTypes.array,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
...state.triggers,
|
||||
triggerName: ownProps.params.trigger,
|
||||
migrationMode: state.main.migrationMode,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
};
|
||||
};
|
||||
|
||||
const settingsConnector = connect => connect(mapStateToProps)(Settings);
|
||||
|
||||
export default settingsConnector;
|
@ -0,0 +1,115 @@
|
||||
import { defaultLogState } from '../EventState';
|
||||
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import requestAction from 'utils/requestAction';
|
||||
import dataHeaders from '../Common/Headers';
|
||||
|
||||
/* ****************** View actions *************/
|
||||
const V_SET_DEFAULTS = 'StreamingLogs/V_SET_DEFAULTS';
|
||||
const V_REQUEST_SUCCESS = 'StreamingLogs/V_REQUEST_SUCCESS';
|
||||
const V_REQUEST_ERROR = 'StreamingLogs/V_REQUEST_ERROR';
|
||||
const V_REQUEST_PROGRESS = 'StreamingLogs/V_REQUEST_PROGRESS';
|
||||
|
||||
/* ****************** action creators *************/
|
||||
|
||||
const vSetDefaults = () => ({ type: V_SET_DEFAULTS });
|
||||
|
||||
const vMakeRequest = triggerName => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const url = Endpoints.query;
|
||||
const originalTrigger = getState().triggers.currentTrigger;
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: true });
|
||||
const currentQuery = JSON.parse(JSON.stringify(state.triggers.log.query));
|
||||
// count query
|
||||
const countQuery = JSON.parse(JSON.stringify(state.triggers.log.query));
|
||||
countQuery.columns = ['id'];
|
||||
|
||||
currentQuery.where = { event: { trigger_name: triggerName } };
|
||||
|
||||
// order_by for relationship
|
||||
currentQuery.order_by = ['-created_at'];
|
||||
|
||||
const requestBody = {
|
||||
type: 'bulk',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
args: {
|
||||
...currentQuery,
|
||||
table: {
|
||||
name: 'event_invocation_logs',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'count',
|
||||
args: {
|
||||
...countQuery,
|
||||
table: {
|
||||
name: 'event_invocation_logs',
|
||||
schema: 'hdb_catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: dataHeaders(getState),
|
||||
credentials: globalCookiePolicy,
|
||||
};
|
||||
return dispatch(requestAction(url, options)).then(
|
||||
data => {
|
||||
const currentTrigger = getState().triggers.currentTrigger;
|
||||
if (originalTrigger === currentTrigger) {
|
||||
Promise.all([
|
||||
dispatch({
|
||||
type: V_REQUEST_SUCCESS,
|
||||
data: data[0],
|
||||
count: data[1].count,
|
||||
}),
|
||||
dispatch({ type: V_REQUEST_PROGRESS, data: false }),
|
||||
]);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
dispatch({ type: V_REQUEST_ERROR, data: error });
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/* ****************** reducer ******************/
|
||||
const streamingLogsReducer = (triggerName, triggerList, logState, action) => {
|
||||
switch (action.type) {
|
||||
case V_SET_DEFAULTS:
|
||||
return {
|
||||
...defaultLogState,
|
||||
query: {
|
||||
columns: [
|
||||
'*',
|
||||
{
|
||||
name: 'event',
|
||||
columns: ['*'],
|
||||
},
|
||||
],
|
||||
limit: 20,
|
||||
where: { event: { trigger_name: triggerName } },
|
||||
},
|
||||
activePath: [triggerName],
|
||||
rows: [],
|
||||
count: null,
|
||||
};
|
||||
case V_REQUEST_SUCCESS:
|
||||
return { ...logState, rows: action.data, count: action.count };
|
||||
case V_REQUEST_PROGRESS:
|
||||
return { ...logState, isProgressing: action.data };
|
||||
default:
|
||||
return logState;
|
||||
}
|
||||
};
|
||||
|
||||
export default streamingLogsReducer;
|
||||
export { vSetDefaults, vMakeRequest };
|
@ -0,0 +1,214 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
import AceEditor from 'react-ace';
|
||||
import Tabs from 'react-bootstrap/lib/Tabs';
|
||||
import Tab from 'react-bootstrap/lib/Tab';
|
||||
import TableHeader from '../TableCommon/TableHeader';
|
||||
import { loadEventLogs } from '../EventActions';
|
||||
import { vMakeRequest, vSetDefaults } from './LogActions';
|
||||
|
||||
class StreamingLogs extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isWatching: false, intervalId: null };
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.dispatch(loadEventLogs(this.props.triggerName));
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.dispatch(vSetDefaults());
|
||||
}
|
||||
watchChanges() {
|
||||
// set state on watch
|
||||
this.setState({ isWatching: !this.state.isWatching });
|
||||
if (this.state.isWatching) {
|
||||
clearInterval(this.state.intervalId);
|
||||
} else {
|
||||
const intervalId = setInterval(this.refreshData, 2000);
|
||||
this.setState({ intervalId: intervalId });
|
||||
}
|
||||
}
|
||||
refreshData() {
|
||||
this.props.dispatch(vMakeRequest(this.props.triggerName));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { triggerName, migrationMode, log, count, dispatch } = this.props;
|
||||
|
||||
const styles = require('../TableCommon/Table.scss');
|
||||
const invocationColumns = [
|
||||
'status',
|
||||
'invocation_id',
|
||||
'event_id',
|
||||
'created_at',
|
||||
];
|
||||
const invocationGridHeadings = [];
|
||||
invocationColumns.map(column => {
|
||||
invocationGridHeadings.push({
|
||||
Header: column,
|
||||
accessor: column,
|
||||
});
|
||||
});
|
||||
const invocationRowsData = [];
|
||||
log.rows.map((r, rowIndex) => {
|
||||
const newRow = {};
|
||||
const status =
|
||||
r.status === 200 ? (
|
||||
<i className={styles.invocationSuccess + ' fa fa-check'} />
|
||||
) : (
|
||||
<i className={styles.invocationFailure + ' fa fa-times'} />
|
||||
);
|
||||
|
||||
// Insert cells corresponding to all rows
|
||||
invocationColumns.forEach(col => {
|
||||
const getCellContent = () => {
|
||||
const conditionalClassname = styles.tableCellCenterAligned;
|
||||
if (r[col] === null) {
|
||||
return (
|
||||
<div className={conditionalClassname}>
|
||||
<i>NULL</i>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (col === 'status') {
|
||||
return <div className={conditionalClassname}>{status}</div>;
|
||||
}
|
||||
if (col === 'invocation_id') {
|
||||
return <div className={conditionalClassname}>{r.id}</div>;
|
||||
}
|
||||
if (col === 'created_at') {
|
||||
const formattedDate = new Date(r.created_at).toUTCString();
|
||||
return formattedDate;
|
||||
}
|
||||
const content = r[col] === undefined ? 'NULL' : r[col].toString();
|
||||
return <div className={conditionalClassname}>{content}</div>;
|
||||
};
|
||||
newRow[col] = getCellContent();
|
||||
});
|
||||
invocationRowsData.push(newRow);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container + ' container-fluid'}>
|
||||
<TableHeader
|
||||
count={count}
|
||||
dispatch={dispatch}
|
||||
triggerName={triggerName}
|
||||
tabName="logs"
|
||||
migrationMode={migrationMode}
|
||||
/>
|
||||
<br />
|
||||
<div>
|
||||
<button
|
||||
onClick={this.watchChanges.bind(this)}
|
||||
className={' btn btn-default'}
|
||||
data-test="run-query"
|
||||
>
|
||||
{this.state.isWatching ? (
|
||||
<span>
|
||||
Streaming... <i className={'fa fa-spinner fa-spin'} />
|
||||
</span>
|
||||
) : (
|
||||
'Stream Logs'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{invocationRowsData.length ? (
|
||||
<div className={styles.streamingLogs + ' streamingLogs'}>
|
||||
<ReactTable
|
||||
data={invocationRowsData}
|
||||
columns={invocationGridHeadings}
|
||||
showPagination={false}
|
||||
pageSize={invocationRowsData.length}
|
||||
SubComponent={logRow => {
|
||||
const finalIndex = logRow.index;
|
||||
const finalRow = log.rows[finalIndex];
|
||||
const currentPayload = JSON.stringify(
|
||||
finalRow.event.payload,
|
||||
null,
|
||||
4
|
||||
);
|
||||
// check if response is type JSON
|
||||
let finalResponse = finalRow.response;
|
||||
try {
|
||||
finalResponse = JSON.parse(finalRow.response);
|
||||
finalResponse = JSON.stringify(finalResponse, null, 4);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Tabs
|
||||
animation={false}
|
||||
defaultActiveKey={1}
|
||||
id="requestResponseTab"
|
||||
>
|
||||
<Tab eventKey={1} title="Request">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>Request</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="payload"
|
||||
value={currentPayload}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey={2} title="Response">
|
||||
<div className={styles.add_mar_top}>
|
||||
<div className={styles.subheading_text}>Response</div>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name="response"
|
||||
value={finalResponse}
|
||||
minLines={4}
|
||||
maxLines={100}
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
showGutter={false}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.add_mar_top}>No data available</div>
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StreamingLogs.propTypes = {
|
||||
log: PropTypes.object,
|
||||
migrationMode: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
...state.triggers,
|
||||
triggerName: ownProps.params.trigger,
|
||||
migrationMode: state.main.migrationMode,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
};
|
||||
};
|
||||
|
||||
const streamingLogsConnector = connect =>
|
||||
connect(mapStateToProps)(StreamingLogs);
|
||||
|
||||
export default streamingLogsConnector;
|
@ -0,0 +1,139 @@
|
||||
.ReactTable .-pagination .-btn {
|
||||
height: 35px !important;
|
||||
width: 40%;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ReactTable .rt-thead {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background-color: #ddd;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ReactTable .rt-thead .rt-resizable-header-content {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ReactTable .rt-thead.-header {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ReactTable .-pagination {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ReactTable .rt-thead .rt-th,
|
||||
.ReactTable .rt-thead .rt-td {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.ReactTable .rt-table .rt-thead .rt-tr .rt-th {
|
||||
background-color: #f2f2f2 !important;
|
||||
color: #4d4d4d;
|
||||
font-weight: 600 !important;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.eventsTableBody .ReactTable .rt-table .rt-thead .rt-tr .rt-th:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.eventsTableBody
|
||||
.ReactTable
|
||||
.rt-table
|
||||
.rt-tbody
|
||||
.rt-tr-group
|
||||
.rt-tr.-odd
|
||||
.rt-td:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.eventsTableBody
|
||||
.ReactTable
|
||||
.rt-table
|
||||
.rt-tbody
|
||||
.rt-tr-group
|
||||
.rt-tr.-even
|
||||
.rt-td:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
.ReactTable .rt-table .rt-tbody .rt-tr-group .rt-tr.-odd:hover {
|
||||
background-color: #ebf7de;
|
||||
}
|
||||
|
||||
.ReactTable .rt-table .rt-tbody .rt-tr-group .rt-tr.-even:hover {
|
||||
background-color: #ebf7de;
|
||||
}
|
||||
|
||||
.ReactTable .rt-tbody {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.ReactTable .rt-thead [role='columnheader'] {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.invocationsSection .ReactTable .rt-tbody .rt-td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invocationsSection .ReactTable .rt-table .rt-thead .rt-tr .rt-th:nth-child(2) {
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
.invocationsSection .ReactTable .rt-table .rt-td:nth-child(2) {
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.invocationsSection .ReactTable {
|
||||
width: 70%;
|
||||
}
|
||||
.invocationsSection .ReactTable .rt-thead.-header {
|
||||
display: none;
|
||||
}
|
||||
.streamingLogs .ReactTable .rt-thead.-header {
|
||||
// display: none;
|
||||
}
|
||||
.streamingLogs .ReactTable .rt-table .rt-thead .rt-tr .rt-th:nth-child(2) {
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
.streamingLogs .ReactTable .rt-table .rt-td:nth-child(2) {
|
||||
width: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
.streamingLogs .ReactTable .rt-table .rt-thead .rt-tr .rt-th:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
}
|
||||
|
||||
.streamingLogs
|
||||
.ReactTable
|
||||
.rt-table
|
||||
.rt-tbody
|
||||
.rt-tr-group
|
||||
.rt-tr.-odd
|
||||
.rt-td:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
}
|
||||
|
||||
.streamingLogs
|
||||
.ReactTable
|
||||
.rt-table
|
||||
.rt-tbody
|
||||
.rt-tr-group
|
||||
.rt-tr.-even
|
||||
.rt-td:first-child {
|
||||
flex: 24 0 auto !important;
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
}
|
||||
|
||||
#requestResponseTab ul li a {
|
||||
color: black;
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
@import "../../../Common/Common.scss";
|
||||
.container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
overflow: auto;
|
||||
//margin-top: 25px;
|
||||
}
|
||||
|
||||
.schemaWrapper {
|
||||
background: #FFF3D5;
|
||||
width: 100%;
|
||||
.schemaSidebarSection {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.aceBlock {
|
||||
max-height: 300px;
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.aceBlockExpand {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: 5px;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.viewRowsContainer {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cellExpand {
|
||||
transform: rotate(-45deg);
|
||||
cursor: pointer;
|
||||
top: -5px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.cellCollapse {
|
||||
cursor: pointer;
|
||||
top: -5px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.insertContainer {
|
||||
button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
label.radioLabel {
|
||||
padding-top: 0;
|
||||
input[type="radio"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
span.radioSpan {
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: auto;
|
||||
thead {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
th {
|
||||
min-width: 100px;
|
||||
font-weight: 300;
|
||||
}
|
||||
td {
|
||||
max-width: 300px;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.expandable {
|
||||
color: #779ecb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.expanded {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.expandable:hover {
|
||||
background: #eee;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableNameInput {
|
||||
width: 300px;
|
||||
}
|
||||
.addCol {
|
||||
.input {
|
||||
width: 300px;
|
||||
display: inline-block;
|
||||
}
|
||||
.inputCheckbox {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
padding-left: 20px;
|
||||
margin: 0px 20px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.inputDefault
|
||||
{
|
||||
width: 150px;
|
||||
margin: 0px 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
.remove_ul_left
|
||||
{
|
||||
|
||||
}
|
||||
.select {
|
||||
display: inline-block;
|
||||
width: 300px;
|
||||
height: 34px;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #B85C27;
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.insertBox {
|
||||
min-width: 270px;
|
||||
}
|
||||
|
||||
.insertBoxLabel {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sqlBody {
|
||||
}
|
||||
|
||||
.tablesBody {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.addTablesBody {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.dataBreadCrumb {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.tableCellCenterAligned {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tableCellCenterAlignedExpanded {
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.selectTrigger {
|
||||
width: 300px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
table {
|
||||
width: 80%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.advancedOperations {
|
||||
hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.retryLabel {
|
||||
display: block;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.retrySection {
|
||||
width: 300px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.invocationsSection {
|
||||
margin-top: 20px;
|
||||
li {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
margin-right: -10px;
|
||||
margin-left: -10px;
|
||||
line-height: 23px;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
.invocationSuccess {
|
||||
color: #28a745
|
||||
}
|
||||
.invocationFailure {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.streamingLogs {
|
||||
margin-top: 20px;
|
||||
li {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
margin-right: -10px;
|
||||
margin-left: -10px;
|
||||
line-height: 23px;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
.invocationSuccess {
|
||||
color: #28a745;
|
||||
}
|
||||
.invocationFailure {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.selectOperations {
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.advancedToggleBtn {
|
||||
width: 300px;
|
||||
background: #f2f2f2 !important;
|
||||
i {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
const TableHeader = ({ triggerName, tabName, count }) => {
|
||||
const styles = require('./Table.scss');
|
||||
let capitalised = tabName;
|
||||
capitalised = capitalised[0].toUpperCase() + capitalised.slice(1);
|
||||
let showCount = '';
|
||||
if (!(count === null || count === undefined)) {
|
||||
showCount = '(' + count + ')';
|
||||
}
|
||||
let activeTab;
|
||||
if (tabName === 'processed') {
|
||||
activeTab = 'Processed';
|
||||
} else if (tabName === 'pending') {
|
||||
activeTab = 'Pending';
|
||||
} else if (tabName === 'settings') {
|
||||
activeTab = 'Settings';
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Helmet
|
||||
title={capitalised + ' - ' + triggerName + ' - Event Triggers | Hasura'}
|
||||
/>
|
||||
<div className={styles.subHeader}>
|
||||
<div className={styles.dataBreadCrumb}>
|
||||
You are here: <Link to={'/events'}>Events</Link>{' '}
|
||||
<i className="fa fa-angle-right" aria-hidden="true" />{' '}
|
||||
<Link to={'/events/manage'}>Manage</Link>{' '}
|
||||
<i className="fa fa-angle-right" aria-hidden="true" />{' '}
|
||||
<Link to={'/events/manage/triggers'}>Triggers</Link>{' '}
|
||||
<i className="fa fa-angle-right" aria-hidden="true" />{' '}
|
||||
<Link to={'/events/manage/triggers/' + triggerName + '/processed'}>
|
||||
{triggerName}
|
||||
</Link>{' '}
|
||||
<i className="fa fa-angle-right" aria-hidden="true" /> {activeTab}
|
||||
</div>
|
||||
<h2 className={styles.heading_text}>{triggerName}</h2>
|
||||
<div className={styles.nav}>
|
||||
<ul className="nav nav-pills">
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === 'processed' ? styles.active : ''}
|
||||
>
|
||||
<Link
|
||||
to={'/events/manage/triggers/' + triggerName + '/processed'}
|
||||
data-test="trigger-processed-events"
|
||||
>
|
||||
Processed {tabName === 'processed' ? showCount : null}
|
||||
</Link>
|
||||
</li>
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === 'pending' ? styles.active : ''}
|
||||
>
|
||||
<Link
|
||||
to={'/events/manage/triggers/' + triggerName + '/pending'}
|
||||
data-test="trigger-pending-events"
|
||||
>
|
||||
Pending {tabName === 'pending' ? showCount : null}
|
||||
</Link>
|
||||
</li>
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === 'running' ? styles.active : ''}
|
||||
>
|
||||
<Link
|
||||
to={'/events/manage/triggers/' + triggerName + '/running'}
|
||||
data-test="trigger-running-events"
|
||||
>
|
||||
Running {tabName === 'running' ? showCount : null}
|
||||
</Link>
|
||||
</li>
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === 'logs' ? styles.active : 'hide'}
|
||||
data-test="trigger-logs"
|
||||
>
|
||||
<Link to={'/events/manage/triggers/' + triggerName + '/logs'}>
|
||||
Streaming Logs
|
||||
</Link>
|
||||
</li>
|
||||
<li
|
||||
role="presentation"
|
||||
className={tabName === 'settings' ? styles.active : ''}
|
||||
data-test="trigger-settings"
|
||||
>
|
||||
<Link to={'/events/manage/triggers/' + triggerName + '/settings'}>
|
||||
Settings
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="clearfix" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default TableHeader;
|
@ -0,0 +1,126 @@
|
||||
.container {
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
background: #eee;
|
||||
h2 {
|
||||
margin: 0;
|
||||
padding: 26px;
|
||||
float: left;
|
||||
line-height: 26px;
|
||||
}
|
||||
.nav {
|
||||
padding: 20px;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
overflow: auto;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.viewRowsContainer {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.insertContainer {
|
||||
button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
label.radioLabel {
|
||||
padding-top: 0;
|
||||
input[type="radio"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
span.radioSpan {
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
width: -webkit-fill-available;
|
||||
// width: auto;
|
||||
thead {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
th {
|
||||
min-width: 100px;
|
||||
font-weight: 300;
|
||||
}
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
td {
|
||||
width: 300px;
|
||||
max-width: 300px;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.expandable {
|
||||
color: #779ecb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.expanded {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.expandable:hover {
|
||||
background: #eee;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableNameInput {
|
||||
width: 300px;
|
||||
}
|
||||
.addCol {
|
||||
.input {
|
||||
width: 250px;
|
||||
display: inline-block;
|
||||
}
|
||||
.select {
|
||||
margin: 0 20px;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
color: #B85C27;
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.roleTag {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.resetPassForm {
|
||||
margin-top: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 1px #eee;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.relationshipTable {
|
||||
width: auto;
|
||||
thead {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
}
|
||||
th {
|
||||
min-width: 100px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.relationshipTopPadding {
|
||||
padding: 10px 0;
|
||||
}
|
5
console/src/components/Services/EventTrigger/Types.js
Normal file
5
console/src/components/Services/EventTrigger/Types.js
Normal file
@ -0,0 +1,5 @@
|
||||
const Integers = ['serial', 'integer', 'bigserial', 'smallint', 'bigint'];
|
||||
const Reals = ['float4', 'float8', 'numeric'];
|
||||
const Numerics = [...Integers, ...Reals];
|
||||
|
||||
export { Numerics, Integers, Reals };
|
14
console/src/components/Services/EventTrigger/index.js
Normal file
14
console/src/components/Services/EventTrigger/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
export eventHeaderConnector from './EventHeader';
|
||||
export eventRouter from './EventRouter';
|
||||
|
||||
export eventReducer from './EventReducer';
|
||||
export addTriggerConnector from './Add/AddTrigger';
|
||||
|
||||
export processedEventsConnector from './ProcessedEvents/ViewTable';
|
||||
export pendingEventsConnector from './PendingEvents/ViewTable';
|
||||
export runningEventsConnector from './RunningEvents/ViewTable';
|
||||
export settingsConnector from './Settings/Settings';
|
||||
export streamingLogsConnector from './StreamingLogs/Logs';
|
||||
export schemaConnector from './Schema/Schema';
|
||||
export schemaContainerConnector from './Schema/SchemaContainer';
|
||||
export migrationsConnector from './Migrations/MigrationsHome';
|
11
console/src/components/Services/EventTrigger/push.js
Normal file
11
console/src/components/Services/EventTrigger/push.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import globals from '../../../Globals';
|
||||
|
||||
const urlPrefix = globals.urlPrefix;
|
||||
const appPrefix = urlPrefix !== '/' ? urlPrefix + '/events' : '/events';
|
||||
|
||||
const _push = path => push(appPrefix + path);
|
||||
|
||||
export default _push;
|
||||
export { appPrefix };
|
132
console/src/components/Services/EventTrigger/utils.js
Normal file
132
console/src/components/Services/EventTrigger/utils.js
Normal file
@ -0,0 +1,132 @@
|
||||
const ordinalColSort = (a, b) => {
|
||||
if (a.ordinal_position < b.ordinal_position) {
|
||||
return -1;
|
||||
}
|
||||
if (a.ordinal_position > b.ordinal_position) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const findFKConstraint = (curTable, column) => {
|
||||
const fkConstraints = curTable.foreign_key_constraints;
|
||||
|
||||
return fkConstraints.find(
|
||||
fk =>
|
||||
Object.keys(fk.column_mapping).length === 1 &&
|
||||
Object.keys(fk.column_mapping)[0] === column
|
||||
);
|
||||
};
|
||||
|
||||
const findTableFromRel = (schemas, curTable, rel) => {
|
||||
let rtable = null;
|
||||
|
||||
// for view
|
||||
if (rel.rel_def.manual_configuration !== undefined) {
|
||||
rtable = rel.rel_def.manual_configuration.remote_table;
|
||||
if (rtable.schema) {
|
||||
rtable = rtable.name;
|
||||
}
|
||||
}
|
||||
|
||||
// for table
|
||||
if (rel.rel_def.foreign_key_constraint_on !== undefined) {
|
||||
// for object relationship
|
||||
if (rel.rel_type === 'object') {
|
||||
const column = rel.rel_def.foreign_key_constraint_on;
|
||||
const fkc = findFKConstraint(curTable, column);
|
||||
if (fkc) {
|
||||
rtable = fkc.ref_table;
|
||||
}
|
||||
}
|
||||
|
||||
// for array relationship
|
||||
if (rel.rel_type === 'array') {
|
||||
rtable = rel.rel_def.foreign_key_constraint_on.table;
|
||||
if (rtable.schema) {
|
||||
rtable = rtable.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schemas.find(x => x.table_name === rtable);
|
||||
};
|
||||
|
||||
const findAllFromRel = (schemas, curTable, rel) => {
|
||||
let rtable = null;
|
||||
let lcol;
|
||||
let rcol;
|
||||
|
||||
const foreignKeyConstraintOn = rel.rel_def.foreign_key_constraint_on;
|
||||
|
||||
// for view
|
||||
if (rel.rel_def.manual_configuration !== undefined) {
|
||||
rtable = rel.rel_def.manual_configuration.remote_table;
|
||||
|
||||
if (rtable.schema) {
|
||||
rtable = rtable.name;
|
||||
}
|
||||
const columnMapping = rel.rel_def.manual_configuration.column_mapping;
|
||||
lcol = Object.keys(columnMapping)[0];
|
||||
rcol = columnMapping[lcol];
|
||||
}
|
||||
|
||||
// for table
|
||||
if (foreignKeyConstraintOn !== undefined) {
|
||||
// for object relationship
|
||||
if (rel.rel_type === 'object') {
|
||||
lcol = foreignKeyConstraintOn;
|
||||
|
||||
const fkc = findFKConstraint(curTable, lcol);
|
||||
if (fkc) {
|
||||
rtable = fkc.ref_table;
|
||||
rcol = fkc.column_mapping[lcol];
|
||||
}
|
||||
}
|
||||
|
||||
// for array relationship
|
||||
if (rel.rel_type === 'array') {
|
||||
rtable = foreignKeyConstraintOn.table;
|
||||
rcol = foreignKeyConstraintOn.column;
|
||||
if (rtable.schema) {
|
||||
// if schema exists, its not public schema
|
||||
rtable = rtable.name;
|
||||
}
|
||||
|
||||
const rtableSchema = schemas.find(x => x.table_name === rtable);
|
||||
const rfkc = findFKConstraint(rtableSchema, rcol);
|
||||
lcol = rfkc.column_mapping[rcol];
|
||||
}
|
||||
}
|
||||
|
||||
return { lcol, rtable, rcol };
|
||||
};
|
||||
|
||||
const getIngForm = string => {
|
||||
return (
|
||||
(string[string.length - 1] === 'e'
|
||||
? string.slice(0, string.length - 1)
|
||||
: string) + 'ing'
|
||||
);
|
||||
};
|
||||
|
||||
const getEdForm = string => {
|
||||
return (
|
||||
(string[string.length - 1] === 'e'
|
||||
? string.slice(0, string.length - 1)
|
||||
: string) + 'ed'
|
||||
);
|
||||
};
|
||||
|
||||
const escapeRegExp = string => {
|
||||
return string.replace(/([.*+?^${}()|[\]\\])/g, '\\$1');
|
||||
};
|
||||
|
||||
export {
|
||||
ordinalColSort,
|
||||
findTableFromRel,
|
||||
findAllFromRel,
|
||||
getEdForm,
|
||||
getIngForm,
|
||||
escapeRegExp,
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
import { dataReducer } from './components/Services/Data';
|
||||
import { eventReducer } from './components/Services/EventTrigger';
|
||||
import mainReducer from './components/Main/Actions';
|
||||
import apiExplorerReducer from 'components/ApiExplorer/Actions';
|
||||
import progressBarReducer from 'components/App/Actions';
|
||||
@ -9,6 +10,7 @@ import { reducer as notifications } from 'react-notification-system-redux';
|
||||
|
||||
const reducer = combineReducers({
|
||||
...dataReducer,
|
||||
...eventReducer,
|
||||
progressBar: progressBarReducer,
|
||||
apiexplorer: apiExplorerReducer,
|
||||
main: mainReducer,
|
||||
|
@ -7,6 +7,8 @@ import { App, Main, PageNotFound } from 'components';
|
||||
|
||||
import { dataRouter } from './components/Services/Data';
|
||||
|
||||
import { eventRouter } from './components/Services/EventTrigger';
|
||||
|
||||
import { loadMigrationStatus } from './components/Main/Actions';
|
||||
|
||||
import { composeOnEnterHooks } from 'utils/router';
|
||||
@ -39,8 +41,10 @@ const routes = store => {
|
||||
|
||||
// loads schema
|
||||
const dataRouterUtils = dataRouter(connect, store, composeOnEnterHooks);
|
||||
const eventRouterUtils = eventRouter(connect, store, composeOnEnterHooks);
|
||||
const requireSchema = dataRouterUtils.requireSchema;
|
||||
const makeDataRouter = dataRouterUtils.makeDataRouter;
|
||||
const makeEventRouter = eventRouterUtils.makeEventRouter;
|
||||
|
||||
return (
|
||||
<Route path="/" component={App}>
|
||||
@ -57,6 +61,7 @@ const routes = store => {
|
||||
component={generatedApiExplorer(connect)}
|
||||
/>
|
||||
{makeDataRouter}
|
||||
{makeEventRouter}
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="404" component={PageNotFound} status="404" />
|
||||
|
@ -96,6 +96,7 @@ library
|
||||
, http-client
|
||||
, http-client-tls
|
||||
, connection
|
||||
, retry
|
||||
|
||||
-- ordered map
|
||||
, insert-ordered-containers
|
||||
@ -105,6 +106,8 @@ library
|
||||
|
||||
-- Templating
|
||||
, mustache
|
||||
, ginger
|
||||
, file-embed
|
||||
|
||||
--
|
||||
, data-has
|
||||
@ -145,6 +148,7 @@ library
|
||||
, Hasura.RQL.Types.Permission
|
||||
, Hasura.RQL.Types.Error
|
||||
, Hasura.RQL.Types.DML
|
||||
, Hasura.RQL.Types.Subscribe
|
||||
, Hasura.RQL.DDL.Deps
|
||||
, Hasura.RQL.DDL.Permission.Internal
|
||||
, Hasura.RQL.DDL.Permission.Triggers
|
||||
@ -155,6 +159,7 @@ library
|
||||
, Hasura.RQL.DDL.Schema.Diff
|
||||
, Hasura.RQL.DDL.Metadata
|
||||
, Hasura.RQL.DDL.Utils
|
||||
, Hasura.RQL.DDL.Subscribe
|
||||
, Hasura.RQL.DML.Delete
|
||||
, Hasura.RQL.DML.Explain
|
||||
, Hasura.RQL.DML.Internal
|
||||
@ -187,6 +192,9 @@ library
|
||||
, Hasura.GraphQL.Resolve.Mutation
|
||||
, Hasura.GraphQL.Resolve.Select
|
||||
|
||||
, Hasura.Events.Lib
|
||||
, Hasura.Events.HTTP
|
||||
|
||||
, Data.Text.Extended
|
||||
, Data.Sequence.NonEmpty
|
||||
, Data.TByteString
|
||||
@ -227,6 +235,9 @@ executable graphql-engine
|
||||
, pg-client
|
||||
, http-client
|
||||
, http-client-tls
|
||||
, stm
|
||||
, wreq
|
||||
, connection
|
||||
|
||||
other-modules: Ops
|
||||
TH
|
||||
@ -262,4 +273,4 @@ test-suite graphql-engine-test
|
||||
, unordered-containers >= 0.2
|
||||
, case-insensitive
|
||||
|
||||
other-modules: Spec
|
||||
other-modules: Spec
|
||||
|
@ -5,6 +5,7 @@ module Main where
|
||||
|
||||
import Ops
|
||||
|
||||
import Control.Monad.STM (atomically)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Options.Applicative
|
||||
import System.Environment (lookupEnv)
|
||||
@ -22,6 +23,7 @@ import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Client.TLS as HTTP
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
|
||||
import Hasura.Events.Lib
|
||||
import Hasura.Logging (defaultLoggerSettings, mkLoggerCtx)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.DDL.Metadata (fetchMetadata)
|
||||
@ -32,6 +34,8 @@ import Hasura.Server.CheckUpdates (checkForUpdates)
|
||||
import Hasura.Server.Init
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Network.HTTP.Client.TLS as TLS
|
||||
import qualified Network.Wreq.Session as WrqS
|
||||
|
||||
data RavenOptions
|
||||
= RavenOptions
|
||||
@ -126,7 +130,8 @@ main = do
|
||||
ci <- either ((>> exitFailure) . putStrLn . connInfoErrModifier)
|
||||
return $ mkConnInfo mEnvDbUrl rci
|
||||
printConnInfo ci
|
||||
loggerCtx <- mkLoggerCtx defaultLoggerSettings
|
||||
loggerCtx <- mkLoggerCtx $ defaultLoggerSettings True
|
||||
hloggerCtx <- mkLoggerCtx $ defaultLoggerSettings False
|
||||
httpManager <- HTTP.newManager HTTP.tlsManagerSettings
|
||||
case ravenMode of
|
||||
ROServe (ServeOptions port cp isoL mRootDir mAccessKey corsCfg mWebHook mJwtSecret enableConsole) -> do
|
||||
@ -140,15 +145,24 @@ main = do
|
||||
CorsConfigG finalCorsDomain $ ccDisabled corsCfg
|
||||
initialise ci
|
||||
migrate ci
|
||||
prepareEvents ci
|
||||
pool <- Q.initPGPool ci cp
|
||||
putStrLn $ "server: running on port " ++ show port
|
||||
app <- mkWaiApp isoL mRootDir loggerCtx pool httpManager am finalCorsCfg enableConsole
|
||||
(app, cacheRef) <- mkWaiApp isoL mRootDir loggerCtx pool httpManager am finalCorsCfg enableConsole
|
||||
let warpSettings = Warp.setPort port Warp.defaultSettings
|
||||
-- Warp.setHost "*" Warp.defaultSettings
|
||||
|
||||
-- start a background thread to check for updates
|
||||
void $ C.forkIO $ checkForUpdates loggerCtx httpManager
|
||||
|
||||
maxEvThrds <- getFromEnv defaultMaxEventThreads "HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE"
|
||||
evPollSec <- getFromEnv defaultPollingIntervalSec "HASURA_GRAPHQL_EVENTS_FETCH_INTERVAL"
|
||||
|
||||
eventEngineCtx <- atomically $ initEventEngineCtx maxEvThrds evPollSec
|
||||
httpSession <- WrqS.newSessionControl Nothing TLS.tlsManagerSettings
|
||||
|
||||
void $ C.forkIO $ processEventQueue hloggerCtx httpSession pool cacheRef eventEngineCtx
|
||||
|
||||
Warp.runSettings warpSettings app
|
||||
|
||||
ROExport -> do
|
||||
@ -176,6 +190,18 @@ main = do
|
||||
currentTime <- getCurrentTime
|
||||
res <- runTx ci $ migrateCatalog currentTime
|
||||
either ((>> exitFailure) . printJSON) putStrLn res
|
||||
prepareEvents ci = do
|
||||
putStrLn "event_triggers: preparing data"
|
||||
res <- runTx ci unlockAllEvents
|
||||
either ((>> exitFailure) . printJSON) return res
|
||||
getFromEnv :: (Read a) => a -> String -> IO a
|
||||
getFromEnv defaults env = do
|
||||
mEnv <- lookupEnv env
|
||||
let mRes = case mEnv of
|
||||
Nothing -> Just defaults
|
||||
Just val -> readMaybe val
|
||||
eRes = maybe (Left "HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE is not an integer") Right mRes
|
||||
either ((>> exitFailure) . putStrLn) return eRes
|
||||
|
||||
cleanSuccess = putStrLn "successfully cleaned graphql-engine related data"
|
||||
|
||||
|
334
server/src-lib/Hasura/Events/HTTP.hs
Normal file
334
server/src-lib/Hasura/Events/HTTP.hs
Normal file
@ -0,0 +1,334 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Hasura.Events.HTTP
|
||||
( HTTP(..)
|
||||
, mkHTTP
|
||||
, mkAnyHTTPPost
|
||||
, mkHTTPMaybe
|
||||
, HTTPErr(..)
|
||||
, runHTTP
|
||||
, default2xxParser
|
||||
, noBody2xxParser
|
||||
, defaultRetryPolicy
|
||||
, defaultRetryFn
|
||||
, defaultParser
|
||||
, defaultParserMaybe
|
||||
, isNetworkError
|
||||
, isNetworkErrorHC
|
||||
, HLogger
|
||||
, mkHLogger
|
||||
, ExtraContext(..)
|
||||
) where
|
||||
|
||||
import qualified Control.Retry as R
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Casing as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.ByteString.Lazy as B
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.TByteString as TBS
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Data.Text.Encoding.Error as TE
|
||||
import qualified Data.Text.Lazy as TL
|
||||
import qualified Data.Text.Lazy.Encoding as TLE
|
||||
import qualified Data.Time.Clock as Time
|
||||
import qualified Network.HTTP.Client as H
|
||||
import qualified Network.HTTP.Types as N
|
||||
import qualified Network.Wreq as W
|
||||
import qualified Network.Wreq.Session as WS
|
||||
import qualified System.Log.FastLogger as FL
|
||||
|
||||
import Control.Exception (try)
|
||||
import Control.Lens
|
||||
import Control.Monad.Except (MonadError, throwError)
|
||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||
import Control.Monad.Reader (MonadReader)
|
||||
import Data.Has
|
||||
import Hasura.Logging
|
||||
-- import Data.Monoid
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Subscribe
|
||||
|
||||
-- import Context (HTTPSessionMgr (..))
|
||||
-- import Log
|
||||
|
||||
type HLogger = (LogLevel, EngineLogType, J.Value) -> IO ()
|
||||
|
||||
data ExtraContext
|
||||
= ExtraContext
|
||||
{ elEventCreatedAt :: Time.UTCTime
|
||||
, elEventId :: TriggerId
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(J.deriveJSON (J.aesonDrop 2 J.snakeCase){J.omitNothingFields=True} ''ExtraContext)
|
||||
|
||||
data HTTPErr
|
||||
= HClient !H.HttpException
|
||||
| HParse !N.Status !String
|
||||
| HStatus !N.Status TBS.TByteString
|
||||
| HOther !String
|
||||
deriving (Show)
|
||||
|
||||
instance J.ToJSON HTTPErr where
|
||||
toJSON err = toObj $ case err of
|
||||
(HClient e) -> ("client", J.toJSON $ show e)
|
||||
(HParse st e) ->
|
||||
( "parse"
|
||||
, J.toJSON (N.statusCode st, show e)
|
||||
)
|
||||
(HStatus st resp) ->
|
||||
("status", J.toJSON (N.statusCode st, resp))
|
||||
(HOther e) -> ("internal", J.toJSON $ show e)
|
||||
where
|
||||
toObj :: (T.Text, J.Value) -> J.Value
|
||||
toObj (k, v) = J.object [ "type" J..= k
|
||||
, "detail" J..= v]
|
||||
|
||||
-- encapsulates a http operation
|
||||
instance ToEngineLog HTTPErr where
|
||||
toEngineLog err = (LevelError, "event-trigger", J.toJSON err )
|
||||
|
||||
data HTTP a
|
||||
= HTTP
|
||||
{ _hMethod :: !String
|
||||
, _hUrl :: !String
|
||||
, _hPayload :: !(Maybe J.Value)
|
||||
, _hFormData :: !(Maybe [W.FormParam])
|
||||
-- options modifier
|
||||
, _hOptions :: W.Options -> W.Options
|
||||
-- the response parser
|
||||
, _hParser :: W.Response B.ByteString -> Either HTTPErr a
|
||||
-- should the operation be retried
|
||||
, _hRetryFn :: Either HTTPErr a -> Bool
|
||||
-- the retry policy
|
||||
, _hRetryPolicy :: R.RetryPolicyM IO
|
||||
}
|
||||
|
||||
-- TODO. Why this istance?
|
||||
-- instance Show (HTTP a) where
|
||||
-- show (HTTP m u p _ _ _ _) = show m ++ " " ++ show u ++ " : " ++ show p
|
||||
|
||||
isNetworkError :: HTTPErr -> Bool
|
||||
isNetworkError = \case
|
||||
HClient he -> isNetworkErrorHC he
|
||||
_ -> False
|
||||
|
||||
isNetworkErrorHC :: H.HttpException -> Bool
|
||||
isNetworkErrorHC = \case
|
||||
H.HttpExceptionRequest _ (H.ConnectionFailure _) -> True
|
||||
H.HttpExceptionRequest _ H.ConnectionTimeout -> True
|
||||
H.HttpExceptionRequest _ H.ResponseTimeout -> True
|
||||
_ -> False
|
||||
|
||||
-- retries on the typical network errors
|
||||
defaultRetryFn :: Either HTTPErr a -> Bool
|
||||
defaultRetryFn = \case
|
||||
Left e -> isNetworkError e
|
||||
Right _ -> False
|
||||
|
||||
-- full jitter backoff
|
||||
defaultRetryPolicy :: (MonadIO m) => R.RetryPolicyM m
|
||||
defaultRetryPolicy =
|
||||
R.capDelay (120 * 1000 * 1000) (R.fullJitterBackoff (2 * 1000 * 1000))
|
||||
<> R.limitRetries 15
|
||||
|
||||
-- a helper function
|
||||
respJson :: (J.FromJSON a) => W.Response B.ByteString -> Either HTTPErr a
|
||||
respJson resp =
|
||||
either (Left . HParse respCode) return $
|
||||
J.eitherDecode respBody
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
respBody = resp ^. W.responseBody
|
||||
|
||||
defaultParser :: (J.FromJSON a) => W.Response B.ByteString -> Either HTTPErr a
|
||||
defaultParser resp = if
|
||||
| respCode == N.status200 -> respJson resp
|
||||
| otherwise -> do
|
||||
let val = TBS.fromLBS $ resp ^. W.responseBody
|
||||
throwError $ HStatus respCode val
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
|
||||
-- like default parser but turns 404 into maybe
|
||||
defaultParserMaybe
|
||||
:: (J.FromJSON a) => W.Response B.ByteString -> Either HTTPErr (Maybe a)
|
||||
defaultParserMaybe resp = if
|
||||
| respCode == N.status200 -> Just <$> respJson resp
|
||||
| respCode == N.status404 -> return Nothing
|
||||
| otherwise -> do
|
||||
let val = TBS.fromLBS $ resp ^. W.responseBody
|
||||
throwError $ HStatus respCode val
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
|
||||
-- default parser which allows all 2xx responses
|
||||
default2xxParser :: (J.FromJSON a) => W.Response B.ByteString -> Either HTTPErr a
|
||||
default2xxParser resp = if
|
||||
| respCode >= N.status200 && respCode < N.status300 -> respJson resp
|
||||
| otherwise -> do
|
||||
let val = TBS.fromLBS $ resp ^. W.responseBody
|
||||
throwError $ HStatus respCode val
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
|
||||
noBody2xxParser :: W.Response B.ByteString -> Either HTTPErr ()
|
||||
noBody2xxParser resp = if
|
||||
| respCode >= N.status200 && respCode < N.status300 -> return ()
|
||||
| otherwise -> do
|
||||
let val = TBS.fromLBS $ resp ^. W.responseBody
|
||||
throwError $ HStatus respCode val
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
|
||||
anyBodyParser :: W.Response B.ByteString -> Either HTTPErr B.ByteString
|
||||
anyBodyParser resp = if
|
||||
| respCode >= N.status200 && respCode < N.status300 -> return $ resp ^. W.responseBody
|
||||
| otherwise -> do
|
||||
let val = TBS.fromLBS $ resp ^. W.responseBody
|
||||
throwError $ HStatus respCode val
|
||||
where
|
||||
respCode = resp ^. W.responseStatus
|
||||
|
||||
mkHTTP :: (J.FromJSON a) => String -> String -> HTTP a
|
||||
mkHTTP method url =
|
||||
HTTP method url Nothing Nothing id defaultParser
|
||||
defaultRetryFn defaultRetryPolicy
|
||||
|
||||
mkAnyHTTPPost :: String -> Maybe J.Value -> HTTP B.ByteString
|
||||
mkAnyHTTPPost url payload =
|
||||
HTTP "POST" url payload Nothing id anyBodyParser
|
||||
defaultRetryFn defaultRetryPolicy
|
||||
|
||||
mkHTTPMaybe :: (J.FromJSON a) => String -> String -> HTTP (Maybe a)
|
||||
mkHTTPMaybe method url =
|
||||
HTTP method url Nothing Nothing id defaultParserMaybe
|
||||
defaultRetryFn defaultRetryPolicy
|
||||
|
||||
-- internal logging related types
|
||||
data HTTPReq
|
||||
= HTTPReq
|
||||
{ _hrqMethod :: !String
|
||||
, _hrqUrl :: !String
|
||||
, _hrqPayload :: !(Maybe J.Value)
|
||||
, _hrqTry :: !Int
|
||||
, _hrqDelay :: !(Maybe Int)
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase){J.omitNothingFields=True} ''HTTPReq)
|
||||
|
||||
instance ToEngineLog HTTPReq where
|
||||
toEngineLog req = (LevelInfo, "event-trigger", J.toJSON req )
|
||||
|
||||
instance ToEngineLog HTTPResp where
|
||||
toEngineLog resp = (LevelInfo, "event-trigger", J.toJSON resp )
|
||||
|
||||
data HTTPResp
|
||||
= HTTPResp
|
||||
{ _hrsStatus :: !Int
|
||||
, _hrsHeaders :: ![T.Text]
|
||||
, _hrsBody :: !TL.Text
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase){J.omitNothingFields=True} ''HTTPResp)
|
||||
|
||||
|
||||
data HTTPRespExtra
|
||||
= HTTPRespExtra
|
||||
{ _hreResponse :: HTTPResp
|
||||
, _hreContext :: Maybe ExtraContext
|
||||
}
|
||||
|
||||
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase){J.omitNothingFields=True} ''HTTPRespExtra)
|
||||
|
||||
instance ToEngineLog HTTPRespExtra where
|
||||
toEngineLog resp = (LevelInfo, "event-trigger", J.toJSON resp )
|
||||
|
||||
mkHTTPResp :: W.Response B.ByteString -> HTTPResp
|
||||
mkHTTPResp resp =
|
||||
HTTPResp
|
||||
(resp ^. W.responseStatus.W.statusCode)
|
||||
(map decodeHeader $ resp ^. W.responseHeaders)
|
||||
(decodeLBS $ resp ^. W.responseBody)
|
||||
where
|
||||
decodeBS = TE.decodeUtf8With TE.lenientDecode
|
||||
decodeLBS = TLE.decodeUtf8With TE.lenientDecode
|
||||
decodeHeader (hdrName, hdrVal)
|
||||
= decodeBS (CI.original hdrName) <> " : " <> decodeBS hdrVal
|
||||
|
||||
|
||||
runHTTP
|
||||
:: ( MonadReader r m
|
||||
, MonadError HTTPErr m
|
||||
, MonadIO m
|
||||
, Has WS.Session r
|
||||
, Has HLogger r
|
||||
)
|
||||
=> W.Options -> HTTP a -> Maybe ExtraContext -> m a
|
||||
runHTTP opts http exLog = do
|
||||
-- try the http request
|
||||
res <- R.retrying retryPol' retryFn' $ httpWithLogging opts http exLog
|
||||
|
||||
-- process the result
|
||||
either throwError return res
|
||||
|
||||
where
|
||||
retryPol' = R.RetryPolicyM $ liftIO . R.getRetryPolicyM (_hRetryPolicy http)
|
||||
retryFn' _ = return . _hRetryFn http
|
||||
|
||||
httpWithLogging
|
||||
:: ( MonadReader r m
|
||||
, MonadIO m
|
||||
, Has WS.Session r
|
||||
, Has HLogger r
|
||||
)
|
||||
=> W.Options -> HTTP a -> Maybe ExtraContext -> R.RetryStatus -> m (Either HTTPErr a)
|
||||
-- the actual http action
|
||||
httpWithLogging opts (HTTP method url mPayload mFormParams optsMod bodyParser _ _) exLog retryStatus = do
|
||||
(logF:: HLogger) <- asks getter
|
||||
-- log the request
|
||||
liftIO $ logF $ toEngineLog $ HTTPReq method url mPayload
|
||||
(R.rsIterNumber retryStatus) (R.rsPreviousDelay retryStatus)
|
||||
|
||||
session <- asks getter
|
||||
|
||||
res <- finallyRunHTTPPlz session
|
||||
case res of
|
||||
Left e -> liftIO $ logF $ toEngineLog $ HClient e
|
||||
Right resp ->
|
||||
--liftIO $ print "=======================>"
|
||||
liftIO $ logF $ toEngineLog $ HTTPRespExtra (mkHTTPResp resp) exLog
|
||||
--liftIO $ print "<======================="
|
||||
|
||||
-- return the processed response
|
||||
return $ either (Left . HClient) bodyParser res
|
||||
|
||||
where
|
||||
-- set wreq options to ignore status code exceptions
|
||||
ignoreStatusCodeExceptions _ _ = return ()
|
||||
finalOpts = optsMod opts
|
||||
& W.checkResponse ?~ ignoreStatusCodeExceptions
|
||||
|
||||
-- the actual function which makes the relevant Wreq calls
|
||||
finallyRunHTTPPlz sessMgr =
|
||||
liftIO $ try $
|
||||
case (mPayload, mFormParams) of
|
||||
(Just payload, _) -> WS.customPayloadMethodWith method finalOpts sessMgr url payload
|
||||
(Nothing, Just fps) -> WS.customPayloadMethodWith method finalOpts sessMgr url fps
|
||||
(Nothing, Nothing) -> WS.customMethodWith method finalOpts sessMgr url
|
||||
|
||||
mkHLogger :: LoggerCtx -> HLogger
|
||||
mkHLogger (LoggerCtx loggerSet serverLogLevel timeGetter) (logLevel, logTy, logDet) = do
|
||||
localTime <- timeGetter
|
||||
when (logLevel >= serverLogLevel) $
|
||||
FL.pushLogStrLn loggerSet $ FL.toLogStr $
|
||||
J.encode $ EngineLog localTime logLevel logTy logDet
|
||||
|
329
server/src-lib/Hasura/Events/Lib.hs
Normal file
329
server/src-lib/Hasura/Events/Lib.hs
Normal file
@ -0,0 +1,329 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Hasura.Events.Lib
|
||||
( initEventEngineCtx
|
||||
, processEventQueue
|
||||
, unlockAllEvents
|
||||
, defaultMaxEventThreads
|
||||
, defaultPollingIntervalSec
|
||||
) where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (async, waitAny)
|
||||
import Control.Concurrent.STM.TVar
|
||||
import Control.Monad.STM (STM, atomically, retry)
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.TH
|
||||
import Data.Either (isLeft)
|
||||
import Data.Has
|
||||
import Data.Int (Int64)
|
||||
import Data.IORef (IORef, readIORef)
|
||||
import Hasura.Events.HTTP
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Control.Concurrent.STM.TQueue as TQ
|
||||
import qualified Control.Retry as R
|
||||
import qualified Data.ByteString.Lazy as B
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.TByteString as TBS
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Time.Clock as Time
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Hasura.GraphQL.Schema as GS
|
||||
import qualified Hasura.Logging as L
|
||||
import qualified Network.HTTP.Types as N
|
||||
import qualified Network.Wreq as W
|
||||
import qualified Network.Wreq.Session as WS
|
||||
|
||||
|
||||
type CacheRef = IORef (SchemaCache, GS.GCtxMap)
|
||||
type UUID = T.Text
|
||||
|
||||
newtype EventInternalErr
|
||||
= EventInternalErr QErr
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance L.ToEngineLog EventInternalErr where
|
||||
toEngineLog (EventInternalErr qerr) = (L.LevelError, "event-trigger", toJSON qerr )
|
||||
|
||||
data TriggerMeta
|
||||
= TriggerMeta
|
||||
{ tmId :: TriggerId
|
||||
, tmName :: TriggerName
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''TriggerMeta)
|
||||
|
||||
data Event
|
||||
= Event
|
||||
{ eId :: UUID
|
||||
, eTable :: QualifiedTable
|
||||
, eTrigger :: TriggerMeta
|
||||
, eEvent :: Value
|
||||
-- , eDelivered :: Bool
|
||||
-- , eError :: Bool
|
||||
, eTries :: Int64
|
||||
, eCreatedAt :: Time.UTCTime
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance ToJSON Event where
|
||||
toJSON (Event eid (QualifiedTable sn tn) trigger event _ created)=
|
||||
object [ "id" .= eid
|
||||
, "table" .= object [ "schema" .= sn
|
||||
, "name" .= tn
|
||||
]
|
||||
, "trigger" .= trigger
|
||||
, "event" .= event
|
||||
, "created_at" .= created
|
||||
]
|
||||
|
||||
$(deriveFromJSON (aesonDrop 1 snakeCase){omitNothingFields=True} ''Event)
|
||||
|
||||
data Invocation
|
||||
= Invocation
|
||||
{ iEventId :: UUID
|
||||
, iStatus :: Int64
|
||||
, iRequest :: Value
|
||||
, iResponse :: TBS.TByteString
|
||||
}
|
||||
|
||||
data EventEngineCtx
|
||||
= EventEngineCtx
|
||||
{ _eeCtxEventQueue :: TQ.TQueue Event
|
||||
, _eeCtxEventThreads :: TVar Int
|
||||
, _eeCtxMaxEventThreads :: Int
|
||||
, _eeCtxPollingIntervalSec :: Int
|
||||
}
|
||||
|
||||
defaultMaxEventThreads :: Int
|
||||
defaultMaxEventThreads = 100
|
||||
|
||||
defaultPollingIntervalSec :: Int
|
||||
defaultPollingIntervalSec = 1
|
||||
|
||||
initEventEngineCtx :: Int -> Int -> STM EventEngineCtx
|
||||
initEventEngineCtx maxT pollI = do
|
||||
q <- TQ.newTQueue
|
||||
c <- newTVar 0
|
||||
return $ EventEngineCtx q c maxT pollI
|
||||
|
||||
processEventQueue :: L.LoggerCtx -> WS.Session -> Q.PGPool -> CacheRef -> EventEngineCtx -> IO ()
|
||||
processEventQueue logctx httpSess pool cacheRef eectx = do
|
||||
putStrLn "event_trigger: starting workers"
|
||||
threads <- mapM async [pollThread , consumeThread]
|
||||
void $ waitAny threads
|
||||
where
|
||||
pollThread = pollEvents (mkHLogger logctx) pool eectx
|
||||
consumeThread = consumeEvents (mkHLogger logctx) httpSess pool cacheRef eectx
|
||||
|
||||
pollEvents
|
||||
:: HLogger -> Q.PGPool -> EventEngineCtx -> IO ()
|
||||
pollEvents logger pool eectx = forever $ do
|
||||
let EventEngineCtx q _ _ pollI = eectx
|
||||
eventsOrError <- runExceptT $ Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) fetchEvents
|
||||
case eventsOrError of
|
||||
Left err -> logger $ L.toEngineLog $ EventInternalErr err
|
||||
Right events -> atomically $ mapM_ (TQ.writeTQueue q) events
|
||||
threadDelay (pollI * 1000 * 1000)
|
||||
|
||||
consumeEvents
|
||||
:: HLogger -> WS.Session -> Q.PGPool -> CacheRef -> EventEngineCtx -> IO ()
|
||||
consumeEvents logger httpSess pool cacheRef eectx = forever $ do
|
||||
event <- atomically $ do
|
||||
let EventEngineCtx q _ _ _ = eectx
|
||||
TQ.readTQueue q
|
||||
async $ runReaderT (processEvent pool event) (logger, httpSess, cacheRef, eectx)
|
||||
|
||||
processEvent
|
||||
:: ( MonadReader r m
|
||||
, MonadIO m
|
||||
, Has WS.Session r
|
||||
, Has HLogger r
|
||||
, Has CacheRef r
|
||||
, Has EventEngineCtx r
|
||||
)
|
||||
=> Q.PGPool -> Event -> m ()
|
||||
processEvent pool e = do
|
||||
(logger:: HLogger) <- asks getter
|
||||
retryPolicy <- getRetryPolicy e
|
||||
res <- R.retrying retryPolicy shouldRetry $ tryWebhook pool e
|
||||
liftIO $ either (errorFn logger) (void.return) res
|
||||
unlockRes <- liftIO $ runExceptT $ runUnlockQ pool e
|
||||
liftIO $ either (logQErr logger) (void.return ) unlockRes
|
||||
where
|
||||
shouldRetry :: (Monad m ) => R.RetryStatus -> Either HTTPErr a -> m Bool
|
||||
shouldRetry _ eitherResp = return $ isLeft eitherResp
|
||||
|
||||
errorFn :: HLogger -> HTTPErr -> IO ()
|
||||
errorFn logger err = do
|
||||
logger $ L.toEngineLog err
|
||||
errorRes <- runExceptT $ runErrorQ pool e
|
||||
case errorRes of
|
||||
Left err' -> logQErr logger err'
|
||||
Right _ -> return ()
|
||||
|
||||
logQErr :: HLogger -> QErr -> IO ()
|
||||
logQErr logger err = logger $ L.toEngineLog $ EventInternalErr err
|
||||
|
||||
getRetryPolicy
|
||||
:: ( MonadReader r m
|
||||
, MonadIO m
|
||||
, Has WS.Session r
|
||||
, Has HLogger r
|
||||
, Has CacheRef r
|
||||
, Has EventEngineCtx r
|
||||
)
|
||||
=> Event -> m (R.RetryPolicyM m)
|
||||
getRetryPolicy e = do
|
||||
cacheRef::CacheRef <- asks getter
|
||||
(cache, _) <- liftIO $ readIORef cacheRef
|
||||
let eti = getEventTriggerInfoFromEvent cache e
|
||||
retryConfM = etiRetryConf <$> eti
|
||||
retryConf = fromMaybe (RetryConf 0 10) retryConfM
|
||||
|
||||
let remainingRetries = max 0 $ fromIntegral (rcNumRetries retryConf) - getTries
|
||||
delay = fromIntegral (rcIntervalSec retryConf) * 1000000
|
||||
policy = R.constantDelay delay <> R.limitRetries remainingRetries
|
||||
return policy
|
||||
where
|
||||
getTries :: Int
|
||||
getTries = fromIntegral $ eTries e
|
||||
|
||||
tryWebhook
|
||||
:: ( MonadReader r m
|
||||
, MonadIO m
|
||||
, Has WS.Session r
|
||||
, Has HLogger r
|
||||
, Has CacheRef r
|
||||
, Has EventEngineCtx r
|
||||
)
|
||||
=> Q.PGPool -> Event -> R.RetryStatus -> m (Either HTTPErr B.ByteString)
|
||||
tryWebhook pool e _ = do
|
||||
logger:: HLogger <- asks getter
|
||||
cacheRef::CacheRef <- asks getter
|
||||
(cache, _) <- liftIO $ readIORef cacheRef
|
||||
let eti = getEventTriggerInfoFromEvent cache e
|
||||
case eti of
|
||||
Nothing -> return $ Left $ HOther "table or event-trigger not found"
|
||||
Just et -> do
|
||||
let webhook = etiWebhook et
|
||||
createdAt = eCreatedAt e
|
||||
eventId = eId e
|
||||
eeCtx <- asks getter
|
||||
|
||||
-- wait for counter and then increment beforing making http
|
||||
liftIO $ atomically $ do
|
||||
let EventEngineCtx _ c maxT _ = eeCtx
|
||||
countThreads <- readTVar c
|
||||
if countThreads >= maxT
|
||||
then retry
|
||||
else modifyTVar' c (+1)
|
||||
eitherResp <- runExceptT $ runHTTP W.defaults (mkAnyHTTPPost (T.unpack webhook) (Just $ toJSON e)) (Just (ExtraContext createdAt eventId))
|
||||
|
||||
--decrement counter once http is done
|
||||
liftIO $ atomically $ do
|
||||
let EventEngineCtx _ c _ _ = eeCtx
|
||||
modifyTVar' c (\v -> v - 1)
|
||||
|
||||
finally <- liftIO $ runExceptT $ case eitherResp of
|
||||
Left err ->
|
||||
case err of
|
||||
HClient excp -> runFailureQ pool $ Invocation (eId e) 1000 (toJSON e) (TBS.fromLBS $ encode $ show excp)
|
||||
HParse _ detail -> runFailureQ pool $ Invocation (eId e) 1001 (toJSON e) (TBS.fromLBS $ encode detail)
|
||||
HStatus status detail -> runFailureQ pool $ Invocation (eId e) (fromIntegral $ N.statusCode status) (toJSON e) detail
|
||||
HOther detail -> runFailureQ pool $ Invocation (eId e) 500 (toJSON e) (TBS.fromLBS $ encode detail)
|
||||
Right resp -> runSuccessQ pool e $ Invocation (eId e) 200 (toJSON e) (TBS.fromLBS resp)
|
||||
case finally of
|
||||
Left err -> liftIO $ logger $ L.toEngineLog $ EventInternalErr err
|
||||
Right _ -> return ()
|
||||
return eitherResp
|
||||
|
||||
getEventTriggerInfoFromEvent :: SchemaCache -> Event -> Maybe EventTriggerInfo
|
||||
getEventTriggerInfoFromEvent sc e = let table = eTable e
|
||||
tableInfo = M.lookup table $ scTables sc
|
||||
in M.lookup ( tmName $ eTrigger e) =<< (tiEventTriggerInfoMap <$> tableInfo)
|
||||
|
||||
fetchEvents :: Q.TxE QErr [Event]
|
||||
fetchEvents =
|
||||
map uncurryEvent <$> Q.listQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET locked = 't'
|
||||
WHERE id IN ( select id from hdb_catalog.event_log where delivered ='f' and error = 'f' and locked = 'f' LIMIT 100 )
|
||||
RETURNING id, schema_name, table_name, trigger_id, trigger_name, payload::json, tries, created_at
|
||||
|] () True
|
||||
where uncurryEvent (id', sn, tn, trid, trn, Q.AltJ payload, tries, created) = Event id' (QualifiedTable sn tn) (TriggerMeta trid trn) payload tries created
|
||||
|
||||
insertInvocation :: Invocation -> Q.TxE QErr ()
|
||||
insertInvocation invo = do
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
INSERT INTO hdb_catalog.event_invocation_logs (event_id, status, request, response)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
|] (iEventId invo, iStatus invo, Q.AltJ $ toJSON $ iRequest invo, Q.AltJ $ toJSON $ iResponse invo) True
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET tries = tries + 1
|
||||
WHERE id = $1
|
||||
|] (Identity $ iEventId invo) True
|
||||
|
||||
markDelivered :: Event -> Q.TxE QErr ()
|
||||
markDelivered e =
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET delivered = 't', error = 'f'
|
||||
WHERE id = $1
|
||||
|] (Identity $ eId e) True
|
||||
|
||||
markError :: Event -> Q.TxE QErr ()
|
||||
markError e =
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET error = 't'
|
||||
WHERE id = $1
|
||||
|] (Identity $ eId e) True
|
||||
|
||||
-- lockEvent :: Event -> Q.TxE QErr ()
|
||||
-- lockEvent e =
|
||||
-- Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
-- UPDATE hdb_catalog.event_log
|
||||
-- SET locked = 't'
|
||||
-- WHERE id = $1
|
||||
-- |] (Identity $ eId e) True
|
||||
|
||||
unlockEvent :: Event -> Q.TxE QErr ()
|
||||
unlockEvent e =
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET locked = 'f'
|
||||
WHERE id = $1
|
||||
|] (Identity $ eId e) True
|
||||
|
||||
unlockAllEvents :: Q.TxE QErr ()
|
||||
unlockAllEvents =
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
UPDATE hdb_catalog.event_log
|
||||
SET locked = 'f'
|
||||
|] () False
|
||||
|
||||
runFailureQ :: Q.PGPool -> Invocation -> ExceptT QErr IO ()
|
||||
runFailureQ pool invo = Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) $ insertInvocation invo
|
||||
|
||||
runSuccessQ :: Q.PGPool -> Event -> Invocation -> ExceptT QErr IO ()
|
||||
runSuccessQ pool e invo = Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) $ do
|
||||
insertInvocation invo
|
||||
markDelivered e
|
||||
|
||||
runErrorQ :: Q.PGPool -> Event -> ExceptT QErr IO ()
|
||||
runErrorQ pool e = Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) $ markError e
|
||||
|
||||
-- runLockQ :: Q.PGPool -> Event -> ExceptT QErr IO ()
|
||||
-- runLockQ pool e = Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) $ lockEvent e
|
||||
|
||||
runUnlockQ :: Q.PGPool -> Event -> ExceptT QErr IO ()
|
||||
runUnlockQ pool e = Q.runTx pool (Q.RepeatableRead, Just Q.ReadWrite) $ unlockEvent e
|
@ -1069,9 +1069,8 @@ mkGCtxMapTable
|
||||
=> TableCache
|
||||
-> TableInfo
|
||||
-> m (Map.HashMap RoleName (TyAgg, RootFlds))
|
||||
mkGCtxMapTable tableCache (TableInfo tn _ fields rolePerms constraints pkeyCols) = do
|
||||
m <- Map.traverseWithKey
|
||||
(mkGCtxRole tableCache tn fields pkeyCols validConstraints) rolePerms
|
||||
mkGCtxMapTable tableCache (TableInfo tn _ fields rolePerms constraints pkeyCols _) = do
|
||||
m <- Map.traverseWithKey (mkGCtxRole tableCache tn fields pkeyCols validConstraints) rolePerms
|
||||
let adminCtx = mkGCtxRole' tn (Just (colInfos, True))
|
||||
(Just selFlds) (Just colInfos) (Just ())
|
||||
pkeyColInfos validConstraints allCols
|
||||
|
@ -105,9 +105,9 @@ data LoggerSettings
|
||||
, _lsLevel :: !LogLevel
|
||||
} deriving (Show, Eq)
|
||||
|
||||
defaultLoggerSettings :: LoggerSettings
|
||||
defaultLoggerSettings =
|
||||
LoggerSettings True Nothing LevelInfo
|
||||
defaultLoggerSettings :: Bool -> LoggerSettings
|
||||
defaultLoggerSettings isCached =
|
||||
LoggerSettings isCached Nothing LevelInfo
|
||||
|
||||
getFormattedTime :: Maybe Time.TimeZone -> IO FormattedTime
|
||||
getFormattedTime tzM = do
|
||||
@ -117,7 +117,7 @@ getFormattedTime tzM = do
|
||||
return $ FormattedTime $ T.pack $ formatTime zt
|
||||
where
|
||||
formatTime = Format.formatTime Format.defaultTimeLocale format
|
||||
format = "%FT%T%z"
|
||||
format = "%FT%H:%M:%S%3Q%z"
|
||||
-- format = Format.iso8601DateFormat (Just "%H:%M:%S")
|
||||
|
||||
mkLoggerCtx :: LoggerSettings -> IO LoggerCtx
|
||||
|
@ -49,6 +49,8 @@ import qualified Hasura.RQL.DDL.Permission as DP
|
||||
import qualified Hasura.RQL.DDL.QueryTemplate as DQ
|
||||
import qualified Hasura.RQL.DDL.Relationship as DR
|
||||
import qualified Hasura.RQL.DDL.Schema.Table as DT
|
||||
import qualified Hasura.RQL.DDL.Subscribe as DS
|
||||
import qualified Hasura.RQL.Types.Subscribe as DTS
|
||||
|
||||
data TableMeta
|
||||
= TableMeta
|
||||
@ -59,11 +61,12 @@ data TableMeta
|
||||
, _tmSelectPermissions :: ![DP.SelPermDef]
|
||||
, _tmUpdatePermissions :: ![DP.UpdPermDef]
|
||||
, _tmDeletePermissions :: ![DP.DelPermDef]
|
||||
, _tmEventTriggers :: ![DTS.EventTriggerDef]
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
mkTableMeta :: QualifiedTable -> TableMeta
|
||||
mkTableMeta qt =
|
||||
TableMeta qt [] [] [] [] [] []
|
||||
TableMeta qt [] [] [] [] [] [] []
|
||||
|
||||
makeLenses ''TableMeta
|
||||
|
||||
@ -81,6 +84,7 @@ instance FromJSON TableMeta where
|
||||
<*> o .:? spKey .!= []
|
||||
<*> o .:? upKey .!= []
|
||||
<*> o .:? dpKey .!= []
|
||||
<*> o .:? etKey .!= []
|
||||
|
||||
where
|
||||
tableKey = "table"
|
||||
@ -90,13 +94,14 @@ instance FromJSON TableMeta where
|
||||
spKey = "select_permissions"
|
||||
upKey = "update_permissions"
|
||||
dpKey = "delete_permissions"
|
||||
etKey = "event_triggers"
|
||||
|
||||
unexpectedKeys =
|
||||
HS.fromList (M.keys o) `HS.difference` expectedKeySet
|
||||
|
||||
expectedKeySet =
|
||||
HS.fromList [ tableKey, orKey, arKey, ipKey
|
||||
, spKey, upKey, dpKey
|
||||
, spKey, upKey, dpKey, etKey
|
||||
]
|
||||
|
||||
parseJSON _ =
|
||||
@ -118,7 +123,7 @@ clearMetadata = Q.catchE defaultTxErrorHandler $ do
|
||||
Q.unitQ "DELETE FROM hdb_catalog.hdb_permission WHERE is_system_defined <> 'true'" () False
|
||||
Q.unitQ "DELETE FROM hdb_catalog.hdb_relationship WHERE is_system_defined <> 'true'" () False
|
||||
Q.unitQ "DELETE FROM hdb_catalog.hdb_table WHERE is_system_defined <> 'true'" () False
|
||||
Q.unitQ clearHdbViews () False
|
||||
clearHdbViews
|
||||
|
||||
instance HDBQuery ClearMetadata where
|
||||
|
||||
@ -158,12 +163,14 @@ applyQP1 (ReplaceMetadata tables templates) = do
|
||||
selPerms = map DP.pdRole $ table ^. tmSelectPermissions
|
||||
updPerms = map DP.pdRole $ table ^. tmUpdatePermissions
|
||||
delPerms = map DP.pdRole $ table ^. tmDeletePermissions
|
||||
eventTriggers = map DTS.etdName $ table ^. tmEventTriggers
|
||||
|
||||
checkMultipleDecls "relationships" allRels
|
||||
checkMultipleDecls "insert permissions" insPerms
|
||||
checkMultipleDecls "select permissions" selPerms
|
||||
checkMultipleDecls "update permissions" updPerms
|
||||
checkMultipleDecls "delete permissions" delPerms
|
||||
checkMultipleDecls "event triggers" eventTriggers
|
||||
|
||||
withPathK "queryTemplates" $
|
||||
checkMultipleDecls "query templates" $ map DQ.cqtName templates
|
||||
@ -214,6 +221,11 @@ applyQP2 (ReplaceMetadata tables templates) = do
|
||||
withPathK "delete_permissions" $ processPerms tabInfo $
|
||||
table ^. tmDeletePermissions
|
||||
|
||||
indexedForM_ tables $ \table -> do
|
||||
withPathK "event_triggers" $
|
||||
indexedForM_ (table ^. tmEventTriggers) $ \et ->
|
||||
DS.subTableP2 (table ^. tmTable) et
|
||||
|
||||
-- query templates
|
||||
withPathK "queryTemplates" $
|
||||
indexedForM_ templates $ \template -> do
|
||||
@ -276,6 +288,10 @@ fetchMetadata = do
|
||||
qtDef <- decodeValue qtDefVal
|
||||
return $ DQ.CreateQueryTemplate qtn qtDef mComment
|
||||
|
||||
-- Fetch all event triggers
|
||||
eventTriggers <- Q.catchE defaultTxErrorHandler fetchEventTriggers
|
||||
triggerMetaDefs <- mkTriggerMetaDefs eventTriggers
|
||||
|
||||
let (_, postRelMap) = flip runState tableMetaMap $ do
|
||||
modMetaMap tmObjectRelationships objRelDefs
|
||||
modMetaMap tmArrayRelationships arrRelDefs
|
||||
@ -283,6 +299,7 @@ fetchMetadata = do
|
||||
modMetaMap tmSelectPermissions selPermDefs
|
||||
modMetaMap tmUpdatePermissions updPermDefs
|
||||
modMetaMap tmDeletePermissions delPermDefs
|
||||
modMetaMap tmEventTriggers triggerMetaDefs
|
||||
|
||||
return $ ReplaceMetadata (M.elems postRelMap) qTmpltDefs
|
||||
|
||||
@ -304,6 +321,12 @@ fetchMetadata = do
|
||||
using <- decodeValue rDef
|
||||
return (QualifiedTable sn tn, DR.RelDef rn using mComment)
|
||||
|
||||
mkTriggerMetaDefs = mapM trigRowToDef
|
||||
|
||||
trigRowToDef (sn, tn, trn, Q.AltJ tDefVal, webhook, nr, rint) = do
|
||||
tDef <- decodeValue tDefVal
|
||||
return (QualifiedTable sn tn, DTS.EventTriggerDef trn tDef webhook (RetryConf nr rint))
|
||||
|
||||
fetchTables =
|
||||
Q.listQ [Q.sql|
|
||||
SELECT table_schema, table_name from hdb_catalog.hdb_table
|
||||
@ -330,6 +353,12 @@ fetchMetadata = do
|
||||
FROM hdb_catalog.hdb_query_template
|
||||
WHERE is_system_defined = 'false'
|
||||
|] () False
|
||||
fetchEventTriggers =
|
||||
Q.listQ [Q.sql|
|
||||
SELECT e.schema_name, e.table_name, e.name, e.definition::json, e.webhook, e.num_retries, e.retry_interval
|
||||
FROM hdb_catalog.event_triggers e
|
||||
|] () False
|
||||
|
||||
|
||||
instance HDBQuery ExportMetadata where
|
||||
|
||||
|
@ -17,6 +17,7 @@ import Hasura.RQL.DDL.Permission.Internal
|
||||
import Hasura.RQL.DDL.QueryTemplate
|
||||
import Hasura.RQL.DDL.Relationship
|
||||
import Hasura.RQL.DDL.Schema.Diff
|
||||
import Hasura.RQL.DDL.Subscribe
|
||||
import Hasura.RQL.DDL.Utils
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.Types
|
||||
@ -141,6 +142,10 @@ purgeDep schemaObjId = case schemaObjId of
|
||||
liftTx $ delQTemplateFromCatalog qtn
|
||||
delQTemplateFromCache qtn
|
||||
|
||||
(SOTableObj qt (TOTrigger trn)) -> do
|
||||
liftTx $ delEventTriggerFromCatalog trn
|
||||
delEventTriggerFromCache qt trn
|
||||
|
||||
_ -> throw500 $
|
||||
"unexpected dependent object : " <> reportSchemaObj schemaObjId
|
||||
|
||||
@ -199,6 +204,10 @@ processSchemaChanges schemaDiff = do
|
||||
DELETE FROM "hdb_catalog"."hdb_permission"
|
||||
WHERE table_schema = $1 AND table_name = $2
|
||||
|] (sn, tn) False
|
||||
Q.unitQ [Q.sql|
|
||||
DELETE FROM "hdb_catalog"."event_triggers"
|
||||
WHERE schema_name = $1 AND table_name = $2
|
||||
|] (sn, tn) False
|
||||
delTableFromCatalog qtn
|
||||
delTableFromCache qtn
|
||||
-- Get schema cache
|
||||
@ -333,6 +342,14 @@ buildSchemaCache = flip execStateT emptySchemaCache $ do
|
||||
qti <- liftP1 qCtx $ createQueryTemplateP1 $
|
||||
CreateQueryTemplate qtn qtDef Nothing
|
||||
addQTemplateToCache qti
|
||||
|
||||
eventTriggers <- lift $ Q.catchE defaultTxErrorHandler fetchEventTriggers
|
||||
forM_ eventTriggers $ \(sn, tn, trid, trn, Q.AltJ tDefVal, webhook, nr, rint) -> do
|
||||
tDef <- decodeValue tDefVal
|
||||
addEventTriggerToCache (QualifiedTable sn tn) trid trn tDef (RetryConf nr rint) webhook
|
||||
liftTx $ mkTriggerQ trid trn (QualifiedTable sn tn) tDef
|
||||
|
||||
|
||||
where
|
||||
permHelper sn tn rn pDef pa = do
|
||||
qCtx <- mkAdminQCtx <$> get
|
||||
@ -368,6 +385,12 @@ buildSchemaCache = flip execStateT emptySchemaCache $ do
|
||||
SELECT template_name, template_defn :: json FROM hdb_catalog.hdb_query_template
|
||||
|] () False
|
||||
|
||||
fetchEventTriggers =
|
||||
Q.listQ [Q.sql|
|
||||
SELECT e.schema_name, e.table_name, e.id, e.name, e.definition::json, e.webhook, e.num_retries, e.retry_interval
|
||||
FROM hdb_catalog.event_triggers e
|
||||
|] () False
|
||||
|
||||
data RunSQL
|
||||
= RunSQL
|
||||
{ rSql :: T.Text
|
||||
@ -388,8 +411,7 @@ runSqlP2 :: (P2C m) => RunSQL -> m RespBody
|
||||
runSqlP2 (RunSQL t cascade) = do
|
||||
|
||||
-- Drop hdb_views so no interference is caused to the sql query
|
||||
liftTx $ Q.catchE defaultTxErrorHandler $
|
||||
Q.unitQ clearHdbViews () False
|
||||
liftTx $ Q.catchE defaultTxErrorHandler clearHdbViews
|
||||
|
||||
-- Get the metadata before the sql query, everything, need to filter this
|
||||
oldMetaU <- liftTx $ Q.catchE defaultTxErrorHandler fetchTableMeta
|
||||
@ -422,6 +444,16 @@ runSqlP2 (RunSQL t cascade) = do
|
||||
forM_ (M.elems $ tiRolePermInfoMap ti) $ \rpi ->
|
||||
maybe (return ()) (liftTx . buildInsInfra tn) $ _permIns rpi
|
||||
|
||||
--recreate triggers
|
||||
forM_ (M.elems $ scTables postSc) $ \ti -> do
|
||||
let tn = tiName ti
|
||||
forM_ (M.toList $ tiEventTriggerInfoMap ti) $ \(trn, eti) -> do
|
||||
let insert = otiCols <$> etiInsert eti
|
||||
update = otiCols <$> etiUpdate eti
|
||||
delete = otiCols <$> etiDelete eti
|
||||
trid = etiId eti
|
||||
liftTx $ mkTriggerQ trid trn tn (TriggerOpsDef insert update delete)
|
||||
|
||||
return $ encode (res :: RunSQLRes)
|
||||
|
||||
where
|
||||
|
195
server/src-lib/Hasura/RQL/DDL/Subscribe.hs
Normal file
195
server/src-lib/Hasura/RQL/DDL/Subscribe.hs
Normal file
@ -0,0 +1,195 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
module Hasura.RQL.DDL.Subscribe where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Int (Int64)
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Data.FileEmbed as FE
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Text.Ginger as TG
|
||||
|
||||
data Ops = INSERT | UPDATE | DELETE deriving (Show)
|
||||
|
||||
data OpVar = OLD | NEW deriving (Show)
|
||||
|
||||
type GingerTmplt = TG.Template TG.SourcePos
|
||||
|
||||
defaultNumRetries :: Int64
|
||||
defaultNumRetries = 0
|
||||
|
||||
defaultRetryInterval :: Int64
|
||||
defaultRetryInterval = 10
|
||||
|
||||
parseGingerTmplt :: TG.Source -> Either String GingerTmplt
|
||||
parseGingerTmplt src = either parseE Right res
|
||||
where
|
||||
res = runIdentity $ TG.parseGinger' parserOptions src
|
||||
parserOptions = TG.mkParserOptions resolver
|
||||
resolver = const $ return Nothing
|
||||
parseE e = Left $ TG.formatParserError (Just "") e
|
||||
|
||||
triggerTmplt :: Maybe GingerTmplt
|
||||
triggerTmplt = case parseGingerTmplt $(FE.embedStringFile "src-rsr/trigger.sql.j2") of
|
||||
Left _ -> Nothing
|
||||
Right tmplt -> Just tmplt
|
||||
|
||||
getDropFuncSql :: Ops -> TriggerName -> T.Text
|
||||
getDropFuncSql op trn = "DROP FUNCTION IF EXISTS"
|
||||
<> " hdb_views.notify_hasura_" <> trn <> "_" <> T.pack (show op) <> "()"
|
||||
<> " CASCADE"
|
||||
|
||||
getTriggerSql :: Ops -> TriggerId -> TriggerName -> SchemaName -> TableName -> Maybe SubscribeOpSpec -> Maybe T.Text
|
||||
getTriggerSql op trid trn sn tn spec =
|
||||
let globalCtx = HashMap.fromList [
|
||||
(T.pack "ID", trid)
|
||||
, (T.pack "NAME", trn)
|
||||
, (T.pack "SCHEMA_NAME", getSchemaTxt sn)
|
||||
, (T.pack "TABLE_NAME", getTableTxt tn)]
|
||||
opCtx = maybe HashMap.empty (createOpCtx op) spec
|
||||
context = HashMap.union globalCtx opCtx
|
||||
in
|
||||
spec >> renderSql context <$> triggerTmplt
|
||||
where
|
||||
createOpCtx :: Ops -> SubscribeOpSpec -> HashMap.HashMap T.Text T.Text
|
||||
createOpCtx op1 (SubscribeOpSpec columns) = HashMap.fromList [
|
||||
(T.pack "OPERATION", T.pack $ show op1)
|
||||
, (T.pack "OLD_DATA_EXPRESSION", renderOldDataExp op1 columns )
|
||||
, (T.pack "NEW_DATA_EXPRESSION", renderNewDataExp op1 columns )]
|
||||
renderOldDataExp :: Ops -> SubscribeColumns -> T.Text
|
||||
renderOldDataExp op2 scs = case op2 of
|
||||
INSERT -> "NULL"
|
||||
UPDATE -> getRowExpression OLD scs
|
||||
DELETE -> getRowExpression OLD scs
|
||||
renderNewDataExp :: Ops -> SubscribeColumns -> T.Text
|
||||
renderNewDataExp op2 scs = case op2 of
|
||||
INSERT -> getRowExpression NEW scs
|
||||
UPDATE -> getRowExpression NEW scs
|
||||
DELETE -> "NULL"
|
||||
getRowExpression :: OpVar -> SubscribeColumns -> T.Text
|
||||
getRowExpression opVar scs = case scs of
|
||||
SubCStar -> "row_to_json(" <> T.pack (show opVar) <> ")"
|
||||
SubCArray cols -> "row_to_json((select r from (select " <> listcols cols opVar <> ") as r))"
|
||||
where
|
||||
listcols :: [PGCol] -> OpVar -> T.Text
|
||||
listcols pgcols var = T.intercalate ", " $ fmap (mkQualified (T.pack $ show var).getPGColTxt) pgcols
|
||||
mkQualified :: T.Text -> T.Text -> T.Text
|
||||
mkQualified v col = v <> "." <> col
|
||||
|
||||
renderSql :: HashMap.HashMap T.Text T.Text -> GingerTmplt -> T.Text
|
||||
renderSql = TG.easyRender
|
||||
|
||||
mkTriggerQ
|
||||
:: TriggerId
|
||||
-> TriggerName
|
||||
-> QualifiedTable
|
||||
-> TriggerOpsDef
|
||||
-> Q.TxE QErr ()
|
||||
mkTriggerQ trid trn (QualifiedTable sn tn) (TriggerOpsDef insert update delete) = do
|
||||
let msql = getTriggerSql INSERT trid trn sn tn insert
|
||||
<> getTriggerSql UPDATE trid trn sn tn update
|
||||
<> getTriggerSql DELETE trid trn sn tn delete
|
||||
case msql of
|
||||
Just sql -> Q.multiQE defaultTxErrorHandler (Q.fromBuilder $ TE.encodeUtf8Builder sql)
|
||||
Nothing -> throw500 "no trigger sql generated"
|
||||
|
||||
addEventTriggerToCatalog :: QualifiedTable -> EventTriggerDef
|
||||
-> Q.TxE QErr TriggerId
|
||||
addEventTriggerToCatalog (QualifiedTable sn tn) (EventTriggerDef name def webhook rconf) = do
|
||||
ids <- map runIdentity <$> Q.listQE defaultTxErrorHandler [Q.sql|
|
||||
INSERT into hdb_catalog.event_triggers (name, type, schema_name, table_name, definition, webhook, num_retries, retry_interval)
|
||||
VALUES ($1, 'table', $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
|] (name, sn, tn, Q.AltJ $ toJSON def, webhook, rcNumRetries rconf, rcIntervalSec rconf) True
|
||||
|
||||
trid <- getTrid ids
|
||||
mkTriggerQ trid name (QualifiedTable sn tn) def
|
||||
return trid
|
||||
where
|
||||
getTrid [] = throw500 "could not create event-trigger"
|
||||
getTrid (x:_) = return x
|
||||
|
||||
|
||||
delEventTriggerFromCatalog :: TriggerName -> Q.TxE QErr ()
|
||||
delEventTriggerFromCatalog trn = do
|
||||
Q.unitQE defaultTxErrorHandler [Q.sql|
|
||||
DELETE FROM
|
||||
hdb_catalog.event_triggers
|
||||
WHERE name = $1
|
||||
|] (Identity trn) True
|
||||
mapM_ tx [INSERT, UPDATE, DELETE]
|
||||
where
|
||||
tx :: Ops -> Q.TxE QErr ()
|
||||
tx op = Q.multiQE defaultTxErrorHandler (Q.fromBuilder $ TE.encodeUtf8Builder $ getDropFuncSql op trn)
|
||||
|
||||
fetchEventTrigger :: TriggerName -> Q.TxE QErr EventTrigger
|
||||
fetchEventTrigger trn = do
|
||||
triggers <- Q.listQE defaultTxErrorHandler [Q.sql|
|
||||
SELECT e.schema_name, e.table_name, e.name, e.definition::json, e.webhook, e.num_retries, e.retry_interval
|
||||
FROM hdb_catalog.event_triggers e
|
||||
WHERE e.name = $1
|
||||
|] (Identity trn) True
|
||||
getTrigger triggers
|
||||
where
|
||||
getTrigger [] = throw400 NotExists ("could not find event trigger '" <> trn <> "'")
|
||||
getTrigger (x:_) = return $ EventTrigger (QualifiedTable sn tn) trn' tDef webhook (RetryConf nr rint)
|
||||
where (sn, tn, trn', Q.AltJ tDef, webhook, nr, rint) = x
|
||||
|
||||
subTableP1 :: (P1C m) => CreateEventTriggerQuery -> m (QualifiedTable, EventTriggerDef)
|
||||
subTableP1 (CreateEventTriggerQuery name qt insert update delete retryConf webhook) = do
|
||||
ti <- askTabInfo qt
|
||||
assertCols ti insert
|
||||
assertCols ti update
|
||||
assertCols ti delete
|
||||
let rconf = fromMaybe (RetryConf defaultNumRetries defaultRetryInterval) retryConf
|
||||
return (qt, EventTriggerDef name (TriggerOpsDef insert update delete) webhook rconf)
|
||||
where
|
||||
assertCols _ Nothing = return ()
|
||||
assertCols ti (Just sos) = do
|
||||
let cols = sosColumns sos
|
||||
case cols of
|
||||
SubCStar -> return ()
|
||||
SubCArray pgcols -> forM_ pgcols (assertPGCol (tiFieldInfoMap ti) "")
|
||||
|
||||
subTableP2 :: (P2C m) => QualifiedTable -> EventTriggerDef -> m ()
|
||||
subTableP2 qt q@(EventTriggerDef name def webhook rconf) = do
|
||||
trid <- liftTx $ addEventTriggerToCatalog qt q
|
||||
addEventTriggerToCache qt trid name def rconf webhook
|
||||
|
||||
subTableP2shim :: (P2C m) => (QualifiedTable, EventTriggerDef) -> m RespBody
|
||||
subTableP2shim (qt, etdef) = do
|
||||
subTableP2 qt etdef
|
||||
return successMsg
|
||||
|
||||
instance HDBQuery CreateEventTriggerQuery where
|
||||
type Phase1Res CreateEventTriggerQuery = (QualifiedTable, EventTriggerDef)
|
||||
phaseOne = subTableP1
|
||||
phaseTwo _ = subTableP2shim
|
||||
schemaCachePolicy = SCPReload
|
||||
|
||||
unsubTableP1 :: (P1C m) => DeleteEventTriggerQuery -> m ()
|
||||
unsubTableP1 _ = return ()
|
||||
|
||||
unsubTableP2 :: (P2C m) => DeleteEventTriggerQuery -> m RespBody
|
||||
unsubTableP2 (DeleteEventTriggerQuery name) = do
|
||||
et <- liftTx $ fetchEventTrigger name
|
||||
delEventTriggerFromCache (etTable et) name
|
||||
liftTx $ delEventTriggerFromCatalog name
|
||||
return successMsg
|
||||
|
||||
instance HDBQuery DeleteEventTriggerQuery where
|
||||
type Phase1Res DeleteEventTriggerQuery = ()
|
||||
phaseOne = unsubTableP1
|
||||
phaseTwo q _ = unsubTableP2 q
|
||||
schemaCachePolicy = SCPReload
|
@ -2,14 +2,35 @@
|
||||
|
||||
module Hasura.RQL.DDL.Utils where
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
import qualified Data.ByteString.Builder as BB
|
||||
import qualified Database.PG.Query as Q
|
||||
import Hasura.Prelude ((<>))
|
||||
|
||||
clearHdbViews :: Q.Query
|
||||
clearHdbViews =
|
||||
clearHdbViews :: Q.Tx ()
|
||||
clearHdbViews = Q.multiQ (Q.fromBuilder (clearHdbOnlyViews <> clearHdbViewsFunc))
|
||||
|
||||
clearHdbOnlyViews :: BB.Builder
|
||||
clearHdbOnlyViews =
|
||||
"DO $$ DECLARE \
|
||||
\ r RECORD; \
|
||||
\ BEGIN \
|
||||
\ FOR r IN (SELECT viewname FROM pg_views WHERE schemaname = 'hdb_views') LOOP \
|
||||
\ EXECUTE 'DROP VIEW IF EXISTS hdb_views.' || quote_ident(r.viewname) || ' CASCADE'; \
|
||||
\ END LOOP; \
|
||||
\ END $$ "
|
||||
\ END $$; "
|
||||
|
||||
|
||||
clearHdbViewsFunc :: BB.Builder
|
||||
clearHdbViewsFunc =
|
||||
"DO $$ DECLARE \
|
||||
\ _sql text; \
|
||||
\ BEGIN \
|
||||
\ SELECT INTO _sql \
|
||||
\ string_agg('DROP FUNCTION hdb_views.' || quote_ident(r.routine_name) || '() CASCADE;' \
|
||||
\ , E'\n') \
|
||||
\ FROM information_schema.routines r \
|
||||
\ WHERE r.specific_schema = 'hdb_views'; \
|
||||
\ IF _sql IS NOT NULL THEN \
|
||||
\ EXECUTE _sql; \
|
||||
\ END IF; \
|
||||
\ END $$; "
|
||||
|
@ -53,6 +53,7 @@ import Hasura.RQL.Types.DML as R
|
||||
import Hasura.RQL.Types.Error as R
|
||||
import Hasura.RQL.Types.Permission as R
|
||||
import Hasura.RQL.Types.SchemaCache as R
|
||||
import Hasura.RQL.Types.Subscribe as R
|
||||
import Hasura.SQL.Types
|
||||
|
||||
import qualified Database.PG.Query as Q
|
||||
|
@ -62,6 +62,12 @@ module Hasura.RQL.Types.SchemaCache
|
||||
, delQTemplateFromCache
|
||||
, TemplateParamInfo(..)
|
||||
|
||||
, addEventTriggerToCache
|
||||
, delEventTriggerFromCache
|
||||
, getOpInfo
|
||||
, EventTriggerInfo(..)
|
||||
, OpTriggerInfo(..)
|
||||
|
||||
, TableObjId(..)
|
||||
, SchemaObjId(..)
|
||||
, reportSchemaObj
|
||||
@ -75,6 +81,7 @@ module Hasura.RQL.Types.SchemaCache
|
||||
, getDependentObjsOfQTemplateCache
|
||||
, getDependentPermsOfTable
|
||||
, getDependentRelsOfTable
|
||||
, getDependentTriggersOfTable
|
||||
, isDependentOn
|
||||
) where
|
||||
|
||||
@ -84,6 +91,7 @@ import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.DML
|
||||
import Hasura.RQL.Types.Error
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.RQL.Types.Subscribe
|
||||
import qualified Hasura.SQL.DML as S
|
||||
import Hasura.SQL.Types
|
||||
|
||||
@ -103,6 +111,7 @@ data TableObjId
|
||||
| TORel !RelName
|
||||
| TOCons !ConstraintName
|
||||
| TOPerm !RoleName !PermType
|
||||
| TOTrigger !TriggerName
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Hashable TableObjId
|
||||
@ -128,6 +137,8 @@ reportSchemaObj (SOTableObj tn (TOCons cn)) =
|
||||
reportSchemaObj (SOTableObj tn (TOPerm rn pt)) =
|
||||
"permission " <> qualTableToTxt tn <> "." <> getRoleTxt rn
|
||||
<> "." <> permTypeToCode pt
|
||||
reportSchemaObj (SOTableObj tn (TOTrigger trn )) =
|
||||
"event-trigger " <> qualTableToTxt tn <> "." <> trn
|
||||
|
||||
reportSchemaObjs :: [SchemaObjId] -> T.Text
|
||||
reportSchemaObjs = T.intercalate ", " . map reportSchemaObj
|
||||
@ -323,6 +334,39 @@ makeLenses ''RolePermInfo
|
||||
|
||||
type RolePermInfoMap = M.HashMap RoleName RolePermInfo
|
||||
|
||||
data OpTriggerInfo
|
||||
= OpTriggerInfo
|
||||
{ otiTable :: !QualifiedTable
|
||||
, otiTriggerName :: !TriggerName
|
||||
, otiCols :: !SubscribeOpSpec
|
||||
, otiDeps :: ![SchemaDependency]
|
||||
} deriving (Show, Eq)
|
||||
$(deriveToJSON (aesonDrop 3 snakeCase) ''OpTriggerInfo)
|
||||
|
||||
instance CachedSchemaObj OpTriggerInfo where
|
||||
dependsOn = otiDeps
|
||||
|
||||
data EventTriggerInfo
|
||||
= EventTriggerInfo
|
||||
{ etiId :: !TriggerId
|
||||
, etiName :: !TriggerName
|
||||
, etiInsert :: !(Maybe OpTriggerInfo)
|
||||
, etiUpdate :: !(Maybe OpTriggerInfo)
|
||||
, etiDelete :: !(Maybe OpTriggerInfo)
|
||||
, etiRetryConf :: !RetryConf
|
||||
, etiWebhook :: !T.Text
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(deriveToJSON (aesonDrop 3 snakeCase) ''EventTriggerInfo)
|
||||
|
||||
type EventTriggerInfoMap = M.HashMap TriggerName EventTriggerInfo
|
||||
|
||||
getTriggers :: EventTriggerInfoMap -> [OpTriggerInfo]
|
||||
getTriggers etim = toOpTriggerInfo $ M.elems etim
|
||||
where
|
||||
toOpTriggerInfo etis = catMaybes $ foldl (\acc eti -> acc ++ [etiInsert eti, etiUpdate eti, etiDelete eti]) [] etis
|
||||
|
||||
|
||||
data ConstraintType
|
||||
= CTCHECK
|
||||
| CTFOREIGNKEY
|
||||
@ -367,20 +411,21 @@ isUniqueOrPrimary (TableConstraint ty _) = case ty of
|
||||
|
||||
data TableInfo
|
||||
= TableInfo
|
||||
{ tiName :: !QualifiedTable
|
||||
, tiSystemDefined :: !Bool
|
||||
, tiFieldInfoMap :: !FieldInfoMap
|
||||
, tiRolePermInfoMap :: !RolePermInfoMap
|
||||
, tiConstraints :: ![TableConstraint]
|
||||
, tiPrimaryKeyCols :: ![PGCol]
|
||||
{ tiName :: !QualifiedTable
|
||||
, tiSystemDefined :: !Bool
|
||||
, tiFieldInfoMap :: !FieldInfoMap
|
||||
, tiRolePermInfoMap :: !RolePermInfoMap
|
||||
, tiConstraints :: ![TableConstraint]
|
||||
, tiPrimaryKeyCols :: ![PGCol]
|
||||
, tiEventTriggerInfoMap :: !EventTriggerInfoMap
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(deriveToJSON (aesonDrop 2 snakeCase) ''TableInfo)
|
||||
|
||||
mkTableInfo :: QualifiedTable -> Bool -> [(ConstraintType, ConstraintName)]
|
||||
-> [(PGCol, PGColType, Bool)] -> [PGCol] -> TableInfo
|
||||
mkTableInfo tn isSystemDefined rawCons cols =
|
||||
TableInfo tn isSystemDefined colMap (M.fromList []) constraints
|
||||
mkTableInfo tn isSystemDefined rawCons cols pcols =
|
||||
TableInfo tn isSystemDefined colMap (M.fromList []) constraints pcols (M.fromList [])
|
||||
where
|
||||
constraints = flip map rawCons $ uncurry TableConstraint
|
||||
colMap = M.fromList $ map f cols
|
||||
@ -539,6 +584,43 @@ withPermType PTSelect f = f PASelect
|
||||
withPermType PTUpdate f = f PAUpdate
|
||||
withPermType PTDelete f = f PADelete
|
||||
|
||||
addEventTriggerToCache
|
||||
:: (QErrM m, CacheRWM m)
|
||||
=> QualifiedTable
|
||||
-> TriggerId
|
||||
-> TriggerName
|
||||
-> TriggerOpsDef
|
||||
-> RetryConf
|
||||
-> T.Text
|
||||
-> m ()
|
||||
addEventTriggerToCache qt trid trn tdef rconf webhook =
|
||||
modTableInCache modEventTriggerInfo qt
|
||||
where
|
||||
modEventTriggerInfo ti = do
|
||||
let eti = EventTriggerInfo
|
||||
trid
|
||||
trn
|
||||
(getOpInfo trn ti $ tdInsert tdef)
|
||||
(getOpInfo trn ti $ tdUpdate tdef)
|
||||
(getOpInfo trn ti $ tdDelete tdef)
|
||||
rconf
|
||||
webhook
|
||||
etim = tiEventTriggerInfoMap ti
|
||||
-- fail $ show (toJSON eti)
|
||||
return $ ti { tiEventTriggerInfoMap = M.insert trn eti etim}
|
||||
|
||||
delEventTriggerFromCache
|
||||
:: (QErrM m, CacheRWM m)
|
||||
=> QualifiedTable
|
||||
-> TriggerName
|
||||
-> m ()
|
||||
delEventTriggerFromCache qt trn =
|
||||
modTableInCache modEventTriggerInfo qt
|
||||
where
|
||||
modEventTriggerInfo ti = do
|
||||
let etim = tiEventTriggerInfoMap ti
|
||||
return $ ti { tiEventTriggerInfoMap = M.delete trn etim }
|
||||
|
||||
addPermToCache
|
||||
:: (QErrM m, CacheRWM m)
|
||||
=> QualifiedTable
|
||||
@ -622,27 +704,30 @@ getDependentObjsOfQTemplateCache objId qtc =
|
||||
|
||||
getDependentObjsOfTable :: SchemaObjId -> TableInfo -> [SchemaObjId]
|
||||
getDependentObjsOfTable objId ti =
|
||||
rels ++ perms
|
||||
rels ++ perms ++ triggers
|
||||
where
|
||||
rels = getDependentRelsOfTable (const True) objId ti
|
||||
perms = getDependentPermsOfTable (const True) objId ti
|
||||
triggers = getDependentTriggersOfTable (const True) objId ti
|
||||
|
||||
|
||||
getDependentObjsOfTableWith :: (T.Text -> Bool) -> SchemaObjId -> TableInfo -> [SchemaObjId]
|
||||
getDependentObjsOfTableWith f objId ti =
|
||||
rels ++ perms
|
||||
rels ++ perms ++ triggers
|
||||
where
|
||||
rels = getDependentRelsOfTable f objId ti
|
||||
perms = getDependentPermsOfTable f objId ti
|
||||
triggers = getDependentTriggersOfTable f objId ti
|
||||
|
||||
getDependentRelsOfTable :: (T.Text -> Bool) -> SchemaObjId
|
||||
-> TableInfo -> [SchemaObjId]
|
||||
getDependentRelsOfTable rsnFn objId (TableInfo tn _ fim _ _ _) =
|
||||
getDependentRelsOfTable rsnFn objId (TableInfo tn _ fim _ _ _ _) =
|
||||
map (SOTableObj tn . TORel . riName) $
|
||||
filter (isDependentOn rsnFn objId) $ getRels fim
|
||||
|
||||
getDependentPermsOfTable :: (T.Text -> Bool) -> SchemaObjId
|
||||
-> TableInfo -> [SchemaObjId]
|
||||
getDependentPermsOfTable rsnFn objId (TableInfo tn _ _ rpim _ _) =
|
||||
getDependentPermsOfTable rsnFn objId (TableInfo tn _ _ rpim _ _ _) =
|
||||
concat $ flip M.mapWithKey rpim $
|
||||
\rn rpi -> map (SOTableObj tn . TOPerm rn) $ getDependentPerms' rsnFn objId rpi
|
||||
|
||||
@ -658,3 +743,23 @@ getDependentPerms' rsnFn objId (RolePermInfo mipi mspi mupi mdpi) =
|
||||
toPermRow :: forall a. (CachedSchemaObj a) => PermType -> a -> Maybe PermType
|
||||
toPermRow pt =
|
||||
bool Nothing (Just pt) . isDependentOn rsnFn objId
|
||||
|
||||
getDependentTriggersOfTable :: (T.Text -> Bool) -> SchemaObjId
|
||||
-> TableInfo -> [SchemaObjId]
|
||||
getDependentTriggersOfTable rsnFn objId (TableInfo tn _ _ _ _ _ et) =
|
||||
map (SOTableObj tn . TOTrigger . otiTriggerName ) $ filter (isDependentOn rsnFn objId) $ getTriggers et
|
||||
|
||||
getOpInfo :: TriggerName -> TableInfo -> Maybe SubscribeOpSpec -> Maybe OpTriggerInfo
|
||||
getOpInfo trn ti mos= fromSubscrOpSpec <$> mos
|
||||
where
|
||||
fromSubscrOpSpec :: SubscribeOpSpec -> OpTriggerInfo
|
||||
fromSubscrOpSpec os =
|
||||
let qt = tiName ti
|
||||
cols = getColsFromSub $ sosColumns os
|
||||
schemaDeps = SchemaDependency (SOTable qt) "event trigger is dependent on table"
|
||||
: map (\col -> SchemaDependency (SOTableObj qt (TOCol col)) "event trigger is dependent on column") (toList cols)
|
||||
in OpTriggerInfo qt trn os schemaDeps
|
||||
where
|
||||
getColsFromSub sc = case sc of
|
||||
SubCStar -> HS.fromList $ map pgiName $ getCols $ tiFieldInfoMap ti
|
||||
SubCArray pgcols -> HS.fromList pgcols
|
||||
|
128
server/src-lib/Hasura/RQL/Types/Subscribe.hs
Normal file
128
server/src-lib/Hasura/RQL/Types/Subscribe.hs
Normal file
@ -0,0 +1,128 @@
|
||||
{-# LANGUAGE DeriveLift #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Hasura.RQL.Types.Subscribe
|
||||
( CreateEventTriggerQuery(..)
|
||||
, SubscribeOpSpec(..)
|
||||
, SubscribeColumns(..)
|
||||
, TriggerName
|
||||
, TriggerId
|
||||
, TriggerOpsDef(..)
|
||||
, EventTrigger(..)
|
||||
, EventTriggerDef(..)
|
||||
, RetryConf(..)
|
||||
, DeleteEventTriggerQuery(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Casing
|
||||
import Data.Aeson.TH
|
||||
import Data.Int (Int64)
|
||||
import Hasura.Prelude
|
||||
import Hasura.SQL.Types
|
||||
import Language.Haskell.TH.Syntax (Lift)
|
||||
import Text.Regex (matchRegex, mkRegex)
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
type TriggerName = T.Text
|
||||
type TriggerId = T.Text
|
||||
|
||||
data SubscribeColumns = SubCStar | SubCArray [PGCol] deriving (Show, Eq, Lift)
|
||||
|
||||
instance FromJSON SubscribeColumns where
|
||||
parseJSON (String s) = case s of
|
||||
"*" -> return SubCStar
|
||||
_ -> fail "only * or [] allowed"
|
||||
parseJSON v@(Array _) = SubCArray <$> parseJSON v
|
||||
parseJSON _ = fail "unexpected columns"
|
||||
|
||||
instance ToJSON SubscribeColumns where
|
||||
toJSON SubCStar = "*"
|
||||
toJSON (SubCArray cols) = toJSON cols
|
||||
|
||||
data SubscribeOpSpec
|
||||
= SubscribeOpSpec
|
||||
{ sosColumns :: !SubscribeColumns
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''SubscribeOpSpec)
|
||||
|
||||
data RetryConf
|
||||
= RetryConf
|
||||
{ rcNumRetries :: !Int64
|
||||
, rcIntervalSec :: !Int64
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''RetryConf)
|
||||
|
||||
data CreateEventTriggerQuery
|
||||
= CreateEventTriggerQuery
|
||||
{ cetqName :: !T.Text
|
||||
, cetqTable :: !QualifiedTable
|
||||
, cetqInsert :: !(Maybe SubscribeOpSpec)
|
||||
, cetqUpdate :: !(Maybe SubscribeOpSpec)
|
||||
, cetqDelete :: !(Maybe SubscribeOpSpec)
|
||||
, cetqRetryConf :: !(Maybe RetryConf)
|
||||
, cetqWebhook :: !T.Text
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
instance FromJSON CreateEventTriggerQuery where
|
||||
parseJSON (Object o) = do
|
||||
name <- o .: "name"
|
||||
table <- o .: "table"
|
||||
insert <- o .:? "insert"
|
||||
update <- o .:? "update"
|
||||
delete <- o .:? "delete"
|
||||
retryConf <- o .:? "retry_conf"
|
||||
webhook <- o .: "webhook"
|
||||
let regex = mkRegex "^\\w+$"
|
||||
mName = matchRegex regex (T.unpack name)
|
||||
case mName of
|
||||
Just _ -> return ()
|
||||
Nothing -> fail "only alphanumeric and underscore allowed for name"
|
||||
case insert <|> update <|> delete of
|
||||
Just _ -> return ()
|
||||
Nothing -> fail "must provide operation spec(s)"
|
||||
return $ CreateEventTriggerQuery name table insert update delete retryConf webhook
|
||||
parseJSON _ = fail "expecting an object"
|
||||
|
||||
$(deriveToJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''CreateEventTriggerQuery)
|
||||
|
||||
data TriggerOpsDef
|
||||
= TriggerOpsDef
|
||||
{ tdInsert :: !(Maybe SubscribeOpSpec)
|
||||
, tdUpdate :: !(Maybe SubscribeOpSpec)
|
||||
, tdDelete :: !(Maybe SubscribeOpSpec)
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''TriggerOpsDef)
|
||||
|
||||
data DeleteEventTriggerQuery
|
||||
= DeleteEventTriggerQuery
|
||||
{ detqName :: !T.Text
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
$(deriveJSON (aesonDrop 4 snakeCase){omitNothingFields=True} ''DeleteEventTriggerQuery)
|
||||
|
||||
data EventTrigger
|
||||
= EventTrigger
|
||||
{ etTable :: !QualifiedTable
|
||||
, etName :: !TriggerName
|
||||
, etDefinition :: !TriggerOpsDef
|
||||
, etWebhook :: !T.Text
|
||||
, etRetryConf :: !RetryConf
|
||||
}
|
||||
|
||||
$(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''EventTrigger)
|
||||
|
||||
data EventTriggerDef
|
||||
= EventTriggerDef
|
||||
{ etdName :: !TriggerName
|
||||
, etdDefinition :: !TriggerOpsDef
|
||||
, etdWebhook :: !T.Text
|
||||
, etdRetryConf :: !RetryConf
|
||||
} deriving (Show, Eq, Lift)
|
||||
|
||||
$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''EventTriggerDef)
|
@ -273,7 +273,7 @@ mkWaiApp
|
||||
-> AuthMode
|
||||
-> CorsConfig
|
||||
-> Bool
|
||||
-> IO Wai.Application
|
||||
-> IO (Wai.Application, IORef (SchemaCache, GS.GCtxMap))
|
||||
mkWaiApp isoLevel mRootDir loggerCtx pool httpManager mode corsCfg enableConsole = do
|
||||
cacheRef <- do
|
||||
pgResp <- liftIO $ runExceptT $ Q.runTx pool (Q.Serializable, Nothing) $ do
|
||||
@ -295,7 +295,7 @@ mkWaiApp isoLevel mRootDir loggerCtx pool httpManager mode corsCfg enableConsole
|
||||
|
||||
wsServerEnv <- WS.createWSServerEnv (scLogger serverCtx) httpManager cacheRef runTx
|
||||
let wsServerApp = WS.createWSServerApp mode wsServerEnv
|
||||
return $ WS.websocketsOr WS.defaultConnectionOptions wsServerApp spockApp
|
||||
return (WS.websocketsOr WS.defaultConnectionOptions wsServerApp spockApp, cacheRef)
|
||||
|
||||
httpApp :: Maybe String -> CorsConfig -> ServerCtx -> Bool -> SpockT IO ()
|
||||
httpApp mRootDir corsCfg serverCtx enableConsole = do
|
||||
|
@ -32,7 +32,7 @@ initErrExit e = print e >> exitFailure
|
||||
|
||||
-- clear the hdb_views schema
|
||||
initStateTx :: Q.Tx ()
|
||||
initStateTx = Q.unitQ clearHdbViews () False
|
||||
initStateTx = clearHdbViews
|
||||
|
||||
data RawConnInfo =
|
||||
RawConnInfo
|
||||
|
@ -74,6 +74,9 @@ data RQLQuery
|
||||
| RQCount !CountQuery
|
||||
| RQBulk ![RQLQuery]
|
||||
|
||||
| RQCreateEventTrigger !CreateEventTriggerQuery
|
||||
| RQDeleteEventTrigger !DeleteEventTriggerQuery
|
||||
|
||||
| RQCreateQueryTemplate !CreateQueryTemplate
|
||||
| RQDropQueryTemplate !DropQueryTemplate
|
||||
| RQExecuteQueryTemplate !ExecQueryTemplate
|
||||
@ -168,6 +171,9 @@ queryNeedsReload qi = case qi of
|
||||
RQDelete q -> queryModifiesSchema q
|
||||
RQCount q -> queryModifiesSchema q
|
||||
|
||||
RQCreateEventTrigger q -> queryModifiesSchema q
|
||||
RQDeleteEventTrigger q -> queryModifiesSchema q
|
||||
|
||||
RQCreateQueryTemplate q -> queryModifiesSchema q
|
||||
RQDropQueryTemplate q -> queryModifiesSchema q
|
||||
RQExecuteQueryTemplate q -> queryModifiesSchema q
|
||||
@ -214,6 +220,9 @@ buildTxAny userInfo sc rq = case rq of
|
||||
RQDelete q -> buildTx userInfo sc q
|
||||
RQCount q -> buildTx userInfo sc q
|
||||
|
||||
RQCreateEventTrigger q -> buildTx userInfo sc q
|
||||
RQDeleteEventTrigger q -> buildTx userInfo sc q
|
||||
|
||||
RQCreateQueryTemplate q -> buildTx userInfo sc q
|
||||
RQDropQueryTemplate q -> buildTx userInfo sc q
|
||||
RQExecuteQueryTemplate q -> buildTx userInfo sc q
|
||||
|
@ -179,3 +179,68 @@ args:
|
||||
args:
|
||||
schema: hdb_catalog
|
||||
name: hdb_query_template
|
||||
|
||||
- type: add_existing_table_or_view
|
||||
args:
|
||||
schema: hdb_catalog
|
||||
name: event_triggers
|
||||
|
||||
- type: add_existing_table_or_view
|
||||
args:
|
||||
schema: hdb_catalog
|
||||
name: event_log
|
||||
|
||||
- type: add_existing_table_or_view
|
||||
args:
|
||||
schema: hdb_catalog
|
||||
name: event_invocation_logs
|
||||
|
||||
- type: create_object_relationship
|
||||
args:
|
||||
name: trigger
|
||||
table:
|
||||
schema: hdb_catalog
|
||||
name: event_log
|
||||
using:
|
||||
manual_configuration:
|
||||
remote_table:
|
||||
schema: hdb_catalog
|
||||
name: event_triggers
|
||||
column_mapping:
|
||||
trigger_id : id
|
||||
|
||||
- type: create_array_relationship
|
||||
args:
|
||||
name: events
|
||||
table:
|
||||
schema: hdb_catalog
|
||||
name: event_triggers
|
||||
using:
|
||||
manual_configuration:
|
||||
remote_table:
|
||||
schema: hdb_catalog
|
||||
name: event_log
|
||||
column_mapping:
|
||||
id : trigger_id
|
||||
|
||||
- type: create_object_relationship
|
||||
args:
|
||||
name: event
|
||||
table:
|
||||
schema: hdb_catalog
|
||||
name: event_invocation_logs
|
||||
using:
|
||||
foreign_key_constraint_on: event_id
|
||||
|
||||
- type: create_array_relationship
|
||||
args:
|
||||
name: logs
|
||||
table:
|
||||
schema: hdb_catalog
|
||||
name: event_log
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
table:
|
||||
schema: hdb_catalog
|
||||
name: event_invocation_logs
|
||||
column: event_id
|
||||
|
@ -179,3 +179,48 @@ LANGUAGE plpgsql AS $$
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- required for generating uuid
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
CREATE TABLE hdb_catalog.event_triggers
|
||||
(
|
||||
id TEXT DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
name TEXT UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
definition JSON,
|
||||
query TEXT,
|
||||
webhook TEXT NOT NULL,
|
||||
num_retries INTEGER DEFAULT 0,
|
||||
retry_interval INTEGER DEFAULT 10,
|
||||
comment TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE hdb_catalog.event_log
|
||||
(
|
||||
id TEXT DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
trigger_id TEXT NOT NULL,
|
||||
trigger_name TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
delivered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
error BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
tries INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
locked BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE hdb_catalog.event_invocation_logs
|
||||
(
|
||||
id TEXT DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
event_id TEXT,
|
||||
status INTEGER,
|
||||
request JSON,
|
||||
response JSON,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
FOREIGN KEY (event_id) REFERENCES hdb_catalog.event_log (id)
|
||||
);
|
||||
|
26
server/src-rsr/trigger.sql.j2
Normal file
26
server/src-rsr/trigger.sql.j2
Normal file
@ -0,0 +1,26 @@
|
||||
CREATE OR REPLACE function hdb_views.notify_hasura_{{NAME}}_{{OPERATION}}() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
payload json;
|
||||
_data json;
|
||||
id text;
|
||||
BEGIN
|
||||
id := gen_random_uuid();
|
||||
_data := json_build_object(
|
||||
'old', {{OLD_DATA_EXPRESSION}},
|
||||
'new', {{NEW_DATA_EXPRESSION}}
|
||||
);
|
||||
payload := json_build_object(
|
||||
'op', TG_OP,
|
||||
'data', _data
|
||||
)::text;
|
||||
INSERT INTO
|
||||
hdb_catalog.event_log (id, schema_name, table_name, trigger_name, trigger_id, payload)
|
||||
VALUES
|
||||
(id, TG_TABLE_SCHEMA, TG_TABLE_NAME, '{{NAME}}', '{{ID}}', payload);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
DROP TRIGGER IF EXISTS notify_hasura_{{NAME}}_{{OPERATION}} ON {{SCHEMA_NAME}}.{{TABLE_NAME}};
|
||||
CREATE TRIGGER notify_hasura_{{NAME}}_{{OPERATION}} AFTER {{OPERATION}} ON {{SCHEMA_NAME}}.{{TABLE_NAME}} FOR EACH ROW EXECUTE PROCEDURE hdb_views.notify_hasura_{{NAME}}_{{OPERATION}}();
|
@ -19,6 +19,8 @@ extra-deps:
|
||||
commit: e61bc37794b4d9e281bad44b2d7c8d35f2dbc770
|
||||
- git: https://github.com/hasura/graphql-parser-hs.git
|
||||
commit: eae59812ec537b3756c3ddb5f59a7cc59508869b
|
||||
- git: https://github.com/tdammers/ginger.git
|
||||
commit: 435c2774963050da04ce9a3369755beac87fbb16
|
||||
- Spock-core-0.13.0.0
|
||||
- reroute-0.5.0.0
|
||||
|
||||
|
@ -46,7 +46,9 @@ ravenApp loggerCtx pool = do
|
||||
let corsCfg = CorsConfigG "*" False -- cors is enabled
|
||||
httpManager <- HTTP.newManager HTTP.tlsManagerSettings
|
||||
-- spockAsApp $ spockT id $ app Q.Serializable Nothing rlogger pool AMNoAuth corsCfg True -- no access key and no webhook
|
||||
mkWaiApp Q.Serializable Nothing loggerCtx pool httpManager AMNoAuth corsCfg True -- no access key and no webhook
|
||||
(app, _) <- mkWaiApp Q.Serializable Nothing loggerCtx pool httpManager AMNoAuth corsCfg True -- no access key and no webhook
|
||||
return app
|
||||
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
@ -63,7 +65,7 @@ main = do
|
||||
liftIO $ initialise pool
|
||||
-- generate the test specs
|
||||
specs <- mkSpecs
|
||||
loggerCtx <- L.mkLoggerCtx L.defaultLoggerSettings
|
||||
loggerCtx <- L.mkLoggerCtx $ L.defaultLoggerSettings True
|
||||
-- run the tests
|
||||
withArgs [] $ hspecWith defaultConfig $ with (ravenApp loggerCtx pool) specs
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user