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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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) {

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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 updateGridhere, we shouldn't throw.

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

const uids = extractOrderedUids(data);

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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;

@n-riescon-riescoAug 8, 2018

Copy link
Copy Markdown
Contributor

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