From 498420c042490870b6669819ddfb8a1047cd2627 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Tue, 3 Jul 2018 17:27:09 -0700 Subject: [PATCH 01/24] Scheduled query browser (Stage 2) --- app/actions/sessions.js | 64 ++++ .../Settings/Preview/Preview.react.js | 63 ++-- .../Settings/Preview/code-editor.jsx | 13 +- .../Settings/Scheduler/createModal.jsx | 233 ++++++++++++++ .../Settings/Scheduler/loginModal.jsx | 60 ++++ .../Settings/Scheduler/previewModal.jsx | 248 +++++++++++++++ .../Settings/Scheduler/scheduler.css | 56 ++++ .../Settings/Scheduler/scheduler.jsx | 288 ++++++++++++++++++ app/components/Settings/Scheduler/sql.jsx | 14 + app/components/Settings/Scheduler/util.js | 14 + app/components/Settings/Settings.react.js | 68 ++++- app/components/modal.jsx | 38 ++- app/reducers/index.js | 21 ++ static/css/Preview.css | 19 ++ .../Settings/Scheduler/createModal.jsx | 99 ++++++ .../Settings/Scheduler/scheduler.test.jsx | 86 ++++++ 16 files changed, 1330 insertions(+), 54 deletions(-) create mode 100644 app/components/Settings/Scheduler/createModal.jsx create mode 100644 app/components/Settings/Scheduler/loginModal.jsx create mode 100644 app/components/Settings/Scheduler/previewModal.jsx create mode 100644 app/components/Settings/Scheduler/scheduler.css create mode 100644 app/components/Settings/Scheduler/scheduler.jsx create mode 100644 app/components/Settings/Scheduler/sql.jsx create mode 100644 app/components/Settings/Scheduler/util.js create mode 100644 test/app/components/Settings/Scheduler/createModal.jsx create mode 100644 test/app/components/Settings/Scheduler/scheduler.test.jsx diff --git a/app/actions/sessions.js b/app/actions/sessions.js index 970b22f55..1c9738d23 100644 --- a/app/actions/sessions.js +++ b/app/actions/sessions.js @@ -10,6 +10,9 @@ export const setTab = createAction('SET_TAB'); export const setTable = createAction('SET_TABLE'); export const setIndex = createAction('SET_INDEX'); export const setScheduledQueries = createAction('SET_SCHEDULED_QUERIES'); +export const createScheduledQueryAction = createAction('CREATE_SCHEDULED_QUERY'); +export const updateScheduledQueryAction = createAction('UPDATE_SCHEDULED_QUERY'); +export const deleteScheduledQueryAction = createAction('DELETE_SCHEDULED_QUERY'); export const mergeConnections = createAction('MERGE_CONNECTIONS'); export const updateConnection = createAction('UPDATE_CREDENTIAL'); export const deleteConnection = createAction('DELETE_CREDENTIAL'); @@ -151,6 +154,67 @@ export function getScheduledQueries() { }; } +export function createScheduledQuery(connectionId, payload = {}) { + return (dispatch) => { + return dispatch(apiThunk( + 'queries', + 'POST', + 'createScheduledQueryRequest', + payload.filename, + { + requestor: payload.requestor, + uids: payload.uids, + fid: payload.fid, + filename: payload.filename, + refreshInterval: payload.refreshInterval, + query: payload.query, + connectionId + } + )).then((res) => { + dispatch(createScheduledQueryAction(res)); + return res; + }); + }; +} + +export function updateScheduledQuery(connectionId, payload = {}) { + return (dispatch) => { + const body = { + requestor: payload.requestor, + uids: payload.uids, + fid: payload.fid, + filename: payload.filename, + refreshInterval: payload.refreshInterval, + query: payload.query, + connectionId + }; + + return dispatch(apiThunk( + 'queries', + 'POST', + 'createScheduledQueryRequest', + payload.filename, + body + )).then((res) => { + dispatch(updateScheduledQueryAction(body)); + return res; + }); + }; +} + +export function deleteScheduledQuery(fid) { + return (dispatch) => { + return dispatch(apiThunk( + `queries/${fid}`, + 'DELETE', + 'createScheduledQueryRequest' + )).then((res) => { + dispatch(deleteScheduledQueryAction(fid)); + return res; + }); + }; +} + export function connect(connectionId) { return apiThunk( `connections/${connectionId}/connect`, diff --git a/app/components/Settings/Preview/Preview.react.js b/app/components/Settings/Preview/Preview.react.js index 7944220a7..5f3b415a6 100644 --- a/app/components/Settings/Preview/Preview.react.js +++ b/app/components/Settings/Preview/Preview.react.js @@ -38,6 +38,7 @@ class Preview extends Component { getSqlSchema: PropTypes.func, runSqlQuery: PropTypes.func, + openScheduler: PropTypes.func, previewTableRequest: PropTypes.object, queryRequest: PropTypes.object, elasticsearchMappingsRequest: PropTypes.object, @@ -347,6 +348,7 @@ class Preview extends Component { dialect={dialect} runQuery={this.runQuery} + scheduleQuery={this.props.openScheduler} schemaRequest={schemaRequest} isLoading={isLoading} /> @@ -454,32 +456,41 @@ class Preview extends Component { -
-
- - - {!isOnPrem() && - } +
+
CHART STUDIO
+
+ + + +
+
MY COMPUTER
+
+ {!isOnPrem() && + }
+ {isLoading ? 'Loading...' : 'Run'} diff --git a/app/components/Settings/Scheduler/createModal.jsx b/app/components/Settings/Scheduler/createModal.jsx new file mode 100644 index 000000000..22656ac03 --- /dev/null +++ b/app/components/Settings/Scheduler/createModal.jsx @@ -0,0 +1,233 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +import Select from 'react-select'; +import { Controlled as CodeMirror } from 'react-codemirror2'; + +import { Row, Column } from '../../layout.jsx'; +import Modal from '../../modal.jsx'; +import { mapDialect } from './util.js'; + +function noop() {} + +const FREQUENCIES = [ + { label: 'Run every minute', value: 60 }, + { label: 'Run every 5 minutes', value: 5 * 60 }, + { label: 'Run hourly', value: 60 * 60 }, + { label: 'Run daily', value: 24 * 60 * 60 }, + { label: 'Run weekly', value: 7 * 24 * 60 * 60 } +]; + +const styles = { + column: { width: '60%', background: '#F5F7FB' }, + innerColumn: { background: '#fff', position: 'relative' }, + header: { marginBottom: '16px', padding: '0 32px' }, + button: { + position: 'absolute', + top: '16px', + right: '16px', + padding: '2px 4px' + }, + detailsColumn: { padding: '0 32px' }, + p: { margin: '32px 0', padding: '16px' }, + submit: { + width: '100%', + margin: '24px 32px 32px' + }, + row: { justifyContent: 'flex-start' }, + input: { + margin: '0 0 16px', + width: '70%' + }, + dropdown: { + padding: 0, + marginBottom: '16px', + width: '100%', + maxWidth: '432px' + } +}; + +const rowStyles = { + header: { width: '20%' }, + body: { width: '80%' } +}; + +const Error = props =>
{props.message}
; +Error.propTypes = { + message: PropTypes.string.isRequired +}; + +export const FrequencySelector = props => ( + +
+ + +
Frequency
+
+
+ +
+
+
+ {this.state.error && ( + + + + )} + + + + + + + ); + } +} + +export default CreateModal; diff --git a/app/components/Settings/Scheduler/loginModal.jsx b/app/components/Settings/Scheduler/loginModal.jsx new file mode 100644 index 000000000..1fce58c8f --- /dev/null +++ b/app/components/Settings/Scheduler/loginModal.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Row, Column } from '../../layout.jsx'; +import Modal from '../../modal.jsx'; + +const styles = { + column: { width: '400px', background: '#F5F7FB', position: 'relative' }, + header: { marginBottom: '16px', padding: '0 32px' }, + button: { + position: 'absolute', + top: '16px', + right: '16px', + padding: '2px 4px' + }, + detailsColumn: { padding: '0 32px' }, + p: { margin: '56px 0 8px', padding: '16px' }, + submit: { + width: '100%', + margin: '24px 32px 32px' + }, + row: { justifyContent: 'flex-start' } +}; + +const PromptLoginModal = props => ( + + + + +

+ To create a scheduled query, you'll need to be logged into + Plotly. +

+
+ + + +
+
+); + +PromptLoginModal.propTypes = { + open: PropTypes.bool.isRequired, + onClickAway: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired +}; + +export default PromptLoginModal; diff --git a/app/components/Settings/Scheduler/previewModal.jsx b/app/components/Settings/Scheduler/previewModal.jsx new file mode 100644 index 000000000..eb69c41ec --- /dev/null +++ b/app/components/Settings/Scheduler/previewModal.jsx @@ -0,0 +1,248 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; +import { Controlled as CodeMirror } from 'react-codemirror2'; +import ms from 'ms'; + +import Modal from '../../modal.jsx'; +import { Link } from '../../Link.react.js'; +import { FrequencySelector } from './createModal.jsx'; +import { Row, Column } from '../../layout.jsx'; +import SQL from './sql.jsx'; +import { plotlyUrl } from '../../../utils/utils.js'; +import { mapDialect } from './util.js'; + +const NO_OP = () => {}; + +const rowStyle = { + justifyContent: 'flex-start', + borderBottom: '1px solid rgba(0, 0, 0, 0.05)', + padding: '16px 0px' +}; +const boxStyle = { boxSizing: 'border-box', width: '50%' }; + +export class PreviewModal extends Component { + static defaultProps = { + onSave: NO_OP, + onDelete: NO_OP + }; + static propTypes = { + query: PropTypes.object, + onSave: PropTypes.func, + onDelete: PropTypes.func, + onClickAway: PropTypes.func, + dialect: PropTypes.string + }; + constructor(props) { + super(props); + this.state = { + editing: false, + code: props.query && props.query.query, + refreshInterval: props.query && props.query.refreshInterval, + confirmedDelete: false + }; + this.toggleEditing = this.toggleEditing.bind(this); + this.updateCode = this.updateCode.bind(this); + this.updateRefreshInterval = this.updateRefreshInterval.bind(this); + this.onSubmit = this.onSubmit.bind(this); + this.onDelete = this.onDelete.bind(this); + this.close = this.close.bind(this); + } + + componentDidUpdate(prevProps) { + if ( + this.props.query && + this.props.query.query !== + (prevProps.query && prevProps.query.query) + ) { + // eslint-disable-next-line + this.setState({ + code: this.props.query.query, + refreshInterval: this.props.query.refreshInterval + }); + } + } + + updateCode(editor, meta, code) { + this.setState({ code }); + } + + updateRefreshInterval({ value: refreshInterval }) { + this.setState({ refreshInterval }); + } + + toggleEditing() { + this.setState(({ editing }) => ({ editing: !editing })); + } + + onSubmit() { + if (this.state.editing) { + const { connectionId, fid, requestor, uids } = this.props.query; + const { code: query, refreshInterval } = this.state; + this.props.onSave({ + connectionId, + fid, + requestor, + uids, + query, + refreshInterval + }); + this.setState({ editing: false, confirmedDelete: false }); + } else { + this.toggleEditing(); + } + } + + onDelete() { + if (this.state.confirmedDelete) { + this.setState({ confirmedDelete: false }, () => { + this.props.onDelete(this.props.query.fid); + }); + } else { + this.setState({ confirmedDelete: true }); + } + } + + close() { + this.setState({ confirmedDelete: false }); + this.props.onClickAway(); + } + + render() { + const props = this.props; + + let content; + if (!props.query) { + content = null; + } else { + const [account, gridId] = props.query.fid.split(':'); + const link = `${plotlyUrl()}/~${account}/${gridId}`; + const editing = this.state.editing; + content = ( + + +
+ {props.query.query} +
+ +
+ + +
Query
+
+ {editing ? ( +
+ +
+ ) : ( + {props.query.query} + )} +
+
+ +
Update Frequency
+ {editing ? ( +
+ +
+ ) : ( + + Runs every{' '} + + {ms( + props.query.refreshInterval * 1000, + { + long: true + } + )} + + + )} +
+ +
Live Dataset
+ + {link} + +
+ + + {!editing && ( + + )} + +
+
+ ); + } + + return ( + + {content} + + ); + } +} + +export default PreviewModal; diff --git a/app/components/Settings/Scheduler/scheduler.css b/app/components/Settings/Scheduler/scheduler.css new file mode 100644 index 000000000..b58772ebf --- /dev/null +++ b/app/components/Settings/Scheduler/scheduler.css @@ -0,0 +1,56 @@ +/* SQL token highlighting */ +.default .hljs-keyword, +.default .hljs-selector-tag { + color: #ab63fa; +} + +.default .hljs-number, +.default .hljs-meta, +.default .hljs-built_in, +.default .hljs-builtin-name, +.default .hljs-literal, +.default .hljs-type, +.default .hljs-params { + color: #00cc96; +} + +.default .hljs-string, +.default .hljs-symbol, +.default .hljs-bullet { + color: #119DFF; +} + +.default .hljs { + color: #585260; +} + +.bold .hljs-keyword, +.bold .hljs-selector-tag { + font-weight: bold; +} + +/* custom ReactDataGrid styles for scheduled queries table */ +.scheduler-table .react-grid-Row--odd .react-grid-Cell { + background: #fff; +} + +.scheduler-table .react-grid-Row--odd:hover .react-grid-Cell { + background-color: #f9f9f9; +} + +.scheduler-table .react-grid-Row { + border-top: 1px solid #c8d4e3; + cursor: pointer; +} + +.scheduler-table .react-grid-Row:first-child { + border-top: none; +} + +.scheduler .CodeMirror { + width: 100%; +} + +.sql-preview pre { + white-space: pre-wrap; +} diff --git a/app/components/Settings/Scheduler/scheduler.jsx b/app/components/Settings/Scheduler/scheduler.jsx new file mode 100644 index 000000000..9ddfe12c7 --- /dev/null +++ b/app/components/Settings/Scheduler/scheduler.jsx @@ -0,0 +1,288 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +import ReactDataGrid from 'react-data-grid'; +import ms from 'ms'; +import matchSorter from 'match-sorter'; + +import CreateModal from './createModal.jsx'; +import PreviewModal from './previewModal.jsx'; +import PromptLoginModal from './loginModal.jsx'; +import { Row, Column } from '../../layout.jsx'; +import SQL from './sql.jsx'; + +import './scheduler.css'; + +const NO_OP = () => {}; + +class QueryFormatter extends React.Component { + static propTypes = { + /* + * Object passed by `react-data-grid` to each row. Here the value + * is an object containg the required `query` string. + */ + value: PropTypes.shape({ + query: PropTypes.string.isRequired + }) + }; + + render() { + const query = this.props.value; + return ( + + + {query.query} + + + ); + } +} + +class IntervalFormatter extends React.Component { + static propTypes = { + /* + * Object passed by `react-data-grid` to each row. Here the value + * is an object containg the required `refreshInterval`. + */ + value: PropTypes.shape({ + refreshInterval: PropTypes.number.isRequired + }) + }; + + render() { + const run = this.props.value; + return ( + + + + {`Runs every ${ms(run.refreshInterval * 1000, { + long: true + })}`} + + + + ); + } +} + +function mapRows(rows) { + return rows.map(r => ({ + query: r, + run: r + })); +} + +class Scheduler extends Component { + static defaultProps = { + queries: [], + refreshQueries: NO_OP, + openLogin: NO_OP, + createScheduledQuery: NO_OP, + updateScheduledQuery: NO_OP, + deleteScheduledQuery: NO_OP + }; + + static propTypes = { + queries: PropTypes.arrayOf( + PropTypes.shape({ + query: PropTypes.string.isRequired, + refreshInterval: PropTypes.number.isRequired, + fid: PropTypes.string.isRequired + }).isRequired + ), + initialCode: PropTypes.string, + requestor: PropTypes.string, + dialect: PropTypes.string, + refreshQueries: PropTypes.func.isRequired, + openLogin: PropTypes.func.isRequired, + createScheduledQuery: PropTypes.func.isRequired, + updateScheduledQuery: PropTypes.func.isRequired, + deleteScheduledQuery: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.state = { + search: '', + selectedQuery: null, + createModalOpen: Boolean(this.props.initialCode) + }; + this.columns = [ + { + key: 'query', + name: 'Query', + filterable: true, + formatter: QueryFormatter + }, + { + key: 'run', + name: 'Interval', + filterable: true, + formatter: IntervalFormatter + } + ]; + + this.handleSearchChange = this.handleSearchChange.bind(this); + this.getRows = this.getRows.bind(this); + this.rowGetter = this.rowGetter.bind(this); + this.openPreview = this.openPreview.bind(this); + this.closePreview = this.closePreview.bind(this); + this.openCreateModal = this.openCreateModal.bind(this); + this.closeCreateModal = this.closeCreateModal.bind(this); + this.createQuery = this.createQuery.bind(this); + this.handleUpdate = this.handleUpdate.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + handleSearchChange(e) { + this.setState({ search: e.target.value }); + } + + getRows() { + return mapRows( + matchSorter(this.props.queries, this.state.search, { + keys: ['query'] + }) + ); + } + + rowGetter(i) { + return this.getRows()[i]; + } + + openPreview(i, query) { + this.setState({ selectedQuery: query.query }); + } + + closePreview() { + this.setState({ selectedQuery: null }); + } + + openCreateModal() { + this.setState({ createModalOpen: true }); + } + + closeCreateModal() { + this.setState({ createModalOpen: false }); + } + + createQuery(queryConfig) { + const newQueryParams = { + ...queryConfig, + requestor: this.props.requestor + }; + this.props.createScheduledQuery(newQueryParams); + this.closeCreateModal(); + } + + handleUpdate(queryConfig) { + const newQueryParams = { + ...queryConfig, + requestor: this.props.requestor + }; + this.props.updateScheduledQuery(newQueryParams); + this.closePreview(); + } + + handleDelete(fid) { + this.props.deleteScheduledQuery(fid); + this.closePreview(); + } + + render() { + const rows = this.getRows(); + const loggedIn = this.props.requestor; + + return ( + + + + + + + + + + {rows.length} queries + + + + + + + + + + + + + + + + + + + + + ); + } +} + +export default Scheduler; diff --git a/app/components/Settings/Scheduler/sql.jsx b/app/components/Settings/Scheduler/sql.jsx new file mode 100644 index 000000000..3e081c4e5 --- /dev/null +++ b/app/components/Settings/Scheduler/sql.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Highlight from 'react-highlight'; + +export const SQL = props => ( + {props.children} +); + +SQL.propTypes = { + children: PropTypes.string, + className: PropTypes.string +}; + +export default SQL; \ No newline at end of file diff --git a/app/components/Settings/Scheduler/util.js b/app/components/Settings/Scheduler/util.js new file mode 100644 index 000000000..ccc95baa6 --- /dev/null +++ b/app/components/Settings/Scheduler/util.js @@ -0,0 +1,14 @@ +import {DIALECTS} from '../../../constants/constants.js'; + +export function mapDialect(dialect) { + return ({ + [DIALECTS.APACHE_SPARK]: 'text/x-sparksql', + [DIALECTS.MYSQL]: 'text/x-mysql', + [DIALECTS.SQLITE]: 'text/x-sqlite', + [DIALECTS.MARIADB]: 'text/x-mariadb', + [DIALECTS.ORACLE]: 'text/x-plsql', + [DIALECTS.POSTGRES]: 'text/x-pgsql', + [DIALECTS.REDSHIFT]: 'text/x-pgsql', + [DIALECTS.MSSQL]: 'text/x-mssql' + })[dialect] || 'text/x-sql'; +} \ No newline at end of file diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js index bf6c05221..5dba23d4f 100644 --- a/app/components/Settings/Settings.react.js +++ b/app/components/Settings/Settings.react.js @@ -13,7 +13,7 @@ import DialectSelector from './DialectSelector/DialectSelector.react'; import ConnectButton from './ConnectButton/ConnectButton.react'; import Preview from './Preview/Preview.react'; import {Link} from '../Link.react'; -import Scheduler from './scheduler.jsx'; +import Scheduler from './Scheduler/scheduler.jsx'; import {DIALECTS, FAQ, PREVIEW_QUERY, SQL_DIALECTS_USING_EDITOR} from '../../constants/constants.js'; import {isElectron, isOnPrem} from '../../utils/utils'; @@ -23,6 +23,8 @@ class Settings extends Component { this.fetchData = this.fetchData.bind(this); this.renderEditButton = this.renderEditButton.bind(this); this.renderSettingsForm = this.renderSettingsForm.bind(this); + this.updateSelectedPanel = this.updateSelectedPanel.bind(this); + this.openScheduler = this.openScheduler.bind(this); this.state = { editMode: true, selectedPanel: {}, @@ -31,7 +33,8 @@ class Settings extends Component { http: null }, timeElapsed: 0, - httpsServerIsOK: false + httpsServerIsOK: false, + scheduledQuery: null }; this.intervals = { timeElapsedInterval: null, @@ -252,6 +255,27 @@ class Settings extends Component { } } + updateSelectedPanel(panelIndex, cb) { + this.props.updatePreview({ + showChart: false, + showEditor: true, + size: 200 + }); + this.setState({ + selectedPanel: { + [this.props.selectedTab]: panelIndex + } + }, typeof cb === 'function' ? cb : () => {}); + } + + openScheduler() { + this.setState({ scheduledQuery: this.props.preview.code }, () => { + this.updateSelectedPanel(2, () => { + this.setState({ scheduledQuery: null }); + }); + }); + } + render() { const { apacheDrillStorageRequest, @@ -276,6 +300,9 @@ class Settings extends Component { selectedTable, selectedScheduledQueries, getScheduledQueries, + createScheduledQuery, + updateScheduledQuery, + deleteScheduledQuery, selectedIndex, setTab, tablesRequest, @@ -322,14 +349,7 @@ class Settings extends Component { { - updatePreview({ - showChart: false, - showEditor: true, - size: 200 - }); - this.setState({selectedPanel: {[selectedTab]: panelIndex}}); - }} + onSelect={this.updateSelectedPanel} > @@ -376,6 +396,7 @@ class Settings extends Component { updatePreview={updatePreview} runSqlQuery={runSqlQuery} + openScheduler={this.openScheduler} queryRequest={queryRequest || {}} s3KeysRequest={s3KeysRequest} apacheDrillStorageRequest={apacheDrillStorageRequest} @@ -388,7 +409,17 @@ class Settings extends Component { - + {isOnPrem() || @@ -607,6 +638,15 @@ function mergeProps(stateProps, dispatchProps, ownProps) { function boundGetScheduledQueries() { return dispatch(Actions.getScheduledQueries(selectedConnectionId)); } + function boundCreateScheduledQuery(payload) { + return dispatch(Actions.createScheduledQuery(selectedConnectionId, payload)); + } + function boundUpdateScheduledQuery(payload) { + return dispatch(Actions.updateScheduledQuery(selectedConnectionId, payload)); + } + function boundDeleteScheduledQuery(fid) { + return dispatch(Actions.deleteScheduledQuery(fid)); + } function boundGetElasticsearchMappings() { return dispatch(Actions.getElasticsearchMappings(selectedConnectionId)); } @@ -701,6 +741,9 @@ function mergeProps(stateProps, dispatchProps, ownProps) { updateConnection: boundUpdateConnection, getTables: boundGetTables, getScheduledQueries: boundGetScheduledQueries, + createScheduledQuery: boundCreateScheduledQuery, + updateScheduledQuery: boundUpdateScheduledQuery, + deleteScheduledQuery: boundDeleteScheduledQuery, getElasticsearchMappings: boundGetElasticsearchMappings, setTable: boundSetTable, setIndex: boundSetIndex, @@ -761,6 +804,9 @@ Settings.propTypes = { })), scheduledQueriesRequest: PropTypes.object, getScheduledQueries: PropTypes.func, + createScheduledQuery: PropTypes.func, + updateScheduledQuery: PropTypes.func, + deleteScheduledQuery: PropTypes.func, tablesRequest: PropTypes.object, deleteTab: PropTypes.func, getSqlSchema: PropTypes.func, diff --git a/app/components/modal.jsx b/app/components/modal.jsx index 31e08723a..00559734a 100644 --- a/app/components/modal.jsx +++ b/app/components/modal.jsx @@ -3,20 +3,25 @@ import PropTypes from 'prop-types'; import enhanceWithClickOutside from 'react-click-outside'; -const Modal = props => { - const EnhancedClass = enhanceWithClickOutside( - class extends Component { - handleClickOutside() { - props.onClickAway(); - } - render() { - return props.children; - } +const ClickAway = enhanceWithClickOutside( + class extends Component { + static propTypes = { + onClickAway: PropTypes.func, + children: PropTypes.node } - ); + handleClickOutside() { + this.props.onClickAway(); + } + render() { + return this.props.children; + } + } +); - return props.open ? +const Modal = props => + props.open ? (
{ zIndex: 9999 }} > - {props.children} -
: null; -}; + + {props.children} + +
+ ) : null; Modal.propTypes = { children: PropTypes.node, onClickAway: PropTypes.func, - open: PropTypes.bool + open: PropTypes.bool, + className: PropTypes.string }; export default Modal; diff --git a/app/reducers/index.js b/app/reducers/index.js index 7b4cd88dd..bae6e6c42 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -184,6 +184,27 @@ function scheduledQueries(state = [], action) { if (action.type === 'SET_SCHEDULED_QUERIES') { return action.payload; } + if (action.type === 'CREATE_SCHEDULED_QUERY') { + return [ + action.payload, + ...state + ]; + } + if (action.type === 'UPDATE_SCHEDULED_QUERY') { + return state.map(q => { + if (q.fid === action.payload.fid) { + return { + ...q, + ...action.payload + }; + } + return q; + }); + } + if (action.type === 'DELETE_SCHEDULED_QUERY') { + const fid = action.payload; + return state.filter(q => q.fid !== fid); + } return state; } diff --git a/static/css/Preview.css b/static/css/Preview.css index c4ad60917..057e3d2a8 100644 --- a/static/css/Preview.css +++ b/static/css/Preview.css @@ -17,6 +17,13 @@ th, td { overflow: auto; } +.scheduleButton { + bottom: 18px; + position: absolute; + right: 136px; + z-index: 10; +} + .runButton { bottom: 18px; position: absolute; @@ -48,3 +55,15 @@ th, td { margin: 0; padding: 20px; } + +.container-title { + font-size: 12px; + margin-bottom: 8px; +} + +.export-options-group { + margin: 8px 0 20px; + padding: 20px; + background: #f3f6fa; + border: 1px solid #ebf0f8; +} diff --git a/test/app/components/Settings/Scheduler/createModal.jsx b/test/app/components/Settings/Scheduler/createModal.jsx new file mode 100644 index 000000000..979108ec4 --- /dev/null +++ b/test/app/components/Settings/Scheduler/createModal.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { mount, configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +describe('Create Modal Test', () => { + let CodeMirror; + let CreateModal; + beforeAll(() => { + configure({ adapter: new Adapter() }); + global.document.createRange = function() { + return { + setEnd: function() {}, + setStart: function() {}, + getBoundingClientRect: function() { + return { right: 0 }; + }, + getClientRects: function() { + return { length: 0 }; + } + }; + }; + CreateModal = + require('../../../../../app/components/Settings/Scheduler/createModal.jsx').default; + CodeMirror = require('react-codemirror2').Controlled; + }); + + it("should not render the editor if it's closed", () => { + const component = mount(); + + expect(component.find(CodeMirror).get(0)).toBeUndefined(); + }); + + it('should render an empty editor if its open', () => { + const component = mount(); + + expect(component.find(CodeMirror).get(0).props.value).toBe(''); + }); + + it('should allow you to pass initialCode and initialFilename', () => { + const component = mount( + + ); + expect(component.find(CodeMirror).get(0).props.value).toBe( + 'SELECT * FROM foods' + ); + expect(component.find('input').get(0).props.value).toBe('filename'); + }); + + it('clicking X should call onClickAway', () => { + const onClickAway = jest.fn(); + const component = mount( + + ); + component + .find('button') + .first() + .simulate('click'); + expect(onClickAway).toHaveBeenCalled(); + }); + + it('clicking set error state if not all criteria, otherwise call onSubmit', () => { + const onSubmit = jest.fn(); + const component = mount( + + ); + component + .find('button') + .at(1) + .simulate('click'); + expect(component.state().error).not.toBeNull(); + expect(onSubmit).not.toHaveBeenCalled(); + + component.setState({ intervalType: { value: 60 } }); + component + .find('button') + .at(1) + .simulate('click'); + expect(onSubmit).toHaveBeenCalled(); + expect(onSubmit).toHaveBeenCalledWith({ + filename: 'filename', + query: 'SELECT * FROM foods', + refreshInterval: 60 + }); + }); +}); diff --git a/test/app/components/Settings/Scheduler/scheduler.test.jsx b/test/app/components/Settings/Scheduler/scheduler.test.jsx new file mode 100644 index 000000000..91d068b5a --- /dev/null +++ b/test/app/components/Settings/Scheduler/scheduler.test.jsx @@ -0,0 +1,86 @@ +import React from 'react'; +import sinon from 'sinon'; +import { mount, configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +import Scheduler, { + SchedulerPreview, + SQL +} from '../../../../../app/components/Settings/Scheduler/scheduler.jsx'; +import Modal from '../../../../../app/components/modal.jsx'; + +const mockQueries = [ + { + query: 'SELECT * FROM foods;', + refreshInterval: 5000, + fid: 'test:1' + }, + { + query: 'SELECT color, price FROM foods;', + refreshInterval: 10000, + fid: 'test:2' + } +]; + +describe('Scheduler Test', () => { + beforeAll(() => { + configure({ adapter: new Adapter() }); + }); + + it('should have no rows if not passed any queries', () => { + const component = mount(); + expect(component.instance().getRows().length).toBe(0); + }); + + it('should have correct number of rows when passed queries', () => { + const component = mount(); + expect(component.instance().getRows().length).toBe(2); + }); + + it('should filter rows based on search', () => { + const component = mount(); + + // set search to only match one mock query + component.setState({ search: 'color' }); + + expect(component.instance().getRows().length).toBe(1); + }); + + it('clicking refresh button calls refreshQueries prop', () => { + const spy = sinon.spy(); + const component = mount(); + + expect(spy.called).toBe(false); + + // click refresh button + component.find('.refresh-button').simulate('click'); + + expect(spy.callCount).toBe(1); + }); + + it('should open and close modal with correct query', () => { + const component = mount( + + ); + + expect(component.find(Modal).get(2).props.open).toBe(false); + + // set selected query + component.setState({ selectedQuery: mockQueries[0] }); + + expect(component.find(Modal).get(2).props.open).toBe(true); + + const modalSqlElements = component + .find(SchedulerPreview) + .find(SQL); + + modalSqlElements.forEach((element) => { + expect(element.text()).toBe('SELECT * FROM foods;'); + }); + + // clear selected query + component.setState({ selectedQuery: null }); + + expect(component.find(Modal).get(2).props.open).toBe(false); + }); +}); From 65e53b3fc1f62a9a2a6eb4f603166afe4d1a93ea Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:14:42 -0700 Subject: [PATCH 02/24] Fix tests --- app/components/Settings/scheduler.css | 52 --- app/components/Settings/scheduler.jsx | 305 ------------------ .../Settings/Scheduler/scheduler.test.jsx | 7 +- .../components/Settings/scheduler.test.jsx | 84 ----- 4 files changed, 3 insertions(+), 445 deletions(-) delete mode 100644 app/components/Settings/scheduler.css delete mode 100644 app/components/Settings/scheduler.jsx delete mode 100644 test/app/components/Settings/scheduler.test.jsx diff --git a/app/components/Settings/scheduler.css b/app/components/Settings/scheduler.css deleted file mode 100644 index 1671700e8..000000000 --- a/app/components/Settings/scheduler.css +++ /dev/null @@ -1,52 +0,0 @@ -/* SQL token highlighting */ -.default .hljs-keyword, -.default .hljs-selector-tag { - color: #ab63fa; -} - -.default .hljs-number, -.default .hljs-meta, -.default .hljs-built_in, -.default .hljs-builtin-name, -.default .hljs-literal, -.default .hljs-type, -.default .hljs-params { - color: #00cc96; -} - -.default .hljs-string, -.default .hljs-symbol, -.default .hljs-bullet { - color: #119DFF; -} - -.default .hljs { - color: #585260; -} - -.bold .hljs-keyword, -.bold .hljs-selector-tag { - font-weight: bold; -} - -/* custom ReactDataGrid styles for scheduled queries table */ -.scheduler-table .react-grid-Row--odd .react-grid-Cell { - background: #fff; -} - -.scheduler-table .react-grid-Row--odd:hover .react-grid-Cell { - background-color: #f9f9f9; -} - -.scheduler-table .react-grid-Row { - border-top: 1px solid #c8d4e3; - cursor: pointer; -} - -.scheduler-table .react-grid-Row:first-child { - border-top: none; -} - -.sql-preview pre { - white-space: pre-wrap; -} diff --git a/app/components/Settings/scheduler.jsx b/app/components/Settings/scheduler.jsx deleted file mode 100644 index fa4bdfdd6..000000000 --- a/app/components/Settings/scheduler.jsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; - -import ReactDataGrid from 'react-data-grid'; -import ms from 'ms'; -import matchSorter from 'match-sorter'; -import Highlight from 'react-highlight'; - -import { Link } from '../Link.react.js'; -import { Row, Column } from '../layout.jsx'; -import Modal from '../modal.jsx'; -import { plotlyUrl } from '../../utils/utils.js'; - -import './scheduler.css'; - -const NO_OP = () => {}; - -export const SQL = props => ( - {props.children} -); -SQL.propTypes = { - children: PropTypes.string, - className: PropTypes.string -}; - -class QueryFormatter extends React.Component { - static propTypes = { - /* - * Object passed by `react-data-grid` to each row. Here the value - * is an object containg the required `query` string. - */ - value: PropTypes.shape({ - query: PropTypes.string.isRequired - }) - }; - - render() { - const query = this.props.value; - return ( - - - {query.query} - - - ); - } -} - -class IntervalFormatter extends React.Component { - static propTypes = { - /* - * Object passed by `react-data-grid` to each row. Here the value - * is an object containg the required `refreshInterval`. - */ - value: PropTypes.shape({ - refreshInterval: PropTypes.number.isRequired - }) - }; - - render() { - const run = this.props.value; - return ( - - - - {`Runs every ${ms(run.refreshInterval * 1000, { - long: true - })}`} - - - - ); - } -} - -const rowStyle = { - justifyContent: 'flex-start', - borderBottom: '1px solid rgba(0, 0, 0, 0.05)', - padding: '16px 0px' -}; -const boxStyle = { boxSizing: 'border-box', width: '50%' }; - -export const SchedulerPreview = props => { - let content; - if (!props.query) { - content = null; - } else { - const [account, gridId] = props.query.fid.split(':'); - const link = `${plotlyUrl()}/~${account}/${gridId}`; - content = ( - - -
- {props.query.query} -
- -
- - -
Query
-
- {props.query.query} -
-
- -
Update Frequency
- - Runs every{' '} - - {ms(props.query.refreshInterval * 1000, { - long: true - })} - - -
- -
Live Dataset
- - {link} - -
-
-
- ); - } - - return ( - - {content} - - ); -}; - -SchedulerPreview.propTypes = { - onCloseBtnClick: PropTypes.func, - query: PropTypes.object -}; - -function mapRows(rows) { - return rows.map(r => ({ - query: r, - run: r - })); -} - -class Scheduler extends Component { - constructor(props) { - super(props); - this.state = { - search: '', - selectedQuery: null - }; - this.columns = [ - { - key: 'query', - name: 'Query', - filterable: true, - formatter: QueryFormatter - }, - { - key: 'run', - name: 'Interval', - filterable: true, - formatter: IntervalFormatter - } - ]; - - this.handleSearchChange = this.handleSearchChange.bind(this); - this.getRows = this.getRows.bind(this); - this.rowGetter = this.rowGetter.bind(this); - this.openPreview = this.openPreview.bind(this); - this.closePreview = this.closePreview.bind(this); - } - - handleSearchChange(e) { - this.setState({ search: e.target.value }); - } - - getRows() { - return mapRows( - matchSorter(this.props.queries, this.state.search, { - keys: ['query'] - }) - ); - } - - rowGetter(i) { - return this.getRows()[i]; - } - - openPreview(i, query) { - this.setState({ selectedQuery: query.query }); - } - - closePreview() { - this.setState({ selectedQuery: null }); - } - - render() { - const rows = this.getRows(); - - return ( - - - - - - - - - {rows.length} queries - - - - - - - - - - - - - - - - - ); - } -} - -Scheduler.defaultProps = { - queries: [], - refreshQueries: NO_OP -}; - -Scheduler.propTypes = { - queries: PropTypes.arrayOf( - PropTypes.shape({ - query: PropTypes.string.isRequired, - refreshInterval: PropTypes.number.isRequired, - fid: PropTypes.string.isRequired - }).isRequired - ), - refreshQueries: PropTypes.func.isRequired -}; - -export default Scheduler; diff --git a/test/app/components/Settings/Scheduler/scheduler.test.jsx b/test/app/components/Settings/Scheduler/scheduler.test.jsx index 91d068b5a..6fe5484c0 100644 --- a/test/app/components/Settings/Scheduler/scheduler.test.jsx +++ b/test/app/components/Settings/Scheduler/scheduler.test.jsx @@ -3,10 +3,9 @@ import sinon from 'sinon'; import { mount, configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; -import Scheduler, { - SchedulerPreview, - SQL -} from '../../../../../app/components/Settings/Scheduler/scheduler.jsx'; +import Scheduler from '../../../../../app/components/Settings/Scheduler/scheduler.jsx'; +import SQL from '../../../../../app/components/Settings/Scheduler/sql.jsx'; +import SchedulerPreview from '../../../../../app/components/Settings/Scheduler/previewModal.jsx'; import Modal from '../../../../../app/components/modal.jsx'; const mockQueries = [ diff --git a/test/app/components/Settings/scheduler.test.jsx b/test/app/components/Settings/scheduler.test.jsx deleted file mode 100644 index 25ac89ef9..000000000 --- a/test/app/components/Settings/scheduler.test.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import sinon from 'sinon'; -import { mount, configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -import Scheduler, { - SchedulerPreview, - SQL -} from '../../../../app/components/Settings/scheduler.jsx'; -import Modal from '../../../../app/components/modal.jsx'; - -const mockQueries = [ - { - query: 'SELECT * FROM foods;', - refreshInterval: 5000, - fid: 'test:1' - }, - { - query: 'SELECT color, price FROM foods;', - refreshInterval: 10000, - fid: 'test:2' - } -]; - -describe('Scheduler Test', () => { - beforeAll(() => { - configure({ adapter: new Adapter() }); - }); - - it('should have no rows if not passed any queries', () => { - const component = mount(); - expect(component.instance().getRows().length).toBe(0); - }); - - it('should have correct number of rows when passed queries', () => { - const component = mount(); - expect(component.instance().getRows().length).toBe(2); - }); - - it('should filter rows based on search', () => { - const component = mount(); - - // set search to only match one mock query - component.setState({ search: 'color' }); - - expect(component.instance().getRows().length).toBe(1); - }); - - it('clicking refresh button calls refreshQueries prop', () => { - const spy = sinon.spy(); - const component = mount(); - - expect(spy.called).toBe(false); - - // click refresh button - component.find('.refresh-button').simulate('click'); - - expect(spy.callCount).toBe(1); - }); - - it('should open and close modal with correct query', () => { - const component = mount(); - - expect(component.find(Modal).prop('open')).toBe(false); - - // set selected query - component.setState({ selectedQuery: mockQueries[0] }); - - expect(component.find(Modal).prop('open')).toBe(true); - - const modalSqlElements = component - .find(SchedulerPreview) - .find(SQL); - - modalSqlElements.forEach((element) => { - expect(element.text()).toBe('SELECT * FROM foods;'); - }); - - // clear selected query - component.setState({ selectedQuery: null }); - - expect(component.find(Modal).prop('open')).toBe(false); - }); -}); From 3c4480b1bb97caf98adc8edbad69f607b69c98a8 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:22:58 -0700 Subject: [PATCH 03/24] Scheduler -> scheduler --- app/components/Settings/{Scheduler => scheduler}/createModal.jsx | 0 app/components/Settings/{Scheduler => scheduler}/loginModal.jsx | 0 app/components/Settings/{Scheduler => scheduler}/previewModal.jsx | 0 app/components/Settings/{Scheduler => scheduler}/scheduler.css | 0 app/components/Settings/{Scheduler => scheduler}/scheduler.jsx | 0 app/components/Settings/{Scheduler => scheduler}/sql.jsx | 0 app/components/Settings/{Scheduler => scheduler}/util.js | 0 .../components/Settings/{Scheduler => scheduler}/createModal.jsx | 0 .../Settings/{Scheduler => scheduler}/scheduler.test.jsx | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename app/components/Settings/{Scheduler => scheduler}/createModal.jsx (100%) rename app/components/Settings/{Scheduler => scheduler}/loginModal.jsx (100%) rename app/components/Settings/{Scheduler => scheduler}/previewModal.jsx (100%) rename app/components/Settings/{Scheduler => scheduler}/scheduler.css (100%) rename app/components/Settings/{Scheduler => scheduler}/scheduler.jsx (100%) rename app/components/Settings/{Scheduler => scheduler}/sql.jsx (100%) rename app/components/Settings/{Scheduler => scheduler}/util.js (100%) rename test/app/components/Settings/{Scheduler => scheduler}/createModal.jsx (100%) rename test/app/components/Settings/{Scheduler => scheduler}/scheduler.test.jsx (100%) diff --git a/app/components/Settings/Scheduler/createModal.jsx b/app/components/Settings/scheduler/createModal.jsx similarity index 100% rename from app/components/Settings/Scheduler/createModal.jsx rename to app/components/Settings/scheduler/createModal.jsx diff --git a/app/components/Settings/Scheduler/loginModal.jsx b/app/components/Settings/scheduler/loginModal.jsx similarity index 100% rename from app/components/Settings/Scheduler/loginModal.jsx rename to app/components/Settings/scheduler/loginModal.jsx diff --git a/app/components/Settings/Scheduler/previewModal.jsx b/app/components/Settings/scheduler/previewModal.jsx similarity index 100% rename from app/components/Settings/Scheduler/previewModal.jsx rename to app/components/Settings/scheduler/previewModal.jsx diff --git a/app/components/Settings/Scheduler/scheduler.css b/app/components/Settings/scheduler/scheduler.css similarity index 100% rename from app/components/Settings/Scheduler/scheduler.css rename to app/components/Settings/scheduler/scheduler.css diff --git a/app/components/Settings/Scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx similarity index 100% rename from app/components/Settings/Scheduler/scheduler.jsx rename to app/components/Settings/scheduler/scheduler.jsx diff --git a/app/components/Settings/Scheduler/sql.jsx b/app/components/Settings/scheduler/sql.jsx similarity index 100% rename from app/components/Settings/Scheduler/sql.jsx rename to app/components/Settings/scheduler/sql.jsx diff --git a/app/components/Settings/Scheduler/util.js b/app/components/Settings/scheduler/util.js similarity index 100% rename from app/components/Settings/Scheduler/util.js rename to app/components/Settings/scheduler/util.js diff --git a/test/app/components/Settings/Scheduler/createModal.jsx b/test/app/components/Settings/scheduler/createModal.jsx similarity index 100% rename from test/app/components/Settings/Scheduler/createModal.jsx rename to test/app/components/Settings/scheduler/createModal.jsx diff --git a/test/app/components/Settings/Scheduler/scheduler.test.jsx b/test/app/components/Settings/scheduler/scheduler.test.jsx similarity index 100% rename from test/app/components/Settings/Scheduler/scheduler.test.jsx rename to test/app/components/Settings/scheduler/scheduler.test.jsx From be70c9d50fac2e947b202acce477df4c84adcd6f Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:25:42 -0700 Subject: [PATCH 04/24] Rename component files --- .../scheduler/{createModal.jsx => create-modal.jsx} | 0 .../Settings/scheduler/{loginModal.jsx => login-modal.jsx} | 0 .../scheduler/{previewModal.jsx => preview-modal.jsx} | 2 +- app/components/Settings/scheduler/scheduler.jsx | 6 +++--- .../scheduler/{createModal.jsx => create-modal.test.jsx} | 2 +- test/app/components/Settings/scheduler/scheduler.test.jsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename app/components/Settings/scheduler/{createModal.jsx => create-modal.jsx} (100%) rename app/components/Settings/scheduler/{loginModal.jsx => login-modal.jsx} (100%) rename app/components/Settings/scheduler/{previewModal.jsx => preview-modal.jsx} (99%) rename test/app/components/Settings/scheduler/{createModal.jsx => create-modal.test.jsx} (98%) diff --git a/app/components/Settings/scheduler/createModal.jsx b/app/components/Settings/scheduler/create-modal.jsx similarity index 100% rename from app/components/Settings/scheduler/createModal.jsx rename to app/components/Settings/scheduler/create-modal.jsx diff --git a/app/components/Settings/scheduler/loginModal.jsx b/app/components/Settings/scheduler/login-modal.jsx similarity index 100% rename from app/components/Settings/scheduler/loginModal.jsx rename to app/components/Settings/scheduler/login-modal.jsx diff --git a/app/components/Settings/scheduler/previewModal.jsx b/app/components/Settings/scheduler/preview-modal.jsx similarity index 99% rename from app/components/Settings/scheduler/previewModal.jsx rename to app/components/Settings/scheduler/preview-modal.jsx index eb69c41ec..e65a1c637 100644 --- a/app/components/Settings/scheduler/previewModal.jsx +++ b/app/components/Settings/scheduler/preview-modal.jsx @@ -5,7 +5,7 @@ import ms from 'ms'; import Modal from '../../modal.jsx'; import { Link } from '../../Link.react.js'; -import { FrequencySelector } from './createModal.jsx'; +import { FrequencySelector } from './create-modal.jsx'; import { Row, Column } from '../../layout.jsx'; import SQL from './sql.jsx'; import { plotlyUrl } from '../../../utils/utils.js'; diff --git a/app/components/Settings/scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx index 9ddfe12c7..062952617 100644 --- a/app/components/Settings/scheduler/scheduler.jsx +++ b/app/components/Settings/scheduler/scheduler.jsx @@ -5,9 +5,9 @@ import ReactDataGrid from 'react-data-grid'; import ms from 'ms'; import matchSorter from 'match-sorter'; -import CreateModal from './createModal.jsx'; -import PreviewModal from './previewModal.jsx'; -import PromptLoginModal from './loginModal.jsx'; +import CreateModal from './create-modal.jsx'; +import PreviewModal from './preview-modal.jsx'; +import PromptLoginModal from './login-modal.jsx'; import { Row, Column } from '../../layout.jsx'; import SQL from './sql.jsx'; diff --git a/test/app/components/Settings/scheduler/createModal.jsx b/test/app/components/Settings/scheduler/create-modal.test.jsx similarity index 98% rename from test/app/components/Settings/scheduler/createModal.jsx rename to test/app/components/Settings/scheduler/create-modal.test.jsx index 979108ec4..87d02d2a4 100644 --- a/test/app/components/Settings/scheduler/createModal.jsx +++ b/test/app/components/Settings/scheduler/create-modal.test.jsx @@ -20,7 +20,7 @@ describe('Create Modal Test', () => { }; }; CreateModal = - require('../../../../../app/components/Settings/Scheduler/createModal.jsx').default; + require('../../../../../app/components/Settings/Scheduler/create-modal.jsx').default; CodeMirror = require('react-codemirror2').Controlled; }); diff --git a/test/app/components/Settings/scheduler/scheduler.test.jsx b/test/app/components/Settings/scheduler/scheduler.test.jsx index 6fe5484c0..a35f5b8f1 100644 --- a/test/app/components/Settings/scheduler/scheduler.test.jsx +++ b/test/app/components/Settings/scheduler/scheduler.test.jsx @@ -5,7 +5,7 @@ import Adapter from 'enzyme-adapter-react-16'; import Scheduler from '../../../../../app/components/Settings/Scheduler/scheduler.jsx'; import SQL from '../../../../../app/components/Settings/Scheduler/sql.jsx'; -import SchedulerPreview from '../../../../../app/components/Settings/Scheduler/previewModal.jsx'; +import SchedulerPreview from '../../../../../app/components/Settings/Scheduler/preview-modal.jsx'; import Modal from '../../../../../app/components/modal.jsx'; const mockQueries = [ From 2096fa4e8252d476d984948e51914e31e044d025 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:34:57 -0700 Subject: [PATCH 05/24] Move getHighlightMode to constants --- app/components/Settings/Preview/code-editor.jsx | 13 ++----------- .../Settings/scheduler/create-modal.jsx | 5 +++-- .../Settings/scheduler/preview-modal.jsx | 4 ++-- app/components/Settings/scheduler/util.js | 14 -------------- app/constants/constants.js | 17 +++++++++++++++++ 5 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 app/components/Settings/scheduler/util.js diff --git a/app/components/Settings/Preview/code-editor.jsx b/app/components/Settings/Preview/code-editor.jsx index 3e918bdbb..6e927263e 100644 --- a/app/components/Settings/Preview/code-editor.jsx +++ b/app/components/Settings/Preview/code-editor.jsx @@ -14,7 +14,7 @@ import 'react-resizable/css/styles.css'; import './code-editor.css'; -import {DIALECTS} from '../../../constants/constants'; +import {getHighlightMode} from '../../../constants/constants'; const MIN_CONSTRAINTS_HEIGHT = 74; @@ -188,16 +188,7 @@ export default class CodeEditor extends React.Component { maxConstraints } = this.state; - const mode = { - [DIALECTS.APACHE_SPARK]: 'text/x-sparksql', - [DIALECTS.MYSQL]: 'text/x-mysql', - [DIALECTS.SQLITE]: 'text/x-sqlite', - [DIALECTS.MARIADB]: 'text/x-mariadb', - [DIALECTS.ORACLE]: 'text/x-plsql', - [DIALECTS.POSTGRES]: 'text/x-pgsql', - [DIALECTS.REDSHIFT]: 'text/x-pgsql', - [DIALECTS.MSSQL]: 'text/x-mssql' - }[dialect] || 'text/x-sql'; + const mode = getHighlightMode(dialect); const options = { lineNumbers: true, diff --git a/app/components/Settings/scheduler/create-modal.jsx b/app/components/Settings/scheduler/create-modal.jsx index 22656ac03..2fa128242 100644 --- a/app/components/Settings/scheduler/create-modal.jsx +++ b/app/components/Settings/scheduler/create-modal.jsx @@ -6,7 +6,8 @@ import { Controlled as CodeMirror } from 'react-codemirror2'; import { Row, Column } from '../../layout.jsx'; import Modal from '../../modal.jsx'; -import { mapDialect } from './util.js'; + +import { getHighlightMode } from '../../../constants/constants.js'; function noop() {} @@ -98,7 +99,7 @@ class CreateModal extends Component { tabSize: 4, readOnly: false, extraKeys: {}, - mode: mapDialect(this.props.dialect) + mode: getHighlightMode(this.props.dialect) }; this.updateCode = this.updateCode.bind(this); this.handleIntervalChange = this.handleIntervalChange.bind(this); diff --git a/app/components/Settings/scheduler/preview-modal.jsx b/app/components/Settings/scheduler/preview-modal.jsx index e65a1c637..40891bf62 100644 --- a/app/components/Settings/scheduler/preview-modal.jsx +++ b/app/components/Settings/scheduler/preview-modal.jsx @@ -9,7 +9,7 @@ import { FrequencySelector } from './create-modal.jsx'; import { Row, Column } from '../../layout.jsx'; import SQL from './sql.jsx'; import { plotlyUrl } from '../../../utils/utils.js'; -import { mapDialect } from './util.js'; +import { getHighlightMode } from '../../../constants/constants.js'; const NO_OP = () => {}; @@ -159,7 +159,7 @@ export class PreviewModal extends Component { lineNumbers: true, tabSize: 4, readOnly: false, - mode: mapDialect(this.props.dialect) + mode: getHighlightMode(this.props.dialect) }} value={this.state.code} diff --git a/app/components/Settings/scheduler/util.js b/app/components/Settings/scheduler/util.js deleted file mode 100644 index ccc95baa6..000000000 --- a/app/components/Settings/scheduler/util.js +++ /dev/null @@ -1,14 +0,0 @@ -import {DIALECTS} from '../../../constants/constants.js'; - -export function mapDialect(dialect) { - return ({ - [DIALECTS.APACHE_SPARK]: 'text/x-sparksql', - [DIALECTS.MYSQL]: 'text/x-mysql', - [DIALECTS.SQLITE]: 'text/x-sqlite', - [DIALECTS.MARIADB]: 'text/x-mariadb', - [DIALECTS.ORACLE]: 'text/x-plsql', - [DIALECTS.POSTGRES]: 'text/x-pgsql', - [DIALECTS.REDSHIFT]: 'text/x-pgsql', - [DIALECTS.MSSQL]: 'text/x-mssql' - })[dialect] || 'text/x-sql'; -} \ No newline at end of file diff --git a/app/constants/constants.js b/app/constants/constants.js index 98287ba68..5a8ded485 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -486,3 +486,20 @@ export const SAMPLE_DBS = { url: 'https://data.world/rflprr/reported-lyme-disease-cases-by-state' } }; + +export function getHighlightMode(dialect) { + if (!SQL_DIALECTS_USING_EDITOR.includes(dialect)) { + return 'text/plain'; + } + + return { + [DIALECTS.APACHE_SPARK]: 'text/x-sparksql', + [DIALECTS.MYSQL]: 'text/x-mysql', + [DIALECTS.SQLITE]: 'text/x-sqlite', + [DIALECTS.MARIADB]: 'text/x-mariadb', + [DIALECTS.ORACLE]: 'text/x-plsql', + [DIALECTS.POSTGRES]: 'text/x-pgsql', + [DIALECTS.REDSHIFT]: 'text/x-pgsql', + [DIALECTS.MSSQL]: 'text/x-mssql' + }[dialect] || 'text/x-sql'; +} From fd8d17d57365ce487948eabb771df117fe307295 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:36:43 -0700 Subject: [PATCH 06/24] Move .scheduleButton to code-editor.css --- app/components/Settings/Preview/code-editor.css | 7 +++++++ static/css/Preview.css | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/components/Settings/Preview/code-editor.css b/app/components/Settings/Preview/code-editor.css index ef9924f10..b91776d60 100644 --- a/app/components/Settings/Preview/code-editor.css +++ b/app/components/Settings/Preview/code-editor.css @@ -15,3 +15,10 @@ .react-resizable-handle { z-index: 9; } + +.scheduleButton { + bottom: 18px; + position: absolute; + right: 136px; + z-index: 10; +} diff --git a/static/css/Preview.css b/static/css/Preview.css index 057e3d2a8..fa1f99aac 100644 --- a/static/css/Preview.css +++ b/static/css/Preview.css @@ -17,13 +17,6 @@ th, td { overflow: auto; } -.scheduleButton { - bottom: 18px; - position: absolute; - right: 136px; - z-index: 10; -} - .runButton { bottom: 18px; position: absolute; From a9c4d5a9c037bfc3a187f92793217b5f6f42a065 Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 09:58:41 -0700 Subject: [PATCH 07/24] Dispatch scheduled query actions directly w/o redux-actions --- app/actions/sessions.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/actions/sessions.js b/app/actions/sessions.js index 1c9738d23..12c0f8fc2 100644 --- a/app/actions/sessions.js +++ b/app/actions/sessions.js @@ -9,10 +9,6 @@ export const mergeTabMap = createAction('MERGE_TAB_MAP'); export const setTab = createAction('SET_TAB'); export const setTable = createAction('SET_TABLE'); export const setIndex = createAction('SET_INDEX'); -export const setScheduledQueries = createAction('SET_SCHEDULED_QUERIES'); -export const createScheduledQueryAction = createAction('CREATE_SCHEDULED_QUERY'); -export const updateScheduledQueryAction = createAction('UPDATE_SCHEDULED_QUERY'); -export const deleteScheduledQueryAction = createAction('DELETE_SCHEDULED_QUERY'); export const mergeConnections = createAction('MERGE_CONNECTIONS'); export const updateConnection = createAction('UPDATE_CREDENTIAL'); export const deleteConnection = createAction('DELETE_CREDENTIAL'); @@ -148,7 +144,10 @@ export function getScheduledQueries() { 'GET', 'scheduledQueriesRequest' )).then((json => { - dispatch(setScheduledQueries(json)); + dispatch({ + type: 'SET_SCHEDULED_QUERIES', + payload: json + }); return json; })); }; @@ -171,7 +170,10 @@ export function createScheduledQuery(connectionId, payload = {}) { connectionId } )).then((res) => { - dispatch(createScheduledQueryAction(res)); + dispatch({ + type: 'CREATE_SCHEDULED_QUERY', + payload: res + }); return res; }); }; @@ -196,7 +198,10 @@ export function updateScheduledQuery(connectionId, payload = {}) { payload.filename, body )).then((res) => { - dispatch(updateScheduledQueryAction(body)); + dispatch({ + type: 'UPDATE_SCHEDULED_QUERY', + payload: body + }); return res; }); }; @@ -209,7 +214,10 @@ export function deleteScheduledQuery(fid) { 'DELETE', 'createScheduledQueryRequest' )).then((res) => { - dispatch(deleteScheduledQueryAction(fid)); + dispatch({ + type: 'DELETE_SCHEDULED_QUERY', + payload: fid + }); return res; }); }; From 863870bd80f13af98885b722d972ffe22ed86a8d Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 10:03:08 -0700 Subject: [PATCH 08/24] scheduleQuery -> openScheduler, remove unused button --- app/components/Settings/Preview/Preview.react.js | 7 +------ app/components/Settings/Preview/code-editor.jsx | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/components/Settings/Preview/Preview.react.js b/app/components/Settings/Preview/Preview.react.js index 5f3b415a6..bf25bb43a 100644 --- a/app/components/Settings/Preview/Preview.react.js +++ b/app/components/Settings/Preview/Preview.react.js @@ -348,7 +348,7 @@ class Preview extends Component { dialect={dialect} runQuery={this.runQuery} - scheduleQuery={this.props.openScheduler} + openScheduler={this.props.openScheduler} schemaRequest={schemaRequest} isLoading={isLoading} /> @@ -474,11 +474,6 @@ class Preview extends Component { > Upload Dataset -
MY COMPUTER
diff --git a/app/components/Settings/Preview/code-editor.jsx b/app/components/Settings/Preview/code-editor.jsx index 6e927263e..5fa2a5918 100644 --- a/app/components/Settings/Preview/code-editor.jsx +++ b/app/components/Settings/Preview/code-editor.jsx @@ -25,7 +25,7 @@ export default class CodeEditor extends React.Component { dialect: PropTypes.string, runQuery: PropTypes.func, - scheduleQuery: PropTypes.func, + openScheduler: PropTypes.func, schemaRequest: PropTypes.object, isLoading: PropTypes.bool } @@ -178,7 +178,7 @@ export default class CodeEditor extends React.Component { dialect, runQuery, isLoading, - scheduleQuery + openScheduler } = this.props; const { @@ -217,7 +217,7 @@ export default class CodeEditor extends React.Component { /> - + -

+

A scheduled query runs and updates its corresponding dataset in Plotly Cloud. Learn more about scheduled queries here.

- -
Query
-
+ +
Query
+
- -
Filename
-
+ +
Filename
+
- -
Frequency
-
-
+ +
Frequency
+
+
{this.state.error && ( - + )} @@ -220,7 +186,7 @@ class CreateModal extends Component {
- + {/* + +
Filename
+
+ +
+
+ */}
Frequency
diff --git a/test/app/components/Settings/scheduler/create-modal.test.jsx b/test/app/components/Settings/scheduler/create-modal.test.jsx index 283a44f35..3488fc37a 100644 --- a/test/app/components/Settings/scheduler/create-modal.test.jsx +++ b/test/app/components/Settings/scheduler/create-modal.test.jsx @@ -45,7 +45,8 @@ describe('Create Modal Test', () => { expect(component.find(CodeMirror).get(0).props.value).toBe( 'SELECT * FROM foods' ); - expect(component.find('input').get(0).props.value).toBe('filename'); + // TODO: Uncomment onces filename input is suppported + // expect(component.find('input').get(0).props.value).toBe('filename'); }); it('clicking X should call onClickAway', () => { @@ -89,7 +90,8 @@ describe('Create Modal Test', () => { .simulate('click'); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit).toHaveBeenCalledWith({ - filename: 'filename', + // Once filename input is supported, this value should be: 'filename', + filename: expect.any(String), query: 'SELECT * FROM foods', refreshInterval: 60 }); From 5eebf5b45ec00e4c64b59e21350674be6b4ac63c Mon Sep 17 00:00:00 2001 From: Mike Fix Date: Thu, 5 Jul 2018 11:36:27 -0700 Subject: [PATCH 14/24] Remove 'Create Scheduled Query' button for dialects using editor --- .../Settings/scheduler/scheduler.jsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/components/Settings/scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx index 062952617..4ef66d65d 100644 --- a/app/components/Settings/scheduler/scheduler.jsx +++ b/app/components/Settings/scheduler/scheduler.jsx @@ -1,6 +1,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; +import {contains} from 'ramda'; import ReactDataGrid from 'react-data-grid'; import ms from 'ms'; import matchSorter from 'match-sorter'; @@ -11,6 +12,8 @@ import PromptLoginModal from './login-modal.jsx'; import { Row, Column } from '../../layout.jsx'; import SQL from './sql.jsx'; +import {SQL_DIALECTS_USING_EDITOR} from '../../../constants/constants'; + import './scheduler.css'; const NO_OP = () => {}; @@ -211,12 +214,15 @@ class Scheduler extends Component { onChange={this.handleSearchChange} placeholder="Search scheduled queries..." /> - + {!contains(this.props.dialect, SQL_DIALECTS_USING_EDITOR) && ( + + )} + Date: Thu, 5 Jul 2018 11:54:28 -0700 Subject: [PATCH 15/24] Extract login-modal styles to CSS file --- .../Settings/scheduler/login-modal.css | 26 ++++++++++++++ .../Settings/scheduler/login-modal.jsx | 34 ++++++------------- 2 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 app/components/Settings/scheduler/login-modal.css diff --git a/app/components/Settings/scheduler/login-modal.css b/app/components/Settings/scheduler/login-modal.css new file mode 100644 index 000000000..cf175db53 --- /dev/null +++ b/app/components/Settings/scheduler/login-modal.css @@ -0,0 +1,26 @@ +.login-modal .container { + background: #F5F7FB; + position: relative +} + +.login-modal .header { + margin-bottom: 16px; + padding: 0 32px +} + +.login-modal .button { + position: absolute; + top: 16px; + right: 16px; + padding: 2px 4px +} + +.login-modal p { + margin: 56px 0 8px; + padding: 16px; +} + +.login-modal .submit { + width: 50%; + margin: 24px 32px 32px +} diff --git a/app/components/Settings/scheduler/login-modal.jsx b/app/components/Settings/scheduler/login-modal.jsx index 1fce58c8f..b4b9e1813 100644 --- a/app/components/Settings/scheduler/login-modal.jsx +++ b/app/components/Settings/scheduler/login-modal.jsx @@ -4,44 +4,30 @@ import PropTypes from 'prop-types'; import { Row, Column } from '../../layout.jsx'; import Modal from '../../modal.jsx'; -const styles = { - column: { width: '400px', background: '#F5F7FB', position: 'relative' }, - header: { marginBottom: '16px', padding: '0 32px' }, - button: { - position: 'absolute', - top: '16px', - right: '16px', - padding: '2px 4px' - }, - detailsColumn: { padding: '0 32px' }, - p: { margin: '56px 0 8px', padding: '16px' }, - submit: { - width: '100%', - margin: '24px 32px 32px' - }, - row: { justifyContent: 'flex-start' } -}; +import './login-modal.css'; + +const containerOverrideStyle = { width: '400px' }; const PromptLoginModal = props => ( - - - -

+ +

To create a scheduled query, you'll need to be logged into Plotly.

- +
diff --git a/app/components/Settings/scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx index 4ef66d65d..dcf797f88 100644 --- a/app/components/Settings/scheduler/scheduler.jsx +++ b/app/components/Settings/scheduler/scheduler.jsx @@ -178,8 +178,10 @@ class Scheduler extends Component { ...queryConfig, requestor: this.props.requestor }; - this.props.createScheduledQuery(newQueryParams); - this.closeCreateModal(); + return this.props.createScheduledQuery(newQueryParams) + .then(() => { + this.closeCreateModal(); + }); } handleUpdate(queryConfig) { diff --git a/test/app/components/Settings/scheduler/create-modal.test.jsx b/test/app/components/Settings/scheduler/create-modal.test.jsx index 3488fc37a..e3c04c306 100644 --- a/test/app/components/Settings/scheduler/create-modal.test.jsx +++ b/test/app/components/Settings/scheduler/create-modal.test.jsx @@ -67,7 +67,7 @@ describe('Create Modal Test', () => { }); it('clicking set error state if not all criteria, otherwise call onSubmit', () => { - const onSubmit = jest.fn(); + const onSubmit = jest.fn(() => Promise.resolve()); const component = mount( Date: Thu, 5 Jul 2018 13:05:15 -0700 Subject: [PATCH 17/24] Preview modal loading and error states --- .../Settings/scheduler/create-modal.jsx | 6 +--- .../Settings/scheduler/preview-modal.jsx | 28 +++++++++++++------ .../Settings/scheduler/scheduler.jsx | 10 +++++-- app/components/error.jsx | 9 ++++++ 4 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 app/components/error.jsx diff --git a/app/components/Settings/scheduler/create-modal.jsx b/app/components/Settings/scheduler/create-modal.jsx index ba08a90d9..4d153b69b 100644 --- a/app/components/Settings/scheduler/create-modal.jsx +++ b/app/components/Settings/scheduler/create-modal.jsx @@ -6,6 +6,7 @@ import { Controlled as CodeMirror } from 'react-codemirror2'; import { Row, Column } from '../../layout.jsx'; import Modal from '../../modal.jsx'; +import Error from '../../error.jsx'; import { getHighlightMode } from '../../../constants/constants.js'; @@ -23,11 +24,6 @@ const FREQUENCIES = [ const rowStyleOverride = { justifyContent: 'flex-start' }; -const Error = props =>
{props.message}
; -Error.propTypes = { - message: PropTypes.string.isRequired -}; - function generateFilename() { let n = Math.floor(Math.random() * 1e8).toString(); diff --git a/app/components/Settings/scheduler/preview-modal.jsx b/app/components/Settings/scheduler/preview-modal.jsx index 20ddaa95b..523e5baeb 100644 --- a/app/components/Settings/scheduler/preview-modal.jsx +++ b/app/components/Settings/scheduler/preview-modal.jsx @@ -4,6 +4,7 @@ import { Controlled as CodeMirror } from 'react-codemirror2'; import ms from 'ms'; import Modal from '../../modal.jsx'; +import Error from '../../error.jsx'; import { Link } from '../../Link.react.js'; import { FrequencySelector } from './create-modal.jsx'; import { Row, Column } from '../../layout.jsx'; @@ -39,7 +40,8 @@ export class PreviewModal extends Component { editing: false, code: props.query && props.query.query, refreshInterval: props.query && props.query.refreshInterval, - confirmedDelete: false + confirmedDelete: false, + loading: false }; this.updateCode = this.updateCode.bind(this); this.updateRefreshInterval = this.updateRefreshInterval.bind(this); @@ -50,12 +52,12 @@ export class PreviewModal extends Component { componentWillReceiveProps(nextProps) { if ( - this.props.query && - this.props.query.query !== (nextProps.query && nextProps.query.query) + nextProps.query && + nextProps.query.query !== (this.props.query && this.props.query.query) ) { this.setState({ - code: this.props.query.query, - refreshInterval: this.props.query.refreshInterval + code: nextProps.query.query, + refreshInterval: nextProps.query.refreshInterval }); } } @@ -72,6 +74,7 @@ export class PreviewModal extends Component { if (this.state.editing) { const { connectionId, fid, requestor, uids } = this.props.query; const { code: query, refreshInterval } = this.state; + this.setState({ loading: true }); this.props.onSave({ connectionId, fid, @@ -79,8 +82,10 @@ export class PreviewModal extends Component { uids, query, refreshInterval - }); - this.setState({ editing: false, confirmedDelete: false }); + }) + .then(() => this.setState({ editing: false, confirmedDelete: false })) + .catch(error => this.setState({ error })) + .then(() => this.setState({ loading: false })); } else { this.setState({ editing: true, confirmedDelete: false }); } @@ -110,7 +115,7 @@ export class PreviewModal extends Component { } else { const [account, gridId] = props.query.fid.split(':'); const link = `${plotlyUrl()}/~${account}/${gridId}`; - const editing = this.state.editing; + const { editing, loading } = this.state; content = ( + {this.state.error && ( + + + + )} - {editing ? 'Save' : 'Edit'} + {loading ? 'Loading...' : (editing ? 'Save' : 'Edit')} {!editing && ( - {!editing && ( - - )} - + {this.props.loggedIn && ( + + + {!editing && ( + + )} + + )} ); diff --git a/app/components/Settings/scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx index fa5c450ff..fe6d5deeb 100644 --- a/app/components/Settings/scheduler/scheduler.jsx +++ b/app/components/Settings/scheduler/scheduler.jsx @@ -303,6 +303,7 @@ class Scheduler extends Component { Date: Fri, 6 Jul 2018 15:21:24 -0700 Subject: [PATCH 24/24] Fix bool proptypes warning --- app/components/Settings/scheduler/scheduler.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Settings/scheduler/scheduler.jsx b/app/components/Settings/scheduler/scheduler.jsx index fe6d5deeb..e25a79a4d 100644 --- a/app/components/Settings/scheduler/scheduler.jsx +++ b/app/components/Settings/scheduler/scheduler.jsx @@ -219,7 +219,7 @@ class Scheduler extends Component { render() { const rows = this.getRows(); - const loggedIn = this.props.requestor; + const loggedIn = Boolean(this.props.requestor); return (