Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c8c0b28
set update scheduled query payload correctly
briandennis Aug 7, 2018
50ce572
update backend to correctly set names and add/remove columns
briandennis Aug 7, 2018
df31eec
fix updateGrid signature in tests
briandennis Aug 7, 2018
81381f2
update scheduler tests for new updateGrid API
mfix22 Aug 7, 2018
9c014ce
fix updateGrid to GET source or truth
mfix22 Aug 8, 2018
5d3b558
if GET is not 200 in updateGrid, return the response
briandennis Aug 8, 2018
dbdd654
omit unnecessary info in tests
briandennis Aug 8, 2018
51ea8cc
remove uid parameters where possible, maintain falcon specific query …
briandennis Aug 8, 2018
61749cb
call scheduled query job with correct parameters
briandennis Aug 8, 2018
a3037b6
move DELETE before PUT to minimize risk of column naming collision
briandennis Aug 8, 2018
02ab47c
add uid tests for updateGrid
briandennis Aug 8, 2018
1a2bfd5
GET /grids/:fid/col
mfix22 Aug 8, 2018
dc40912
check for status as the end of `queryAndUpdateGrid`
mfix22 Aug 8, 2018
9049db3
fix syncing issue after updating query cronInterval
mfix22 Aug 8, 2018
b4f4f0a
extract getGridColumn into API function
mfix22 Aug 8, 2018
9351cc9
rename preview-modal.jsx -> preview-modal.test.jsx
mfix22 Aug 8, 2018
a4cf0d4
revert removing refreshInterval from test
mfix22 Aug 8, 2018
400b5c2
clean up SuccessMessage, show datasetUrl in create-modal
jakedex Aug 8, 2018
75fd5fc
clean up routes.queries.spec
mfix22 Aug 8, 2018
005702a
remove omit from routes.queries
mfix22 Aug 8, 2018
7a74837
keep routes.spec as close to master as possible
mfix22 Aug 8, 2018
ee413c4
throw non 200's in QueryScheduler
mfix22 Aug 8, 2018
c526883
confirm res and res.status are not undefined
mfix22 Aug 8, 2018
8f1d1dd
remove uids references from routes.queries.spec POST bodys
mfix22 Aug 9, 2018
c39a0fd
disable collaborator query update test
briandennis Aug 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/actions/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function updateScheduledQuery(connectionId, payload = {}) {
if (!res.error) {
dispatch({
type: 'UPDATE_SCHEDULED_QUERY',
payload: body
payload: res
});
}
return res;
Expand Down
19 changes: 15 additions & 4 deletions app/components/Settings/scheduler/create-modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import cronstrue from 'cronstrue';

import {Row, Column} from '../../layout.jsx';
import Modal from '../../modal.jsx';
import {Link} from '../../Link.react';
import SuccessMessage from '../../success.jsx';
import RequestError from './request-error.jsx';
import TimedMessage from './timed-message.jsx';
import CronPicker from '../cron-picker/cron-picker.jsx';
import SQL from './sql.jsx';

import {datasetUrl as getDatasetURL} from '../../../utils/utils';
import {getHighlightMode, WAITING_MESSAGE, SAVE_WARNING} from '../../../constants/constants.js';

import './create-modal.css';
Expand Down Expand Up @@ -62,7 +64,8 @@ class CreateModal extends Component {
name: props.initialName,
interval: '*/5 * * * *',
error: null,
saving: false
saving: false,
datasetUrl: null
};
this.options = {
lineWrapping: true,
Expand Down Expand Up @@ -108,8 +111,12 @@ class CreateModal extends Component {
cronInterval: this.state.interval,
name: this.state.name ? this.state.name.trim() : ''
})
.then(() => {
this.setState({successMessage: 'Scheduled query saved successfully!', saving: false});
.then((res) => {
this.setState({
successMessage: 'Scheduled query saved successfully!',
saving: false,
datasetUrl: getDatasetURL(res.fid)
});
})
.catch(error => this.setState({error: error.message, saving: false}));
}
Expand Down Expand Up @@ -193,7 +200,11 @@ class CreateModal extends Component {
</Column>
<Row>
{this.state.successMessage ? (
<SuccessMessage>{this.state.successMessage}</SuccessMessage>
<Column style={{ padding: '0 32px 16px' }}>
<SuccessMessage message={this.state.successMessage}>
<Link href={this.state.datasetUrl}>View Live Dataset</Link>
</SuccessMessage>
</Column>
) : (
<Column>
<button type="submit" className="submit" onClick={this.submit}>
Expand Down
13 changes: 8 additions & 5 deletions app/components/Settings/scheduler/preview-modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Link} from '../../Link.react.js';
import CronPicker from '../cron-picker/cron-picker.jsx';
import {Row, Column} from '../../layout.jsx';
import SQL from './sql.jsx';
import {plotlyUrl} from '../../../utils/utils.js';
import {datasetUrl} from '../../../utils/utils.js';
import {getHighlightMode, WAITING_MESSAGE, SAVE_WARNING} from '../../../constants/constants.js';
import {getInitialCronMode} from '../cron-picker/cron-helpers.js';

Expand Down Expand Up @@ -156,7 +156,11 @@ export class PreviewModal extends Component {
}

if (success) {
return <SuccessMessage>{this.state.successMessage}</SuccessMessage>;
return (
<Column>
<SuccessMessage>{this.state.successMessage}</SuccessMessage>
</Column>
);
}

if (editing) {
Expand Down Expand Up @@ -196,8 +200,7 @@ export class PreviewModal extends Component {
if (!props.query) {
content = null;
} else {
const [account, gridId] = props.query.fid.split(':');
const link = `${plotlyUrl()}/~${account}/${gridId}`;
const link = datasetUrl(props.query.fid);
const {editing, loading} = this.state;

const initialModeId = getInitialCronMode(props.query);
Expand Down Expand Up @@ -308,7 +311,7 @@ export class PreviewModal extends Component {
</div>
) : (
<em style={valueStyle}>
{props.query.cronInterval ? (
{this.state.cronInterval ? (
<b>{cronstrue.toString(this.state.cronInterval)}</b>
) : (
<React.Fragment>
Expand Down
10 changes: 6 additions & 4 deletions app/components/success.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';

const style = {marginBottom: 16, fontSize: 16, color: '#00cc96'};
const style = {fontSize: 16};
const SuccessMessage = (props) => (
<em style={style}>
<div style={{ padding: '8px 16px', borderLeft: '4px solid #00cc96', background: 'white'}}>
<div style={style}>{props.message}</div>
{props.children}
</em>
</div>
);

SuccessMessage.propTypes = {
children: PropTypes.node
children: PropTypes.node,
message: PropTypes.string
};

export default SuccessMessage;
5 changes: 5 additions & 0 deletions app/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export function homeUrl() {
'';
}

export function datasetUrl(fid) {
const [account, gridId] = fid.split(':');
return `${plotlyUrl()}/~${account}/${gridId}`;
}

export function getPathNames(url) {
const parser = document.createElement('a');
parser.href = url;
Expand Down
50 changes: 36 additions & 14 deletions backend/persistent/QueryScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import {
getGridMeta,
newGrid,
patchGrid,
updateGrid
updateGrid,
getGridColumn
} from './plotly-api.js';

const SUCCESS_CODES = [200, 201, 204];

class QueryScheduler {
constructor() {
this.scheduleQuery = this.scheduleQuery.bind(this);
Expand All @@ -34,15 +37,14 @@ class QueryScheduler {

// this.job wraps this.queryAndUpdateGrid to avoid concurrent runs of the same job
this.runningJobs = {};
this.job = (fid, uids, query, connectionId, requestor) => {
this.job = (fid, query, connectionId, requestor, cronInterval, refreshInterval) => {
try {
if (this.runningJobs[fid]) {
return;
}

this.runningJobs[fid] = true;

return this.queryAndUpdateGrid(fid, uids, query, connectionId, requestor)
return this.queryAndUpdateGrid(fid, query, connectionId, requestor, cronInterval, refreshInterval)
.catch(error => {
Logger.log(error, 0);
}).then(() => {
Expand Down Expand Up @@ -113,7 +115,7 @@ class QueryScheduler {
saveQuery(queryParams);

// Schedule
const job = () => this.job(fid, uids, query, connectionId, requestor);
const job = () => this.job(fid, query, connectionId, requestor, cronInterval, refreshInterval);
let jobInterval = cronInterval;
if (!jobInterval) {
// convert refresh interval to cron representation
Expand Down Expand Up @@ -236,7 +238,7 @@ class QueryScheduler {
});
}

queryAndUpdateGrid(fid, uids, query, connectionId, requestor, cronInterval, refreshInterval) {
queryAndUpdateGrid(fid, query, connectionId, requestor, cronInterval, refreshInterval) {
const requestedDBConnections = getConnectionById(connectionId);
const formattedRefresh = refreshInterval || mapCronToRefresh(cronInterval);
let startTime = process.hrtime();
Expand Down Expand Up @@ -281,8 +283,7 @@ class QueryScheduler {
*/
throw new Error(`QueryExecutionError: ${e.message}`);
});
}).then(({rows}) => {

}).then(({rows, columnnames}) => {
Logger.log(`Query "${query}" took ${process.hrtime(startTime)[0]} seconds`, 2);
Logger.log(`Updating grid ${fid} with new data`, 2);
Logger.log(
Expand All @@ -294,8 +295,8 @@ class QueryScheduler {

return updateGrid(
rows,
columnnames,
fid,
uids,
requestor
).catch((e) => {
/*
Expand All @@ -305,7 +306,7 @@ class QueryScheduler {
});
}).then(res => {
Logger.log(`Request to Plotly for grid ${fid} took ${process.hrtime(startTime)[0]} seconds`, 2);
if (res.status !== 200) {
if (!SUCCESS_CODES.includes(res.status)) {
Logger.log(`Error ${res.status} while updating grid ${fid}.`, 2);

/*
Expand Down Expand Up @@ -357,6 +358,11 @@ class QueryScheduler {
this.clearQuery(fid);
return deleteQuery(fid);
}
/*
* Warning: The front end looks for "PlotlyApiError" in this error message. Don't change it!
*/
throw new Error(`PlotlyApiError: non 200 grid update response code (got: ${res.status})`);

});
});

Expand All @@ -381,11 +387,27 @@ class QueryScheduler {
*/
throw new Error(`MetadataError: ${e.message}`);
});
}).then((res) => {
}).then(res => {
Logger.log(`Request to Plotly for creating a grid took ${process.hrtime(startTime)[0]} seconds`, 2);
return res.json().then(() => {
Logger.log(`Grid ${fid} has been updated.`, 2);
});
Logger.log(`Grid ${fid} has been updated.`, 2);

// res is undefined if the `deleteQuery` is returned above
if (res && res.status && res.status !== 200) {
throw new Error(`MetadataError: error updating grid metadata (status: ${res.status})`);
}

startTime = process.hrtime();

// fetch updated grid column for returning
return getGridColumn(fid, requestor);
}).then(res => {
Logger.log(`Request to Plotly for fetching updated grid took ${process.hrtime(startTime)[0]} seconds`, 2);

if (res.status !== 200) {
throw new Error(`PlotlyApiError: error fetching updated columns (status: ${res.status})`);
}

return res.json();
});
}
}
Expand Down
122 changes: 95 additions & 27 deletions backend/persistent/plotly-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getCredentials,
getSetting
} from '../settings.js';
import {extractOrderedUids} from '../utils/gridUtils.js';


// Module to access Plot.ly REST API
Expand Down Expand Up @@ -164,36 +165,103 @@ export function patchGrid(fid, requestor, body) {
});
}

export function updateGrid(rows, fid, uids, requestor) {
export function getGridColumn(fid, requestor) {
const {username, apiKey, accessToken} = getCredentials(requestor);

// TODO - Test case where no rows are returned.
if (uids.length !== (rows[0] || []).length) {
Logger.log(`
A different number of columns was returned in the
query than what was initially saved in the grid.
${rows[0].length} columns were queried,
${uids.length} columns were originally saved.
The connector does not create columns (yet),
and so we will only update the first ${uids.length}
columns.
`);
}
return plotlyAPIRequest(`grids/${fid}/col`, {
method: 'GET',
username,
apiKey,
accessToken
});
}

/*
* For now, only update up to the number of columns that are
* already saved. In the future, we should just create more
* columns. See error message above.
*/
const columns = getColumns(rows, uids.length);

const url = `grids/${fid}/col?uid=${uids.join(',')}`;
const body = {
cols: JSON.stringify(columns.map(column => ({
data: column
})))
};
return plotlyAPIRequest(url, {body, username, apiKey, accessToken, method: 'PUT'});
export function updateGrid(rows, columnnames, fid, requestor) {
const numColumns = columnnames.length;
const columns = getColumns(rows, numColumns);
const columnEntries = columns.map((column, columnIndex) => {
return { data: column, name: columnnames[columnIndex], order: columnIndex };
});

const {username, apiKey, accessToken} = getCredentials(requestor);
const baseUrl = `grids/${fid}/col`;
const baseParams = { username, apiKey, accessToken };

// fetch latest grid to get the source of truth
return getGridColumn(fid, requestor)
.then(res => {
if (res.status !== 200) {
return res;
}
return res.json();
}).then(data => {
if (data.status) {
Copy link
Contributor

@n-riesco n-riesco Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elsewhere we throw an error, why not here?
This is done so that it behaves like plotlyAPIRequest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mfix22 Judging by how we use updateGrid here, we shouldn't throw.

// bad res was passed along, return it
return data;
}

const uids = extractOrderedUids(data);
Copy link
Contributor

@n-riesco n-riesco Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function orders uids by order.
Is this what we want?
Changing a query from select a,b to select b,a would break a plot.


Please, ignore this comment. I've checked and this is already the behaviour in master.


if (numColumns > uids.length) {
// repopulate existing columns
const putUrl = `${baseUrl}?uid=${uids.join(',')}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to slice uids here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, UIDs are shorter in length than numColumns. Not sure what we would slice it to.

const putBody = { cols: columnEntries.slice(0, uids.length) };

// append new columns
const postUrl = baseUrl;
const postBody = { cols: columnEntries.slice(uids.length) };

return plotlyAPIRequest(postUrl, {
...baseParams,
body: postBody,
method: 'POST'
}).then((res) => {
if (res.status !== 200) {
return res;
Copy link
Contributor

@n-riesco n-riesco Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw?

}

return plotlyAPIRequest(putUrl, {
...baseParams,
body: putBody,
method: 'PUT'
});
});
} else if (numColumns < uids.length) {
// delete unused existing columns
const deleteUrl = `${baseUrl}?uid=${uids.slice(numColumns)}`;

// repopulate used existing columns
const putUrl = `${baseUrl}?uid=${uids.slice(0, numColumns).join(',')}`;
const putBody = { cols: columnEntries };



return plotlyAPIRequest(deleteUrl, {
...baseParams,
method: 'DELETE'
}).then((res) => {
if (res.status !== 204) {
return res;
}

return plotlyAPIRequest(putUrl, {
...baseParams,
body: putBody,
method: 'PUT'
});
});
}

// repopulate existing columns
const putUrl = `${baseUrl}?uid=${uids.join(',')}`;
const putBody = { cols: columnEntries };

return plotlyAPIRequest(putUrl, {
...baseParams,
body: putBody,
method: 'PUT'
});
});
}

function getColumns(rows, maxColumns) {
Expand Down
Loading