diff --git a/caravel/assets/javascripts/explorev2/actions/exploreActions.js b/caravel/assets/javascripts/explorev2/actions/exploreActions.js
new file mode 100644
index 000000000000..e5e5c33eb4e1
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/actions/exploreActions.js
@@ -0,0 +1,192 @@
+const $ = window.$ = require('jquery');
+export const SET_DATASOURCE = 'SET_DATASOURCE';
+export const SET_VIZTYPE = 'SET_VIZTYPE';
+export const SET_TIME_COLUMN_OPTS = 'SET_TIME_COLUMN_OPTS';
+export const SET_TIME_GRAIN_OPTS = 'SET_TIME_GRAIN_OPTS';
+export const SET_TIME_COLUMN = 'SET_TIME_COLUMN';
+export const SET_TIME_GRAIN = 'SET_TIME_GRAIN';
+export const SET_SINCE = 'SET_SINCE';
+export const SET_UNTIL = 'SET_UNTIL';
+export const SET_GROUPBY_COLUMNS = 'SET_GROUPBY_COLUMNS';
+export const SET_GROUPBY_COLUMN_OPTS = 'SET_GROUPBY_COLUMN_OPTS';
+export const SET_METRICS = 'SET_METRICS';
+export const SET_METRICS_OPTS = 'SET_METRICS_OPTS';
+export const ADD_COLUMN = 'ADD_COLUMN';
+export const REMOVE_COLUMN = 'REMOVE_COLUMN';
+export const ADD_ORDERING = 'ADD_ORDERING';
+export const REMOVE_ORDERING = 'REMOVE_ORDERING';
+export const SET_TIME_STAMP = 'SET_TIME_STAMP';
+export const SET_ROW_LIMIT = 'SET_ROW_LIMIT';
+export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX';
+export const SET_FILTER_COLUMN_OPTS = 'SET_FILTER_COLUMN_OPTS';
+export const SET_WHERE_CLAUSE = 'SET_WHERE_CLAUSE';
+export const SET_HAVING_CLAUSE = 'SET_HAVING_CLAUSE';
+export const ADD_FILTER = 'ADD_FILTER';
+export const SET_FILTER = 'SET_FILTER';
+export const REMOVE_FILTER = 'REMOVE_FILTER';
+export const CHANGE_FILTER_FIELD = 'CHANGE_FILTER_FIELD';
+export const CHANGE_FILTER_OP = 'CHANGE_FILTER_OP';
+export const CHANGE_FILTER_VALUE = 'CHANGE_FILTER_VALUE';
+export const RESET_FORM_DATA = 'RESET_FORM_DATA';
+export const CLEAR_ALL_OPTS = 'CLEAR_ALL_OPTS';
+
+export function setTimeColumnOpts(timeColumnOpts) {
+ return { type: SET_TIME_COLUMN_OPTS, timeColumnOpts };
+}
+
+export function setTimeGrainOpts(timeGrainOpts) {
+ return { type: SET_TIME_GRAIN_OPTS, timeGrainOpts };
+}
+
+export function setGroupByColumnOpts(groupByColumnOpts) {
+ return { type: SET_GROUPBY_COLUMN_OPTS, groupByColumnOpts };
+}
+
+export function setMetricsOpts(metricsOpts) {
+ return { type: SET_METRICS_OPTS, metricsOpts };
+}
+
+export function setFilterColumnOpts(filterColumnOpts) {
+ return { type: SET_FILTER_COLUMN_OPTS, filterColumnOpts };
+}
+
+export function resetFormData() {
+ // Clear all form data when switching datasource
+ return { type: RESET_FORM_DATA };
+}
+
+export function clearAllOpts() {
+ return { type: CLEAR_ALL_OPTS };
+}
+
+export function setFormOpts(datasourceId, datasourceType) {
+ return function (dispatch) {
+ const timeColumnOpts = [];
+ const groupByColumnOpts = [];
+ const metricsOpts = [];
+ const filterColumnOpts = [];
+ const timeGrainOpts = [];
+
+ if (datasourceId) {
+ const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`];
+ const url = '/caravel/fetch_datasource_metadata?' + params.join('&');
+
+ $.get(url, (data, status) => {
+ if (status === 'success') {
+ data.dttm_cols.forEach((d) => {
+ if (d) timeColumnOpts.push({ value: d, label: d });
+ });
+ data.groupby_cols.forEach((d) => {
+ if (d) groupByColumnOpts.push({ value: d, label: d });
+ });
+ data.metrics.forEach((d) => {
+ if (d) metricsOpts.push({ value: d[1], label: d[0] });
+ });
+ data.filter_cols.forEach((d) => {
+ if (d) filterColumnOpts.push({ value: d, label: d });
+ });
+ data.time_grains.forEach((d) => {
+ if (d) timeGrainOpts.push({ value: d, label: d });
+ });
+ // Repopulate options for controls
+ dispatch(setTimeColumnOpts(timeColumnOpts));
+ dispatch(setTimeGrainOpts(timeGrainOpts));
+ dispatch(setGroupByColumnOpts(groupByColumnOpts));
+ dispatch(setMetricsOpts(metricsOpts));
+ dispatch(setFilterColumnOpts(filterColumnOpts));
+ }
+ });
+ } else {
+ // Clear all Select options
+ dispatch(clearAllOpts());
+ }
+ };
+}
+
+export function setDatasource(datasourceId) {
+ return { type: SET_DATASOURCE, datasourceId };
+}
+
+export function setVizType(vizType) {
+ return { type: SET_VIZTYPE, vizType };
+}
+
+export function setTimeColumn(timeColumn) {
+ return { type: SET_TIME_COLUMN, timeColumn };
+}
+
+export function setTimeGrain(timeGrain) {
+ return { type: SET_TIME_GRAIN, timeGrain };
+}
+
+export function setSince(since) {
+ return { type: SET_SINCE, since };
+}
+
+export function setUntil(until) {
+ return { type: SET_UNTIL, until };
+}
+
+export function setGroupByColumns(groupByColumns) {
+ return { type: SET_GROUPBY_COLUMNS, groupByColumns };
+}
+
+export function setMetrics(metrics) {
+ return { type: SET_METRICS, metrics };
+}
+
+export function addColumn(column) {
+ return { type: ADD_COLUMN, column };
+}
+
+export function removeColumn(column) {
+ return { type: REMOVE_COLUMN, column };
+}
+
+export function addOrdering(ordering) {
+ return { type: ADD_ORDERING, ordering };
+}
+
+export function removeOrdering(ordering) {
+ return { type: REMOVE_ORDERING, ordering };
+}
+
+export function setTimeStamp(timeStampFormat) {
+ return { type: SET_TIME_STAMP, timeStampFormat };
+}
+
+export function setRowLimit(rowLimit) {
+ return { type: SET_ROW_LIMIT, rowLimit };
+}
+
+export function toggleSearchBox(searchBox) {
+ return { type: TOGGLE_SEARCHBOX, searchBox };
+}
+
+export function setWhereClause(whereClause) {
+ return { type: SET_WHERE_CLAUSE, whereClause };
+}
+
+export function setHavingClause(havingClause) {
+ return { type: SET_HAVING_CLAUSE, havingClause };
+}
+
+export function addFilter(filter) {
+ return { type: ADD_FILTER, filter };
+}
+
+export function removeFilter(filter) {
+ return { type: REMOVE_FILTER, filter };
+}
+
+export function changeFilterField(filter, field) {
+ return { type: CHANGE_FILTER_FIELD, filter, field };
+}
+
+export function changeFilterOp(filter, op) {
+ return { type: CHANGE_FILTER_OP, filter, op };
+}
+
+export function changeFilterValue(filter, value) {
+ return { type: CHANGE_FILTER_VALUE, filter, value };
+}
diff --git a/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx b/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx
new file mode 100644
index 000000000000..3666e923a201
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/ChartContainer.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { Panel } from 'react-bootstrap';
+
+const ChartContainer = function () {
+ return (
+
+ chart goes here
+
+ );
+};
+export default ChartContainer;
diff --git a/caravel/assets/javascripts/explorev2/components/ChartControl.jsx b/caravel/assets/javascripts/explorev2/components/ChartControl.jsx
new file mode 100644
index 000000000000..eee6e0d6ff45
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/ChartControl.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import Select from 'react-select';
+import { bindActionCreators } from 'redux';
+import * as actions from '../actions/exploreActions';
+import { connect } from 'react-redux';
+import { VIZ_TYPES } from '../constants';
+
+const propTypes = {
+ actions: React.PropTypes.object,
+ datasources: React.PropTypes.array,
+ datasourceId: React.PropTypes.number,
+ datasourceType: React.PropTypes.string,
+ vizType: React.PropTypes.string,
+};
+
+const defaultProps = {
+ datasources: [],
+ datasourceId: null,
+ datasourceType: null,
+ vizType: null,
+};
+
+class ChartControl extends React.Component {
+ componentWillMount() {
+ if (this.props.datasourceId) {
+ this.props.actions.setFormOpts(this.props.datasourceId, this.props.datasourceType);
+ }
+ }
+ changeDatasource(datasourceOpt) {
+ const val = (datasourceOpt) ? datasourceOpt.value : null;
+ this.props.actions.setDatasource(val);
+ this.props.actions.resetFormData();
+ this.props.actions.setFormOpts(val, this.props.datasourceType);
+ }
+ changeViz(vizOpt) {
+ const val = (vizOpt) ? vizOpt.value : null;
+ this.props.actions.setVizType(val);
+ }
+ render() {
+ return (
+
+
Chart Options
+
+
Datasource
+
+
+
Viz Type
+
+
+
+
+
+ );
+ }
+}
+
+ChartControl.propTypes = propTypes;
+ChartControl.defaultProps = defaultProps;
+
+function mapStateToProps(state) {
+ return {
+ datasources: state.datasources,
+ datasourceId: state.datasourceId,
+ datasourceType: state.datasourceType,
+ vizType: state.vizType,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+export default connect(mapStateToProps, mapDispatchToProps)(ChartControl);
diff --git a/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx b/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx
new file mode 100644
index 000000000000..c3f70e9532a6
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Panel } from 'react-bootstrap';
+import TimeFilter from './TimeFilter';
+import ChartControl from './ChartControl';
+import GroupBy from './GroupBy';
+import SqlClause from './SqlClause';
+import Filters from './Filters';
+
+const ControlPanelsContainer = function () {
+ return (
+
+
+
+
+
+
+
+ );
+};
+export default ControlPanelsContainer;
diff --git a/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx b/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
new file mode 100644
index 000000000000..f6b6b52faae3
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/ExploreViewContainer.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import ChartContainer from './ChartContainer';
+import ControlPanelsContainer from './ControlPanelsContainer';
+import QueryAndSaveButtons from './QueryAndSaveButtons';
+
+const ExploreViewContainer = function () {
+ return (
+
+
+
+ { console.log('clicked query'); }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ExploreViewContainer;
diff --git a/caravel/assets/javascripts/explorev2/components/Filters.jsx b/caravel/assets/javascripts/explorev2/components/Filters.jsx
new file mode 100644
index 000000000000..49e384157ff6
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/Filters.jsx
@@ -0,0 +1,128 @@
+import React from 'react';
+// import { Tab, Row, Col, Nav, NavItem } from 'react-bootstrap';
+import Select from 'react-select';
+import { Button } from 'react-bootstrap';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as actions from '../actions/exploreActions';
+import shortid from 'shortid';
+
+const propTypes = {
+ actions: React.PropTypes.object,
+ filterColumnOpts: React.PropTypes.array,
+ filters: React.PropTypes.array,
+};
+
+const defaultProps = {
+ filterColumnOpts: [],
+ filters: [],
+};
+
+class Filters extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ opOpts: ['in', 'not in'],
+ };
+ }
+ changeField(filter, fieldOpt) {
+ const val = (fieldOpt) ? fieldOpt.value : null;
+ this.props.actions.changeFilterField(filter, val);
+ }
+ changeOp(filter, opOpt) {
+ const val = (opOpt) ? opOpt.value : null;
+ this.props.actions.changeFilterOp(filter, val);
+ }
+ changeValue(filter, value) {
+ this.props.actions.changeFilterValue(filter, value);
+ }
+ removeFilter(filter) {
+ this.props.actions.removeFilter(filter);
+ }
+ addFilter() {
+ this.props.actions.addFilter({
+ id: shortid.generate(),
+ field: null,
+ op: null,
+ value: null,
+ });
+ }
+ render() {
+ const filters = this.props.filters.map((filter) => (
+
+
+
+
+
+ )
+ );
+ return (
+
+
Filters
+
+ {filters}
+
+
+
+ );
+ }
+}
+
+Filters.propTypes = propTypes;
+
+Filters.defaultProps = defaultProps;
+
+function mapStateToProps(state) {
+ return {
+ filterColumnOpts: state.filterColumnOpts,
+ filters: state.filters,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Filters);
diff --git a/caravel/assets/javascripts/explorev2/components/GroupBy.jsx b/caravel/assets/javascripts/explorev2/components/GroupBy.jsx
new file mode 100644
index 000000000000..cbf10fdfe59e
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/GroupBy.jsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import Select from 'react-select';
+import { bindActionCreators } from 'redux';
+import * as actions from '../actions/exploreActions';
+import { connect } from 'react-redux';
+
+const propTypes = {
+ actions: React.PropTypes.object,
+ metricsOpts: React.PropTypes.array,
+ metrics: React.PropTypes.array,
+ groupByColumnOpts: React.PropTypes.array,
+ groupByColumns: React.PropTypes.array,
+};
+
+const defaultProps = {
+ metricsOpts: [],
+ metrics: [],
+ groupByColumnOpts: [],
+ groupByColumns: [],
+};
+
+class GroupBy extends React.Component {
+ changeColumns(groupByColumnOpts) {
+ this.props.actions.setGroupByColumns(groupByColumnOpts);
+ }
+ changeMetrics(metricsOpts) {
+ this.props.actions.setMetrics(metricsOpts);
+ }
+ render() {
+ return (
+
+
GroupBy
+
+
+
GroupBy Column
+
+
+
+
Metrics
+
+
+
+
+ );
+ }
+}
+
+GroupBy.propTypes = propTypes;
+GroupBy.defaultProps = defaultProps;
+
+function mapStateToProps(state) {
+ return {
+ metricsOpts: state.metricsOpts,
+ metrics: state.metrics,
+ groupByColumnOpts: state.groupByColumnOpts,
+ groupByColumns: state.groupByColumns,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(GroupBy);
diff --git a/caravel/assets/javascripts/explorev2/components/QueryAndSaveButtons.jsx b/caravel/assets/javascripts/explorev2/components/QueryAndSaveButtons.jsx
new file mode 100644
index 000000000000..1a521393402f
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/QueryAndSaveButtons.jsx
@@ -0,0 +1,31 @@
+import React, { PropTypes } from 'react';
+import classnames from 'classnames';
+
+const propTypes = {
+ canAdd: PropTypes.string.isRequired,
+ onQuery: PropTypes.func.isRequired,
+};
+
+export default function QueryAndSaveBtns({ canAdd, onQuery }) {
+ const saveClasses = classnames('btn btn-default btn-sm', {
+ 'disabled disabledButton': canAdd !== 'True',
+ });
+
+ return (
+
+
+
+
+ );
+}
+
+QueryAndSaveBtns.propTypes = propTypes;
diff --git a/caravel/assets/javascripts/explorev2/components/SqlClause.jsx b/caravel/assets/javascripts/explorev2/components/SqlClause.jsx
new file mode 100644
index 000000000000..ab484dfe20ab
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/SqlClause.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { bindActionCreators } from 'redux';
+import * as actions from '../actions/exploreActions';
+import { connect } from 'react-redux';
+
+const propTypes = {
+ actions: React.PropTypes.object,
+};
+
+class SqlClause extends React.Component {
+ changeWhere(whereClause) {
+ this.props.actions.setWhereClause(whereClause);
+ }
+ changeHaving(havingClause) {
+ this.props.actions.setHavingClause(havingClause);
+ }
+ render() {
+ return (
+
+ );
+ }
+}
+
+SqlClause.propTypes = propTypes;
+
+function mapStateToProps() {
+ return {};
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SqlClause);
diff --git a/caravel/assets/javascripts/explorev2/components/TimeFilter.jsx b/caravel/assets/javascripts/explorev2/components/TimeFilter.jsx
new file mode 100644
index 000000000000..2cbf7afedea8
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/components/TimeFilter.jsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import Select from 'react-select';
+import { bindActionCreators } from 'redux';
+import * as actions from '../actions/exploreActions';
+import { connect } from 'react-redux';
+import { sinceOptions, untilOptions } from '../constants';
+
+const propTypes = {
+ actions: React.PropTypes.object,
+ timeColumnOpts: React.PropTypes.array,
+ timeColumn: React.PropTypes.string,
+ timeGrainOpts: React.PropTypes.array,
+ timeGrain: React.PropTypes.string,
+ since: React.PropTypes.string,
+ until: React.PropTypes.string,
+};
+
+const defaultProps = {
+ timeColumnOpts: [],
+ timeColumn: null,
+ timeGrainOpts: [],
+ timeGrain: null,
+ since: null,
+ until: null,
+};
+
+class TimeFilter extends React.Component {
+ changeTimeColumn(timeColumnOpt) {
+ const val = (timeColumnOpt) ? timeColumnOpt.value : null;
+ this.props.actions.setTimeColumn(val);
+ }
+ changeTimeGrain(timeGrainOpt) {
+ const val = (timeGrainOpt) ? timeGrainOpt.value : null;
+ this.props.actions.setTimeGrain(val);
+ }
+ changeSince(sinceOpt) {
+ const val = (sinceOpt) ? sinceOpt.value : null;
+ this.props.actions.setSince(val);
+ }
+ changeUntil(untilOpt) {
+ const val = (untilOpt) ? untilOpt.value : null;
+ this.props.actions.setUntil(val);
+ }
+ render() {
+ return (
+
+
Time Filter
+
+
+
Time Column & Grain
+
+
+
+
+
Since - Until
+
+
+
+ );
+ }
+}
+
+TimeFilter.propTypes = propTypes;
+TimeFilter.defaultProps = defaultProps;
+
+function mapStateToProps(state) {
+ return {
+ timeColumnOpts: state.timeColumnOpts,
+ timeColumn: state.timeColumn,
+ timeGrainOpts: state.timeGrainOpts,
+ timeGrain: state.timeGrain,
+ since: state.since,
+ until: state.until,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(actions, dispatch),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(TimeFilter);
diff --git a/caravel/assets/javascripts/explorev2/constants.js b/caravel/assets/javascripts/explorev2/constants.js
new file mode 100644
index 000000000000..2415eaa43e5b
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/constants.js
@@ -0,0 +1,35 @@
+export const VIZ_TYPES = [
+ { value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
+ { value: 'pie', label: 'Pie Chart', requiresTime: false },
+ { value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
+ { value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
+ { value: 'compare', label: 'Time Series - Percent Change', requiresTime: true },
+ { value: 'area', label: 'Time Series - Stacked', requiresTime: true },
+ { value: 'table', label: 'Table View', requiresTime: false },
+ { value: 'markup', label: 'Markup', requiresTime: false },
+ { value: 'pivot_table', label: 'Pivot Table', requiresTime: false },
+ { value: 'separator', label: 'Separator', requiresTime: false },
+ { value: 'word_cloud', label: 'Word Cloud', requiresTime: false },
+ { value: 'treemap', label: 'Treemap', requiresTime: false },
+ { value: 'cal_heatmap', label: 'Calendar Heatmap', requiresTime: true },
+ { value: 'box_plot', label: 'Box Plot', requiresTime: false },
+ { value: 'bubble', label: 'Bubble Chart', requiresTime: false },
+ { value: 'big_number', label: 'Big Number with Trendline', requiresTime: false },
+ { value: 'bubble', label: 'Bubble Chart', requiresTime: false },
+ { value: 'histogram', label: 'Histogram', requiresTime: false },
+ { value: 'sunburst', label: 'Sunburst', requiresTime: false },
+ { value: 'sankey', label: 'Sankey', requiresTime: false },
+ { value: 'directed_force', label: 'Directed Force Layout', requiresTime: false },
+ { value: 'world_map', label: 'World Map', requiresTime: false },
+ { value: 'filter_box', label: 'Filter Box', requiresTime: false },
+ { value: 'iframe', label: 'iFrame', requiresTime: false },
+ { value: 'para', label: 'Parallel Coordinates', requiresTime: false },
+ { value: 'heatmap', label: 'Heatmap', requiresTime: false },
+ { value: 'horizon', label: 'Horizon', requiresTime: false },
+ { value: 'mapbox', label: 'Mapbox', requiresTime: false },
+];
+
+export const sinceOptions = ['1 hour ago', '12 hours ago', '1 day ago',
+ '7 days ago', '28 days ago', '90 days ago', '1 year ago'];
+export const untilOptions = ['now', '1 day ago', '7 days ago',
+ '28 days ago', '90 days ago', '1 year ago'];
diff --git a/caravel/assets/javascripts/explorev2/index.jsx b/caravel/assets/javascripts/explorev2/index.jsx
new file mode 100644
index 000000000000..e478ee5a406c
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/index.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import ExploreViewContainer from './components/ExploreViewContainer';
+
+import { createStore, applyMiddleware, compose } from 'redux';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+
+import { initialState } from './stores/store';
+
+const exploreViewContainer = document.getElementById('js-explore-view-container');
+const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
+
+import { exploreReducer } from './reducers/exploreReducer';
+
+const bootstrappedState = Object.assign(initialState, {
+ datasources: bootstrapData.datasources,
+ datasourceId: parseInt(bootstrapData.datasource_id, 10),
+ datasourceType: bootstrapData.datasource_type,
+ sliceName: bootstrapData.viz.form_data.slice_name,
+ sliceId: bootstrapData.viz.form_data.slice_id,
+ vizType: bootstrapData.viz.form_data.viz_type,
+ timeColumn: bootstrapData.viz.form_data.granularity_sqla,
+ timeGrain: bootstrapData.viz.form_data.time_grain_sqla,
+ metrics: [bootstrapData.viz.form_data.metric].map((m) => ({ value: m, label: m })),
+ since: bootstrapData.viz.form_data.since,
+ until: bootstrapData.viz.form_data.until,
+ havingClause: bootstrapData.viz.form_data.having,
+ whereClause: bootstrapData.viz.form_data.where,
+});
+const store = createStore(exploreReducer, bootstrappedState,
+ compose(applyMiddleware(thunk))
+);
+
+ReactDOM.render(
+
+
+ ,
+ exploreViewContainer
+);
+
diff --git a/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js b/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js
new file mode 100644
index 000000000000..ffef2dac4101
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/reducers/exploreReducer.js
@@ -0,0 +1,111 @@
+import { defaultFormData, defaultOpts } from '../stores/store';
+import * as actions from '../actions/exploreActions';
+import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils';
+
+export const exploreReducer = function (state, action) {
+ const actionHandlers = {
+ [actions.SET_DATASOURCE]() {
+ return Object.assign({}, state, { datasourceId: action.datasourceId });
+ },
+ [actions.SET_VIZTYPE]() {
+ return Object.assign({}, state, { vizType: action.vizType });
+ },
+ [actions.SET_TIME_COLUMN_OPTS]() {
+ return Object.assign({}, state, { timeColumnOpts: action.timeColumnOpts });
+ },
+ [actions.SET_TIME_GRAIN_OPTS]() {
+ return Object.assign({}, state, { timeGrainOpts: action.timeGrainOpts });
+ },
+ [actions.SET_TIME_COLUMN]() {
+ return Object.assign({}, state, { timeColumn: action.timeColumn });
+ },
+ [actions.SET_TIME_GRAIN]() {
+ return Object.assign({}, state, { timeGrain: action.timeGrain });
+ },
+ [actions.SET_SINCE]() {
+ return Object.assign({}, state, { since: action.since });
+ },
+ [actions.SET_UNTIL]() {
+ return Object.assign({}, state, { until: action.until });
+ },
+ [actions.SET_GROUPBY_COLUMN_OPTS]() {
+ return Object.assign({}, state, { groupByColumnOpts: action.groupByColumnOpts });
+ },
+ [actions.SET_GROUPBY_COLUMNS]() {
+ return Object.assign({}, state, { groupByColumns: action.groupByColumns });
+ },
+ [actions.SET_METRICS_OPTS]() {
+ return Object.assign({}, state, { metricsOpts: action.metricsOpts });
+ },
+ [actions.SET_METRICS]() {
+ return Object.assign({}, state, { metrics: action.metrics });
+ },
+ [actions.ADD_COLUMN]() {
+ return Object.assign({}, state, { columns: [...state.columns, action.column] });
+ },
+ [actions.REMOVE_COLUMN]() {
+ const newColumns = [];
+ state.columns.forEach((c) => {
+ if (c !== action.column) {
+ newColumns.push(c);
+ }
+ });
+ return Object.assign({}, state, { columns: newColumns });
+ },
+ [actions.ADD_ORDERING]() {
+ return Object.assign({}, state, { orderings: [...state.orderings, action.ordering] });
+ },
+ [actions.REMOVE_ORDERING]() {
+ const newOrderings = [];
+ state.orderings.forEach((o) => {
+ if (o !== action.ordering) {
+ newOrderings.push(o);
+ }
+ });
+ return Object.assign({}, state, { orderings: newOrderings });
+ },
+ [actions.SET_TIME_STAMP]() {
+ return Object.assign({}, state, { timeStampFormat: action.timeStampFormat });
+ },
+ [actions.SET_ROW_LIMIT]() {
+ return Object.assign({}, state, { rowLimit: action.rowLimit });
+ },
+ [actions.TOGGLE_SEARCHBOX]() {
+ return Object.assign({}, state, { searchBox: action.searchBox });
+ },
+ [actions.SET_WHERE_CLAUSE]() {
+ return Object.assign({}, state, { whereClause: action.whereClause });
+ },
+ [actions.SET_HAVING_CLAUSE]() {
+ return Object.assign({}, state, { havingClause: action.havingClause });
+ },
+ [actions.SET_FILTER_COLUMN_OPTS]() {
+ return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts });
+ },
+ [actions.ADD_FILTER]() {
+ return addToArr(state, 'filters', action.filter);
+ },
+ [actions.REMOVE_FILTER]() {
+ return removeFromArr(state, 'filters', action.filter);
+ },
+ [actions.CHANGE_FILTER_FIELD]() {
+ return alterInArr(state, 'filters', action.filter, { field: action.field });
+ },
+ [actions.CHANGE_FILTER_OP]() {
+ return alterInArr(state, 'filters', action.filter, { op: action.op });
+ },
+ [actions.CHANGE_FILTER_VALUE]() {
+ return alterInArr(state, 'filters', action.filter, { value: action.value });
+ },
+ [actions.RESET_FORM_DATA]() {
+ return Object.assign({}, state, defaultFormData);
+ },
+ [actions.CLEAR_ALL_OPTS]() {
+ return Object.assign({}, state, defaultOpts);
+ },
+ };
+ if (action.type in actionHandlers) {
+ return actionHandlers[action.type]();
+ }
+ return state;
+};
diff --git a/caravel/assets/javascripts/explorev2/stores/store.js b/caravel/assets/javascripts/explorev2/stores/store.js
new file mode 100644
index 000000000000..80d1bd7296c9
--- /dev/null
+++ b/caravel/assets/javascripts/explorev2/stores/store.js
@@ -0,0 +1,52 @@
+export const initialState = {
+ datasources: null,
+ datasourceId: null,
+ datasourceType: null,
+ vizType: null,
+ timeColumnOpts: [],
+ timeColumn: null,
+ timeGrainOpts: [],
+ timeGrain: null,
+ since: null,
+ until: null,
+ groupByColumnOpts: [],
+ groupByColumns: [],
+ metricsOpts: [],
+ metrics: [],
+ columns: [],
+ orderings: [],
+ timeStampFormat: null,
+ rowLimit: null,
+ searchBox: false,
+ whereClause: '',
+ havingClause: '',
+ filters: [],
+ filterColumnOpts: [],
+};
+
+// TODO: add datasource_type here after druid support is added
+export const defaultFormData = {
+ vizType: null,
+ timeColumn: null,
+ timeGrain: null,
+ since: null,
+ until: null,
+ groupByColumns: [],
+ metrics: [],
+ columns: [],
+ orderings: [],
+ timeStampFormat: null,
+ rowLimit: null,
+ searchBox: false,
+ whereClause: '',
+ havingClause: '',
+ filters: [],
+};
+
+export const defaultOpts = {
+ timeColumnOpts: [],
+ timeGrainOpts: [],
+ groupByColumnOpts: [],
+ metricsOpts: [],
+ filterColumnOpts: [],
+};
diff --git a/caravel/assets/package.json b/caravel/assets/package.json
index fa678959aa72..f70a924eaf90 100644
--- a/caravel/assets/package.json
+++ b/caravel/assets/package.json
@@ -77,6 +77,7 @@
"reactable": "^0.14.0",
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
+ "redux-thunk": "^2.1.0",
"select2": "3.5",
"select2-bootstrap-css": "^1.4.6",
"shortid": "^2.2.6",
@@ -101,9 +102,9 @@
"eslint-plugin-jsx-a11y": "^1.2.0",
"eslint-plugin-react": "^5.2.2",
"exports-loader": "^0.6.3",
- "istanbul": "^1.0.0-alpha",
"file-loader": "^0.8.5",
"imports-loader": "^0.6.5",
+ "istanbul": "^1.0.0-alpha",
"jsdom": "^8.0.1",
"json-loader": "^0.5.4",
"less": "^2.6.1",
diff --git a/caravel/assets/spec/javascripts/explore/components/actions_spec.js b/caravel/assets/spec/javascripts/explore/components/actions_spec.js
new file mode 100644
index 000000000000..06cb77e4d9ee
--- /dev/null
+++ b/caravel/assets/spec/javascripts/explore/components/actions_spec.js
@@ -0,0 +1,91 @@
+import { it, describe } from 'mocha';
+import { expect } from 'chai';
+import shortid from 'shortid';
+import * as actions from '../../../../javascripts/explorev2/actions/exploreActions';
+import { initialState } from '../../../../javascripts/explorev2/stores/store';
+import { exploreReducer } from '../../../../javascripts/explorev2/reducers/exploreReducer';
+
+describe('reducers', () => {
+ it('should return new state with datasource id', () => {
+ const newState = exploreReducer(initialState, actions.setDatasource(1));
+ expect(newState.datasourceId).to.equal(1);
+ });
+
+ it('should return new state with viz type', () => {
+ const newState = exploreReducer(initialState, actions.setVizType('bar'));
+ expect(newState.vizType).to.equal('bar');
+ });
+
+ it('should return new state with added column', () => {
+ const newColumn = 'col';
+ const newState = exploreReducer(initialState, actions.addColumn(newColumn));
+ expect(newState.columns).to.deep.equal([newColumn]);
+ });
+
+ it('should return new state with removed column', () => {
+ const testState = { initialState, columns: ['col1', 'col2'] };
+ const remColumn = 'col1';
+ const newState = exploreReducer(testState, actions.removeColumn(remColumn));
+ expect(newState.columns).to.deep.equal(['col2']);
+ });
+
+ it('should return new state with added ordering', () => {
+ const newOrdering = 'ord';
+ const newState = exploreReducer(initialState, actions.addOrdering(newOrdering));
+ expect(newState.orderings).to.deep.equal(['ord']);
+ });
+
+ it('should return new state with removed ordering', () => {
+ const testState = { initialState, orderings: ['ord1', 'ord2'] };
+ const remOrdering = 'ord1';
+ const newState = exploreReducer(testState, actions.removeOrdering(remOrdering));
+ expect(newState.orderings).to.deep.equal(['ord2']);
+ });
+
+ it('should return new state with time stamp', () => {
+ const newState = exploreReducer(initialState, actions.setTimeStamp(1));
+ expect(newState.timeStampFormat).to.equal(1);
+ });
+
+ it('should return new state with row limit', () => {
+ const newState = exploreReducer(initialState, actions.setRowLimit(10));
+ expect(newState.rowLimit).to.equal(10);
+ });
+
+ it('should return new state with search box toggled', () => {
+ const newState = exploreReducer(initialState, actions.toggleSearchBox(true));
+ expect(newState.searchBox).to.equal(true);
+ });
+
+ it('should return new state with added filter', () => {
+ const newFilter = {
+ id: shortid.generate(),
+ eq: 'value',
+ op: 'in',
+ col: 'vals',
+ };
+ const newState = exploreReducer(initialState, actions.addFilter(newFilter));
+ expect(newState.filters).to.deep.equal([newFilter]);
+ });
+
+ it('should return new state with removed filter', () => {
+ const filter1 = {
+ id: shortid.generate(),
+ eq: 'value',
+ op: 'in',
+ col: 'vals1',
+ };
+ const filter2 = {
+ id: shortid.generate(),
+ eq: 'value',
+ op: 'not in',
+ col: 'vals2',
+ };
+ const testState = {
+ initialState,
+ filters: [filter1, filter2],
+ };
+ const newState = exploreReducer(testState, actions.removeFilter(filter1));
+ expect(newState.filters).to.deep.equal([filter2]);
+ });
+});
diff --git a/caravel/assets/utils/reducerUtils.js b/caravel/assets/utils/reducerUtils.js
new file mode 100644
index 000000000000..e233c2413b9a
--- /dev/null
+++ b/caravel/assets/utils/reducerUtils.js
@@ -0,0 +1,53 @@
+import shortid from 'shortid';
+
+export function addToObject(state, arrKey, obj) {
+ const newObject = Object.assign({}, state[arrKey]);
+ const copiedObject = Object.assign({}, obj);
+
+ if (!copiedObject.id) {
+ copiedObject.id = shortid.generate();
+ }
+ newObject[copiedObject.id] = copiedObject;
+ return Object.assign({}, state, { [arrKey]: newObject });
+}
+
+export function alterInObject(state, arrKey, obj, alterations) {
+ const newObject = Object.assign({}, state[arrKey]);
+ newObject[obj.id] = (Object.assign({}, newObject[obj.id], alterations));
+ return Object.assign({}, state, { [arrKey]: newObject });
+}
+
+export function alterInArr(state, arrKey, obj, alterations) {
+ // Finds an item in an array in the state and replaces it with a
+ // new object with an altered property
+ const idKey = 'id';
+ const newArr = [];
+ state[arrKey].forEach((arrItem) => {
+ if (obj[idKey] === arrItem[idKey]) {
+ newArr.push(Object.assign({}, arrItem, alterations));
+ } else {
+ newArr.push(arrItem);
+ }
+ });
+ return Object.assign({}, state, { [arrKey]: newArr });
+}
+
+export function removeFromArr(state, arrKey, obj, idKey = 'id') {
+ const newArr = [];
+ state[arrKey].forEach((arrItem) => {
+ if (!(obj[idKey] === arrItem[idKey])) {
+ newArr.push(arrItem);
+ }
+ });
+ return Object.assign({}, state, { [arrKey]: newArr });
+}
+
+export function addToArr(state, arrKey, obj) {
+ const newObj = Object.assign({}, obj);
+ if (!newObj.id) {
+ newObj.id = shortid.generate();
+ }
+ const newState = {};
+ newState[arrKey] = [...state[arrKey], newObj];
+ return Object.assign({}, state, newState);
+}
diff --git a/caravel/assets/webpack.config.js b/caravel/assets/webpack.config.js
index 7b851e2e0e8c..1667e016f117 100644
--- a/caravel/assets/webpack.config.js
+++ b/caravel/assets/webpack.config.js
@@ -15,6 +15,7 @@ const config = {
'css-theme': APP_DIR + '/javascripts/css-theme.js',
dashboard: APP_DIR + '/javascripts/dashboard/Dashboard.jsx',
explore: APP_DIR + '/javascripts/explore/explore.jsx',
+ explorev2: APP_DIR + '/javascripts/explorev2/index.jsx',
welcome: APP_DIR + '/javascripts/welcome.js',
standalone: APP_DIR + '/javascripts/standalone.js',
common: APP_DIR + '/javascripts/common.js',
diff --git a/caravel/templates/caravel/explorev2.html b/caravel/templates/caravel/explorev2.html
new file mode 100644
index 000000000000..57e0c3a4c7fa
--- /dev/null
+++ b/caravel/templates/caravel/explorev2.html
@@ -0,0 +1,15 @@
+{% extends "caravel/basic.html" %}
+
+{% block body %}
+
+{% endblock %}
+
+{% block tail_js %}
+ {{ super() }}
+ {% with filename="explorev2" %}
+ {% include "caravel/partials/_script_tag.html" %}
+ {% endwith %}
+{% endblock %}
diff --git a/caravel/views.py b/caravel/views.py
index baba51a9388c..11e6a6901f2d 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -1201,6 +1201,113 @@ def explore(self, datasource_type, datasource_id, slice_id=None):
can_download=slice_download_perm,
userid=g.user.get_id() if g.user else '')
+ @has_access
+ @expose("/exploreV2////")
+ @expose("/exploreV2///")
+ @log_this
+ def exploreV2(self, datasource_type, datasource_id, slice_id=None):
+ error_redirect = '/slicemodelview/list/'
+ datasource_class = SourceRegistry.sources[datasource_type]
+ datasources = db.session.query(datasource_class).all()
+ datasources = sorted(datasources, key=lambda ds: ds.full_name)
+ datasource = [ds for ds in datasources if int(datasource_id) == ds.id]
+ datasource = datasource[0] if datasource else None
+
+ if not datasource:
+ flash(DATASOURCE_MISSING_ERR, "alert")
+ return redirect(error_redirect)
+
+ if not self.datasource_access(datasource):
+ flash(
+ __(get_datasource_access_error_msg(datasource.name)), "danger")
+ return redirect('caravel/request_access_form/{}/{}/{}'.format(
+ datasource_type, datasource_id, datasource.name))
+
+ request_args_multi_dict = request.args # MultiDict
+
+ slice_id = slice_id or request_args_multi_dict.get("slice_id")
+ slc = None
+ # build viz_obj and get it's params
+ if slice_id:
+ slc = db.session.query(models.Slice).filter_by(id=slice_id).first()
+ try:
+ viz_obj = slc.get_viz(
+ url_params_multidict=request_args_multi_dict)
+ except Exception as e:
+ logging.exception(e)
+ flash(utils.error_msg_from_exception(e), "danger")
+ return redirect(error_redirect)
+ else:
+ viz_type = request_args_multi_dict.get("viz_type")
+ if not viz_type and datasource.default_endpoint:
+ return redirect(datasource.default_endpoint)
+ # default to table if no default endpoint and no viz_type
+ viz_type = viz_type or "table"
+ # validate viz params
+ try:
+ viz_obj = viz.viz_types[viz_type](
+ datasource, request_args_multi_dict)
+ except Exception as e:
+ logging.exception(e)
+ flash(utils.error_msg_from_exception(e), "danger")
+ return redirect(error_redirect)
+ slice_params_multi_dict = ImmutableMultiDict(viz_obj.orig_form_data)
+
+ # slc perms
+ slice_add_perm = self.can_access('can_add', 'SliceModelView')
+ slice_edit_perm = check_ownership(slc, raise_if_false=False)
+ slice_download_perm = self.can_access('can_download', 'SliceModelView')
+
+ # handle save or overwrite
+ action = slice_params_multi_dict.get('action')
+ if action in ('saveas', 'overwrite'):
+ return self.save_or_overwrite_slice(
+ slice_params_multi_dict, slc, slice_add_perm, slice_edit_perm)
+
+ # handle different endpoints
+ if slice_params_multi_dict.get("json") == "true":
+ if config.get("DEBUG"):
+ # Allows for nice debugger stack traces in debug mode
+ return Response(
+ viz_obj.get_json(),
+ status=200,
+ mimetype="application/json")
+ try:
+ return Response(
+ viz_obj.get_json(),
+ status=200,
+ mimetype="application/json")
+ except Exception as e:
+ logging.exception(e)
+ return json_error_response(utils.error_msg_from_exception(e))
+
+ elif slice_params_multi_dict.get("csv") == "true":
+ payload = viz_obj.get_csv()
+ return Response(
+ payload,
+ status=200,
+ headers=generate_download_headers("csv"),
+ mimetype="application/csv")
+ else:
+ bootstrap_data = {
+ "can_add": slice_add_perm,
+ "can_download": slice_download_perm,
+ "can_edit": slice_edit_perm,
+ # TODO: separate endpoint for fetching datasources
+ "datasources": [(d.id, d.full_name) for d in datasources],
+ "datasource_id": datasource_id,
+ "datasource_type": datasource_type,
+ "user_id": g.user.get_id() if g.user else None,
+ "viz": json.loads(viz_obj.get_json())
+ }
+ if slice_params_multi_dict.get("standalone") == "true":
+ template = "caravel/standalone.html"
+ else:
+ template = "caravel/explorev2.html"
+ return self.render_template(
+ template,
+ bootstrap_data=json.dumps(bootstrap_data))
+
def save_or_overwrite_slice(
self, args, slc, slice_add_perm, slice_edit_perm):
"""Save or overwrite a slice"""
@@ -1831,6 +1938,34 @@ def csv(self, client_id):
'attachment; filename={}.csv'.format(query.name))
return response
+ @has_access
+ @expose("/fetch_datasource_metadata")
+ @log_this
+ def fetch_datasource_metadata(self):
+ # TODO: check permissions
+ # TODO: check if datasource exits
+ session = db.session
+ datasource_type = request.args.get('datasource_type')
+ datasource_class = SourceRegistry.sources[datasource_type]
+ datasource = (
+ session.query(datasource_class)
+ .filter_by(id=request.args.get('datasource_id'))
+ .first()
+ )
+ # SUPPORT DRUID
+ # TODO: move this logic to the model (maybe)
+ datasource_grains = datasource.database.grains()
+ grain_names = [str(grain.name) for grain in datasource_grains]
+ form_data = {
+ "dttm_cols": datasource.dttm_cols,
+ "time_grains": grain_names,
+ "groupby_cols": datasource.groupby_column_names,
+ "metrics": datasource.metrics_combo,
+ "filter_cols": datasource.filterable_column_names,
+ }
+ return Response(
+ json.dumps(form_data), mimetype="application/json")
+
@has_access
@expose("/queries/")
@log_this