mirror of https://github.com/apache/superset.git
[explore-v2] setup, basic layout, control panels, v2 url (#1233)
* Explore control panel - Chart control, TimeFilter, GroupBy, Filters (#1205) * create structure for new forked explore view (#1099) * create structure for new forked explore view * update component name * add bootstrap data pattern * remove console.log * Associate version to entry files (#1060) * Associate version to entry files * Modified path joins for configs * Made changes based on comments * Created store and reducers (#1108) * Created store and reducers * Added spec * Modifications based on comments * Explore control panel components: Chart control, Time filter, SQL, GroupBy and Filters * Modifications based on comments * accommodate old and new explore urls * move bootstrap data up in scope * fix code climate issues * fix long lines * fix syntax error
This commit is contained in:
parent
d8638dbcf3
commit
e6e902e8df
|
@ -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 };
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Panel } from 'react-bootstrap';
|
||||
|
||||
const ChartContainer = function () {
|
||||
return (
|
||||
<Panel header="Chart title">
|
||||
chart goes here
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
export default ChartContainer;
|
|
@ -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 (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">Chart Options</div>
|
||||
<div className="panel-body">
|
||||
<h5 className="section-heading">Datasource</h5>
|
||||
<div className="row">
|
||||
<Select
|
||||
name="select-datasource"
|
||||
placeholder="Select a datasource"
|
||||
options={this.props.datasources.map((d) => ({ value: d[0], label: d[1] }))}
|
||||
value={this.props.datasourceId}
|
||||
autosize={false}
|
||||
onChange={this.changeDatasource.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<h5 className="section-heading">Viz Type</h5>
|
||||
<div className="row">
|
||||
<Select
|
||||
name="select-viztype"
|
||||
placeholder="Select a viz type"
|
||||
options={VIZ_TYPES}
|
||||
value={this.props.vizType}
|
||||
autosize={false}
|
||||
onChange={this.changeViz.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
|
@ -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 (
|
||||
<Panel>
|
||||
<ChartControl />
|
||||
<TimeFilter />
|
||||
<GroupBy />
|
||||
<SqlClause />
|
||||
<Filters />
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
export default ControlPanelsContainer;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import ChartContainer from './ChartContainer';
|
||||
import ControlPanelsContainer from './ControlPanelsContainer';
|
||||
import QueryAndSaveButtons from './QueryAndSaveButtons';
|
||||
|
||||
const ExploreViewContainer = function () {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-sm-3">
|
||||
<QueryAndSaveButtons
|
||||
canAdd="True"
|
||||
onQuery={() => { console.log('clicked query'); }}
|
||||
/>
|
||||
<br /><br />
|
||||
<ControlPanelsContainer />
|
||||
</div>
|
||||
<div className="col-sm-9">
|
||||
<ChartContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreViewContainer;
|
|
@ -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) => (
|
||||
<div>
|
||||
<Select
|
||||
className="row"
|
||||
multi={false}
|
||||
name="select-column"
|
||||
placeholder="Select column"
|
||||
options={this.props.filterColumnOpts}
|
||||
value={filter.field}
|
||||
autosize={false}
|
||||
onChange={this.changeField.bind(this, filter)}
|
||||
/>
|
||||
<div className="row">
|
||||
<Select
|
||||
className="col-sm-3"
|
||||
multi={false}
|
||||
name="select-op"
|
||||
placeholder="Select operator"
|
||||
options={this.state.opOpts.map((o) => ({ value: o, label: o }))}
|
||||
value={filter.op}
|
||||
autosize={false}
|
||||
onChange={this.changeOp.bind(this, filter)}
|
||||
/>
|
||||
<div className="col-sm-6">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeValue.bind(this, filter)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter value"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.removeFilter.bind(this, filter)}
|
||||
>
|
||||
<i className="fa fa-minus" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">Filters</div>
|
||||
<div className="panel-body">
|
||||
{filters}
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
onClick={this.addFilter.bind(this)}
|
||||
>
|
||||
<i className="fa fa-plus" />Add Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
|
@ -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 (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">GroupBy</div>
|
||||
<div className="panel-body">
|
||||
<div className="row">
|
||||
<h5 className="section-heading">GroupBy Column</h5>
|
||||
<Select
|
||||
multi
|
||||
name="select-time-column"
|
||||
placeholder="Select groupby columns"
|
||||
options={this.props.groupByColumnOpts}
|
||||
value={this.props.groupByColumns}
|
||||
autosize={false}
|
||||
onChange={this.changeColumns.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Metrics</h5>
|
||||
<Select
|
||||
multi
|
||||
name="select-since"
|
||||
placeholder="Select metrics"
|
||||
options={this.props.metricsOpts}
|
||||
value={this.props.metrics}
|
||||
autosize={false}
|
||||
onChange={this.changeMetrics.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
|
@ -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 (
|
||||
<div className="btn-group query-and-save">
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={onQuery}>
|
||||
<i className="fa fa-bolt"></i> Query
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={saveClasses}
|
||||
data-target="#save_modal"
|
||||
data-toggle="modal"
|
||||
>
|
||||
<i className="fa fa-plus-circle"></i> Save as
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAndSaveBtns.propTypes = propTypes;
|
|
@ -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 (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">SQL</div>
|
||||
<div className="panel-body">
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Where</h5>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeWhere.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Where Clause"
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Having</h5>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeHaving.bind(this)}
|
||||
className="form-control input-sm"
|
||||
placeholder="Having Clause"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlClause.propTypes = propTypes;
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SqlClause);
|
|
@ -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 (
|
||||
<div className="panel space-1">
|
||||
<div className="panel-header">Time Filter</div>
|
||||
<div className="panel-body">
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Time Column & Grain</h5>
|
||||
<Select
|
||||
className="col-sm-6"
|
||||
name="select-time-column"
|
||||
placeholder="Select a time column"
|
||||
options={this.props.timeColumnOpts}
|
||||
value={this.props.timeColumn}
|
||||
autosize={false}
|
||||
onChange={this.changeTimeColumn.bind(this)}
|
||||
/>
|
||||
<Select
|
||||
className="col-sm-6"
|
||||
name="select-time-grain"
|
||||
placeholder="Select a time grain"
|
||||
options={this.props.timeGrainOpts}
|
||||
value={this.props.timeGrain}
|
||||
autosize={false}
|
||||
onChange={this.changeTimeGrain.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<h5 className="section-heading">Since - Until</h5>
|
||||
<Select
|
||||
className="col-sm-6"
|
||||
name="select-since"
|
||||
placeholder="Select Since Time"
|
||||
options={sinceOptions.map((s) => ({ value: s, label: s }))}
|
||||
value={this.props.since}
|
||||
autosize={false}
|
||||
onChange={this.changeSince.bind(this)}
|
||||
/>
|
||||
<Select
|
||||
className="col-sm-6"
|
||||
name="select-until"
|
||||
placeholder="Select Until Time"
|
||||
options={untilOptions.map((u) => ({ value: u, label: u }))}
|
||||
value={this.props.until}
|
||||
autosize={false}
|
||||
onChange={this.changeUntil.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
|
@ -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'];
|
|
@ -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(
|
||||
<Provider store={store}>
|
||||
<ExploreViewContainer
|
||||
data={bootstrapData}
|
||||
/>
|
||||
</Provider>,
|
||||
exploreViewContainer
|
||||
);
|
||||
|
|
@ -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;
|
||||
};
|
|
@ -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: [],
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{% extends "caravel/basic.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div
|
||||
id="js-explore-view-container"
|
||||
data-bootstrap="{{ bootstrap_data }}"
|
||||
></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
{% with filename="explorev2" %}
|
||||
{% include "caravel/partials/_script_tag.html" %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
135
caravel/views.py
135
caravel/views.py
|
@ -1201,6 +1201,113 @@ class Caravel(BaseCaravelView):
|
|||
can_download=slice_download_perm,
|
||||
userid=g.user.get_id() if g.user else '')
|
||||
|
||||
@has_access
|
||||
@expose("/exploreV2/<datasource_type>/<datasource_id>/<slice_id>/")
|
||||
@expose("/exploreV2/<datasource_type>/<datasource_id>/")
|
||||
@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 @@ class Caravel(BaseCaravelView):
|
|||
'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/<last_updated_ms>")
|
||||
@log_this
|
||||
|
|
Loading…
Reference in New Issue