[WiP] Deprecate Explore v1 (#2064)

* Simplifying the viz interface (#2005)

* Working on dashes

* Making this a collaborative branch

* Fixing some bugs

* Fixing bugs

* More improvements

* Add datasource back in bootstrap data

* Decent state

* Linting

* Moving forward

* Some more linting

* Fix the timer

* Triggering events through state

* Lingint

* Put filters in an array instead of flt strings (#2090)

* Put filters in an array instead of flt strings

* Remove query_filter(), put opChoices into Filter

* Update version_info.json

* Fix migrations

* More renderTrigger=true

* Fixing bugs

* Working on standalone

* getting standalone to work

* Fixed forcedHeight for standalone =view

* Linting

* Get save slice working in v2 (#2106)

* Filter bugfix

* Fixing empty series limit bug

* Fixed dashboard view

* Fixing short urls

* Only allow owners to overwrite slice (#2142)

* Raise exception when date range is wrong

* Only allow owner to overwrite a slice

* Fix tests for deprecate v1 (#2140)

* Fixed tests for control panels container and filters

* Fixed python tests for explorev2

* Fix linting errors

* Add in stop button during slice querying/rendering (#2121)

* Add in stop button during slice querying/rendering

* Abort ajax request on stop

* Adding missing legacy module

* Removing select2.sortable.js because of license

* Allow query to display while slice is loading (#2100)

* Allow query to display while slice is loading

* Put latestQueryFormData in store

* Reorganized query function, got rid of tu[le return values

* Merging migrations

* Wrapping up shortner migration

* Fixing tests

* Add folder creation to syncBackend

* Fixing edit URL in explore view

* Fix look of Stop button

* Adding syntax highlighting to query modal

* Fix cast_form_data and flase checkbox on dash

* Bugfix

* Going deeper

* Fix filtering

* Deleing invalid filters when changing datasource

* Minor adjustments

* Fixing calendar heatmap examples

* Moving edit datasource button to header's right side

* Fixing mapbox example

* Show stack trace when clicking alert

* Adding npm sync-backend command to build instruction

* Bumping up JS dependencies

* rm dep on select2

* Fix py3 urlparse

* rm superset-select2.js

* Improving migration scripts

* Bugfixes on staging

* Fixing Markup viz
This commit is contained in:
Maxime Beauchemin
2017-02-16 17:28:35 -08:00
committed by GitHub
parent 3b023e5eaa
commit 0cc8eff1c3
82 changed files with 4018 additions and 3867 deletions

View File

@@ -3,7 +3,7 @@ import cx from 'classnames';
import TooltipWrapper from './TooltipWrapper';
const propTypes = {
sliceId: PropTypes.string.isRequired,
sliceId: PropTypes.number.isRequired,
actions: PropTypes.object.isRequired,
isStarred: PropTypes.bool.isRequired,
};

View File

@@ -13,7 +13,6 @@ import Header from './components/Header';
require('bootstrap');
require('../../stylesheets/dashboard.css');
require('../superset-select2.js');
export function getInitialState(dashboardData, context) {
const dashboard = Object.assign({ context }, utils.controllerInterface, dashboardData);
@@ -83,9 +82,6 @@ function initDashboardView(dashboard) {
);
$('div.grid-container').css('visibility', 'visible');
$('.select2').select2({
dropdownAutoWidth: true,
});
$('div.widget').click(function (e) {
const $this = $(this);
const $target = $(e.target);
@@ -165,9 +161,7 @@ export function dashboardContainer(dashboard) {
}
},
effectiveExtraFilters(sliceId) {
// Summarized filter, not defined by sliceId
// returns k=field, v=array of values
const f = {};
const f = [];
const immuneSlices = this.metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
// The slice is immune to dashboard fiterls
@@ -185,7 +179,11 @@ export function dashboardContainer(dashboard) {
for (const filteringSliceId in this.filters) {
for (const field in this.filters[filteringSliceId]) {
if (!immuneToFields.includes(field)) {
f[field] = this.filters[filteringSliceId][field];
f.push({
col: field,
op: 'in',
val: this.filters[filteringSliceId][field],
});
}
}
}

View File

@@ -98,7 +98,7 @@ class GridLayout extends React.Component {
id={'slice_' + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={`widget ${slice.viz_name}`}
className={`widget ${slice.form_data.viz_type}`}
>
<SliceCell
slice={slice}

View File

@@ -24,7 +24,7 @@ class Header extends React.PureComponent {
<span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
</h1>
</div>
<div className="pull-right">
<div className="pull-right" style={{ marginTop: '35px' }}>
{!this.props.dashboard.context.standalone_mode &&
<Controls dashboard={dashboard} />
}

View File

@@ -67,13 +67,13 @@ function SliceCell({ expandedSlices, removeSlice, slice }) {
</div>
<div className="row chart-container">
<input type="hidden" value="false" />
<div id={slice.token} className="token col-md-12">
<div id={'token_' + slice.slice_id} className="token col-md-12">
<img
src="/static/assets/images/loading.gif"
className="loading"
alt="loading"
/>
<div className="slice_container" id={slice.token + '_con'}></div>
<div className="slice_container" id={'con_' + slice.slice_id}></div>
</div>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import React, { PropTypes } from 'react';
import ModalTrigger from './../../components/ModalTrigger';
const propTypes = {
query: PropTypes.string,
};
const defaultProps = {
query: '',
};
export default function DisplayQueryButton({ query }) {
const modalBody = (<pre>{query}</pre>);
return (
<ModalTrigger
isButton
triggerNode={<span>Query</span>}
modalTitle="Query"
modalBody={modalBody}
/>
);
}
DisplayQueryButton.propTypes = propTypes;
DisplayQueryButton.defaultProps = defaultProps;

View File

@@ -1,46 +0,0 @@
import React, { PropTypes } from 'react';
import cx from 'classnames';
import URLShortLinkButton from './URLShortLinkButton';
import EmbedCodeButton from './EmbedCodeButton';
import DisplayQueryButton from './DisplayQueryButton';
const propTypes = {
canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
slice: PropTypes.object.isRequired,
query: PropTypes.string,
};
export default function ExploreActionButtons({ canDownload, slice, query }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload,
});
return (
<div className="btn-group results" role="group">
<URLShortLinkButton slice={slice} />
<EmbedCodeButton slice={slice} />
<a
href={slice.data.json_endpoint}
className="btn btn-default btn-sm"
title="Export to .json"
target="_blank"
>
<i className="fa fa-file-code-o"></i> .json
</a>
<a
href={slice.data.csv_endpoint}
className={exportToCSVClasses}
title="Export to .csv format"
target="_blank"
>
<i className="fa fa-file-text-o"></i> .csv
</a>
<DisplayQueryButton query={query} />
</div>
);
}
ExploreActionButtons.propTypes = propTypes;

View File

@@ -1,403 +0,0 @@
// Javascript for the explorer page
// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice
// nb: to add a new vis, you must also add a Python fn in viz.py
//
// js
const $ = window.$ = require('jquery');
const px = require('./../modules/superset.js');
const utils = require('./../modules/utils.js');
const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
import React from 'react';
import ReactDOM from 'react-dom';
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
import ExploreActionButtons from './components/ExploreActionButtons.jsx';
require('jquery-ui');
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
require('bootstrap');
require('./../superset-select2.js');
// css
require('../../vendor/pygments.css');
require('../../stylesheets/explore.css');
let slice;
const getPanelClass = function (fieldPrefix) {
return (fieldPrefix === 'flt' ? 'filter' : 'having') + '_panel';
};
function prepForm() {
// Assigning the right id to form elements in filters
const fixId = function ($filter, fieldPrefix, i) {
$filter.attr('id', function () {
return fieldPrefix + '_' + i;
});
['col', 'op', 'eq'].forEach(function (fieldMiddle) {
const fieldName = fieldPrefix + '_' + fieldMiddle;
$filter.find('[id^=' + fieldName + '_]')
.attr('id', function () {
return fieldName + '_' + i;
})
.attr('name', function () {
return fieldName + '_' + i;
});
});
};
['flt', 'having'].forEach(function (fieldPrefix) {
let i = 1;
$('#' + getPanelClass(fieldPrefix) + ' #filters > div').each(function () {
fixId($(this), fieldPrefix, i);
i++;
});
});
}
function query(forceUpdate, pushState) {
let force = forceUpdate;
if (force === undefined) {
force = false;
}
$('.query-and-save button').attr('disabled', 'disabled');
if (force) { // Don't hide the alert message when the page is just loaded
$('div.alert').remove();
}
$('#is_cached').hide();
prepForm();
if (pushState !== false) {
// update the url after prepForm() fix the field ids
history.pushState({}, document.title, slice.querystring());
}
slice.container.html('');
slice.render(force);
}
function saveSlice() {
const action = $('input[name=rdo_save]:checked').val();
if (action === 'saveas') {
const sliceName = $('input[name=new_slice_name]').val();
if (sliceName === '') {
utils.showModal({
title: 'Error',
body: 'You must pick a name for the new slice',
});
return;
}
document.getElementById('slice_name').value = sliceName;
}
const addToDash = $('input[name=addToDash]:checked').val();
if (addToDash === 'existing' && $('#save_to_dashboard_id').val() === '') {
utils.showModal({
title: 'Error',
body: 'You must pick an existing dashboard',
});
return;
} else if (addToDash === 'new' && $('input[name=new_dashboard_name]').val() === '') {
utils.showModal({
title: 'Error',
body: 'Please enter a name for the new dashboard',
});
return;
}
$('#action').val(action);
prepForm();
$('#query').submit();
}
function initExploreView() {
function getCollapsedFieldsets() {
let collapsedFieldsets = $('#collapsedFieldsets').val();
if (collapsedFieldsets !== undefined && collapsedFieldsets !== '') {
collapsedFieldsets = collapsedFieldsets.split('||');
} else {
collapsedFieldsets = [];
}
return collapsedFieldsets;
}
function toggleFieldset(legend, animation) {
const parent = legend.parent();
const fieldset = parent.find('.legend_label').text();
const collapsedFieldsets = getCollapsedFieldsets();
let index;
if (parent.hasClass('collapsed')) {
if (animation) {
parent.find('.panel-body').slideDown();
} else {
parent.find('.panel-body').show();
}
parent.removeClass('collapsed');
parent.find('span.collapser').text('[-]');
// removing from array, js is overcomplicated
index = collapsedFieldsets.indexOf(fieldset);
if (index !== -1) {
collapsedFieldsets.splice(index, 1);
}
} else { // not collapsed
if (animation) {
parent.find('.panel-body').slideUp();
} else {
parent.find('.panel-body').hide();
}
parent.addClass('collapsed');
parent.find('span.collapser').text('[+]');
index = collapsedFieldsets.indexOf(fieldset);
if (index === -1 && fieldset !== '' && fieldset !== undefined) {
collapsedFieldsets.push(fieldset);
}
}
$('#collapsedFieldsets').val(collapsedFieldsets.join('||'));
}
px.initFavStars();
$('#viz_type').change(function () {
$('#query').submit();
});
$('#datasource_id').change(function () {
window.location = $(this).find('option:selected').attr('url');
});
const collapsedFieldsets = getCollapsedFieldsets();
for (let i = 0; i < collapsedFieldsets.length; i++) {
toggleFieldset($('legend:contains("' + collapsedFieldsets[i] + '")'), false);
}
function formatViz(viz) {
const url = `/static/assets/images/viz_thumbnails/${viz.id}.png`;
const noImg = '/static/assets/images/noimg.png';
return $(
`<img class="viz-thumb-option" src="${url}" onerror="this.src='${noImg}';">` +
`<span>${viz.text}</span>`
);
}
$('.select2').select2({
dropdownAutoWidth: true,
});
$('.select2Sortable').select2({
dropdownAutoWidth: true,
});
$('.select2-with-images').select2({
dropdownAutoWidth: true,
dropdownCssClass: 'bigdrop',
formatResult: formatViz,
});
$('.select2Sortable').select2Sortable({
bindOrder: 'sortableStop',
});
$('form').show();
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
$('.ui-helper-hidden-accessible').remove(); // jQuery-ui 1.11+ creates a div for every tooltip
function addFilter(i, fieldPrefix) {
const cp = $('#' + fieldPrefix + '0').clone();
$(cp).appendTo('#' + getPanelClass(fieldPrefix) + ' #filters');
$(cp).show();
if (i !== undefined) {
$(cp).find('#' + fieldPrefix + '_eq_0').val(px.getParam(fieldPrefix + '_eq_' + i));
$(cp).find('#' + fieldPrefix + '_op_0').val(px.getParam(fieldPrefix + '_op_' + i));
$(cp).find('#' + fieldPrefix + '_col_0').val(px.getParam(fieldPrefix + '_col_' + i));
}
$(cp).find('select').select2();
$(cp).find('.remove').click(function () {
$(this)
.parent()
.parent()
.remove();
});
}
function setFilters() {
['flt', 'having'].forEach(function (prefix) {
for (let i = 1; i < 10; i++) {
const col = px.getParam(prefix + '_col_' + i);
if (col !== '') {
addFilter(i, prefix);
}
}
});
}
setFilters();
$(window).bind('popstate', function () {
// Browser back button
const returnLocation = history.location || document.location;
// Could do something more lightweight here, but we're not optimizing
// for the use of the back button anyways
returnLocation.reload();
});
$('#filter_panel #plus').click(function () {
addFilter(undefined, 'flt');
});
$('#having_panel #plus').click(function () {
addFilter(undefined, 'having');
});
function createChoices(term, data) {
const filtered = $(data).filter(function () {
return this.text.localeCompare(term) === 0;
});
if (filtered.length === 0) {
return {
id: term,
text: term,
};
}
return {};
}
function initSelectionToValue(element, callback) {
callback({
id: element.val(),
text: element.val(),
});
}
$('.select2_freeform').each(function () {
const parent = $(this).parent();
const name = $(this).attr('name');
const l = [];
let selected = '';
for (let i = 0; i < this.options.length; i++) {
l.push({
id: this.options[i].value,
text: this.options[i].text,
});
if (this.options[i].selected) {
selected = this.options[i].value;
}
}
parent.append(
`<input class="${$(this).attr('class')}" ` +
`name="${name}" type="text" value="${selected}">`
);
$(`input[name='${name}']`).select2({
createSearchChoice: createChoices,
initSelection: initSelectionToValue,
dropdownAutoWidth: true,
multiple: false,
data: l,
});
$(this).remove();
});
function prepSaveDialog() {
const setButtonsState = function () {
const addToDash = $('input[name=addToDash]:checked').val();
if (addToDash === 'existing' || addToDash === 'new') {
$('.gotodash').removeAttr('disabled');
} else {
$('.gotodash').prop('disabled', true);
}
};
const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + $('#userid').val();
$.get(url, function (data) {
const choices = [];
for (let i = 0; i < data.pks.length; i++) {
choices.push({ id: data.pks[i], text: data.result[i].dashboard_title });
}
$('#save_to_dashboard_id').select2({
data: choices,
dropdownAutoWidth: true,
}).on('select2-selecting', function () {
$('#addToDash_existing').prop('checked', true);
setButtonsState();
});
});
$('input[name=addToDash]').change(setButtonsState);
$("input[name='new_dashboard_name']").on('focus', function () {
$('#add_to_new_dash').prop('checked', true);
setButtonsState();
});
$("input[name='new_slice_name']").on('focus', function () {
$('#save_as_new').prop('checked', true);
setButtonsState();
});
$('#btn_modal_save').on('click', () => saveSlice());
$('#btn_modal_save_goto_dash').click(() => {
document.getElementById('goto_dash').value = 'true';
saveSlice();
});
}
prepSaveDialog();
}
function renderExploreActions() {
const exploreActionsEl = document.getElementById('js-explore-actions');
ReactDOM.render(
<ExploreActionButtons
canDownload={exploreActionsEl.getAttribute('data-can-download')}
slice={slice}
query={slice.viewSqlQuery}
/>,
exploreActionsEl
);
}
function initComponents() {
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
ReactDOM.render(
<QueryAndSaveBtns
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
onQuery={() => query(true)}
/>,
queryAndSaveBtnsEl
);
renderExploreActions();
}
let exploreController = {
type: 'slice',
done: (sliceObj) => {
slice = sliceObj;
renderExploreActions();
const cachedSelector = $('#is_cached');
if (slice.data !== undefined && slice.data.is_cached) {
cachedSelector
.attr(
'title',
`Served from data cached at ${slice.data.cached_dttm}. Click [Query] to force refresh`)
.show()
.tooltip('fixTitle');
} else {
cachedSelector.hide();
}
},
error: (sliceObj) => {
slice = sliceObj;
renderExploreActions();
},
};
exploreController = Object.assign({}, utils.controllerInterface, exploreController);
$(document).ready(function () {
const data = $('.slice').data('slice');
initExploreView();
slice = px.Slice(data, exploreController);
// call vis render method, which issues ajax
// calls render on the slice for the first time
query(false, false);
slice.bindResizeToWindowResize();
initComponents();
});

View File

@@ -13,42 +13,88 @@ export function setDatasource(datasource) {
return { type: SET_DATASOURCE, datasource };
}
export const FETCH_STARTED = 'FETCH_STARTED';
export function fetchStarted() {
return { type: FETCH_STARTED };
export const SET_DATASOURCES = 'SET_DATASOURCES';
export function setDatasources(datasources) {
return { type: SET_DATASOURCES, datasources };
}
export const FETCH_SUCCEEDED = 'FETCH_SUCCEEDED';
export function fetchSucceeded() {
return { type: FETCH_SUCCEEDED };
export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
export function fetchDatasourceStarted() {
return { type: FETCH_DATASOURCE_STARTED };
}
export const FETCH_FAILED = 'FETCH_FAILED';
export function fetchFailed(error) {
return { type: FETCH_FAILED, error };
export const FETCH_DATASOURCE_SUCCEEDED = 'FETCH_DATASOURCE_SUCCEEDED';
export function fetchDatasourceSucceeded() {
return { type: FETCH_DATASOURCE_SUCCEEDED };
}
export function fetchDatasourceMetadata(datasourceId, datasourceType) {
export const FETCH_DATASOURCE_FAILED = 'FETCH_DATASOURCE_FAILED';
export function fetchDatasourceFailed(error) {
return { type: FETCH_DATASOURCE_FAILED, error };
}
export const FETCH_DATASOURCES_STARTED = 'FETCH_DATASOURCES_STARTED';
export function fetchDatasourcesStarted() {
return { type: FETCH_DATASOURCES_STARTED };
}
export const FETCH_DATASOURCES_SUCCEEDED = 'FETCH_DATASOURCES_SUCCEEDED';
export function fetchDatasourcesSucceeded() {
return { type: FETCH_DATASOURCES_SUCCEEDED };
}
export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED';
export function fetchDatasourcesFailed(error) {
return { type: FETCH_DATASOURCES_FAILED, error };
}
export const RESET_FIELDS = 'RESET_FIELDS';
export function resetFields() {
return { type: RESET_FIELDS };
}
export const TRIGGER_QUERY = 'TRIGGER_QUERY';
export function triggerQuery() {
return { type: TRIGGER_QUERY };
}
export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) {
return function (dispatch) {
dispatch(fetchStarted());
dispatch(fetchDatasourceStarted());
const url = `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`;
$.ajax({
type: 'GET',
url,
success: (data) => {
dispatch(setDatasource(data));
dispatch(fetchDatasourceSucceeded());
dispatch(resetFields());
if (alsoTriggerQuery) {
dispatch(triggerQuery());
}
},
error(error) {
dispatch(fetchDatasourceFailed(error.responseJSON.error));
},
});
};
}
if (datasourceId) {
const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`];
const url = '/superset/fetch_datasource_metadata?' + params.join('&');
$.ajax({
type: 'GET',
url,
success: (data) => {
dispatch(setDatasource(data));
dispatch(fetchSucceeded());
},
error(error) {
dispatch(fetchFailed(error.responseJSON.error));
},
});
} else {
dispatch(fetchFailed('Please select a datasource'));
}
export function fetchDatasources() {
return function (dispatch) {
dispatch(fetchDatasourcesStarted());
const url = '/superset/datasources/';
$.ajax({
type: 'GET',
url,
success: (data) => {
dispatch(setDatasources(data));
dispatch(fetchDatasourcesSucceeded());
},
error(error) {
dispatch(fetchDatasourcesFailed(error.responseJSON.error));
},
});
};
}
@@ -85,8 +131,8 @@ export function setFieldValue(fieldName, value, validationErrors) {
}
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
export function chartUpdateStarted() {
return { type: CHART_UPDATE_STARTED };
export function chartUpdateStarted(queryRequest) {
return { type: CHART_UPDATE_STARTED, queryRequest };
}
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
@@ -94,6 +140,14 @@ export function chartUpdateSucceeded(queryResponse) {
return { type: CHART_UPDATE_SUCCEEDED, queryResponse };
}
export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED';
export function chartUpdateStopped(queryRequest) {
if (queryRequest) {
queryRequest.abort();
}
return { type: CHART_UPDATE_STOPPED };
}
export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED';
export function chartUpdateFailed(queryResponse) {
return { type: CHART_UPDATE_FAILED, queryResponse };
@@ -126,7 +180,7 @@ export function fetchDashboardsSucceeded(choices) {
export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED';
export function fetchDashboardsFailed(userId) {
return { type: FETCH_FAILED, userId };
return { type: FETCH_DASHBOARDS_FAILED, userId };
}
export function fetchDashboards(userId) {
@@ -177,12 +231,19 @@ export function updateChartStatus(status) {
export const RUN_QUERY = 'RUN_QUERY';
export function runQuery(formData, datasourceType) {
return function (dispatch) {
dispatch(updateChartStatus('loading'));
const url = getExploreUrl(formData, datasourceType, 'json');
$.getJSON(url, function (queryResponse) {
const queryRequest = $.getJSON(url, function (queryResponse) {
dispatch(chartUpdateSucceeded(queryResponse));
}).fail(function (err) {
dispatch(chartUpdateFailed(err));
if (err.statusText !== 'abort') {
dispatch(chartUpdateFailed(err.responseJSON));
}
});
dispatch(chartUpdateStarted(queryRequest));
};
}
export const RENDER_TRIGGERED = 'RENDER_TRIGGERED';
export function renderTriggered() {
return { type: RENDER_TRIGGERED };
}

View File

@@ -1,13 +1,15 @@
import $ from 'jquery';
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Panel, Alert } from 'react-bootstrap';
import { Panel, Alert, Collapse } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
import ExploreActionButtons from '../../explore/components/ExploreActionButtons';
import ExploreActionButtons from './ExploreActionButtons';
import FaveStar from '../../components/FaveStar';
import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromFields } from '../stores/store';
const CHART_STATUS_MAP = {
failed: 'danger',
@@ -17,20 +19,20 @@ const CHART_STATUS_MAP = {
const propTypes = {
actions: PropTypes.object.isRequired,
can_download: PropTypes.bool.isRequired,
slice_id: PropTypes.string.isRequired,
slice_name: PropTypes.string.isRequired,
viz_type: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
containerId: PropTypes.string.isRequired,
query: PropTypes.string,
column_formats: PropTypes.object,
chartStatus: PropTypes.string,
isStarred: PropTypes.bool.isRequired,
chartUpdateStartTime: PropTypes.number.isRequired,
chartUpdateEndTime: PropTypes.number,
alert: PropTypes.string,
can_download: PropTypes.bool.isRequired,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
chartUpdateStartTime: PropTypes.number.isRequired,
column_formats: PropTypes.object,
containerId: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
table_name: PropTypes.string,
viz_type: PropTypes.string.isRequired,
formData: PropTypes.object,
latestQueryFormData: PropTypes.object,
};
class ChartContainer extends React.PureComponent {
@@ -38,14 +40,16 @@ class ChartContainer extends React.PureComponent {
super(props);
this.state = {
selector: `#${props.containerId}`,
showStackTrace: false,
};
}
renderViz() {
this.props.actions.renderTriggered();
const mockSlice = this.getMockedSliceObject();
this.setState({ mockSlice });
try {
visMap[this.props.viz_type](mockSlice, this.props.queryResponse);
this.setState({ mockSlice });
} catch (e) {
this.props.actions.chartRenderingFailed(e);
}
@@ -53,8 +57,13 @@ class ChartContainer extends React.PureComponent {
componentDidUpdate(prevProps) {
if (
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height
(
prevProps.queryResponse !== this.props.queryResponse ||
prevProps.height !== this.props.height ||
this.props.triggerRender
) && !this.props.queryResponse.error
&& this.props.chartStatus !== 'failed'
&& this.props.chartStatus !== 'stopped'
) {
this.renderViz();
}
@@ -62,10 +71,15 @@ class ChartContainer extends React.PureComponent {
getMockedSliceObject() {
const props = this.props;
const getHeight = () => {
const headerHeight = this.props.standalone ? 0 : 100;
return parseInt(props.height, 10) - headerHeight;
};
return {
viewSqlQuery: props.query,
viewSqlQuery: this.props.queryResponse.query,
containerId: props.containerId,
selector: this.state.selector,
formData: this.props.formData,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
@@ -77,7 +91,7 @@ class ChartContainer extends React.PureComponent {
// should call callback to adjust height of chart
$(this.state.selector).css(dim, size);
},
height: () => parseInt(props.height, 10) - 100,
height: getHeight,
show: () => { },
get: (n) => ($(this.state.selector).get(n)),
find: (classname) => ($(this.state.selector).find(classname)),
@@ -85,7 +99,7 @@ class ChartContainer extends React.PureComponent {
width: () => this.chartContainerRef.getBoundingClientRect().width,
height: () => parseInt(props.height, 10) - 100,
height: getHeight,
setFilter: () => {
// set filter according to data in store
@@ -111,9 +125,10 @@ class ChartContainer extends React.PureComponent {
},
data: {
csv_endpoint: props.queryResponse.csv_endpoint,
json_endpoint: props.queryResponse.json_endpoint,
standalone_endpoint: props.queryResponse.standalone_endpoint,
csv_endpoint: getExploreUrl(this.props.formData, this.props.datasource_type, 'csv'),
json_endpoint: getExploreUrl(this.props.formData, this.props.datasource_type, 'json'),
standalone_endpoint: getExploreUrl(
this.props.formData, this.props.datasource_type, 'standalone'),
},
};
@@ -125,26 +140,45 @@ class ChartContainer extends React.PureComponent {
renderChartTitle() {
let title;
if (this.props.slice_name) {
title = this.props.slice_name;
if (this.props.slice) {
title = this.props.slice.slice_name;
} else {
title = `[${this.props.table_name}] - untitled`;
}
return title;
}
renderAlert() {
const msg = (
<div>
{this.props.alert}
<i
className="fa fa-close pull-right"
onClick={this.removeAlert.bind(this)}
style={{ cursor: 'pointer' }}
/>
</div>);
return (
<div>
<Alert
bsStyle="warning"
onClick={() => this.setState({ showStackTrace: !this.state.showStackTrace })}
>
{msg}
</Alert>
{this.props.queryResponse && this.props.queryResponse.stacktrace &&
<Collapse in={this.state.showStackTrace}>
<pre>
{this.props.queryResponse.stacktrace}
</pre>
</Collapse>
}
</div>);
}
renderChart() {
if (this.props.alert) {
return (
<Alert bsStyle="warning">
{this.props.alert}
<i
className="fa fa-close pull-right"
onClick={this.removeAlert.bind(this)}
style={{ cursor: 'pointer' }}
/>
</Alert>
);
return this.renderAlert();
}
const loading = this.props.chartStatus === 'loading';
return (
@@ -170,6 +204,9 @@ class ChartContainer extends React.PureComponent {
}
render() {
if (this.props.standalone) {
return this.renderChart();
}
return (
<div className="chart-container">
<Panel
@@ -181,10 +218,10 @@ class ChartContainer extends React.PureComponent {
>
{this.renderChartTitle()}
{this.props.slice_id &&
{this.props.slice &&
<span>
<FaveStar
sliceId={this.props.slice_id}
sliceId={this.props.slice.slice_id}
actions={this.props.actions}
isStarred={this.props.isStarred}
/>
@@ -195,7 +232,7 @@ class ChartContainer extends React.PureComponent {
>
<a
className="edit-desc-icon"
href={`/slicemodelview/edit/${this.props.slice_id}`}
href={`/slicemodelview/edit/${this.props.slice.slice_id}`}
>
<i className="fa fa-edit" />
</a>
@@ -208,16 +245,15 @@ class ChartContainer extends React.PureComponent {
startTime={this.props.chartUpdateStartTime}
endTime={this.props.chartUpdateEndTime}
isRunning={this.props.chartStatus === 'loading'}
state={CHART_STATUS_MAP[this.props.chartStatus]}
status={CHART_STATUS_MAP[this.props.chartStatus]}
style={{ fontSize: '10px', marginRight: '5px' }}
/>
{this.state.mockSlice &&
<ExploreActionButtons
slice={this.state.mockSlice}
canDownload={this.props.can_download}
query={this.props.queryResponse.query}
/>
}
<ExploreActionButtons
slice={this.state.mockSlice}
canDownload={this.props.can_download}
queryEndpoint={getExploreUrl(
this.props.latestQueryFormData, this.props.datasource_type, 'query')}
/>
</div>
</div>
}
@@ -232,21 +268,24 @@ class ChartContainer extends React.PureComponent {
ChartContainer.propTypes = propTypes;
function mapStateToProps(state) {
const formData = getFormDataFromFields(state.fields);
return {
containerId: `slice-container-${state.viz.form_data.slice_id}`,
slice_id: state.viz.form_data.slice_id,
slice_name: state.viz.form_data.slice_name,
viz_type: state.viz.form_data.viz_type,
can_download: state.can_download,
chartUpdateStartTime: state.chartUpdateStartTime,
chartUpdateEndTime: state.chartUpdateEndTime,
query: state.viz.query,
column_formats: state.viz.column_formats,
chartStatus: state.chartStatus,
isStarred: state.isStarred,
alert: state.chartAlert,
table_name: state.viz.form_data.datasource_name,
can_download: state.can_download,
chartStatus: state.chartStatus,
chartUpdateEndTime: state.chartUpdateEndTime,
chartUpdateStartTime: state.chartUpdateStartTime,
column_formats: state.datasource ? state.datasource.column_formats : null,
containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container',
formData,
latestQueryFormData: state.latestQueryFormData,
isStarred: state.isStarred,
queryResponse: state.queryResponse,
slice: state.slice,
standalone: state.standalone,
table_name: formData.datasource_name,
viz_type: formData.viz_type,
triggerRender: state.triggerRender,
};
}

View File

@@ -6,41 +6,72 @@ const propTypes = {
label: PropTypes.string.isRequired,
description: PropTypes.string,
validationErrors: PropTypes.array,
renderTrigger: PropTypes.bool,
rightNode: PropTypes.node,
};
const defaultProps = {
description: null,
validationErrors: [],
renderTrigger: false,
};
export default function ControlHeader({ label, description, validationErrors }) {
export default function ControlHeader({
label, description, validationErrors, renderTrigger, rightNode }) {
const hasError = (validationErrors.length > 0);
return (
<ControlLabel>
{hasError ?
<strong className="text-danger">{label}</strong> :
<span>{label}</span>
}
{' '}
{(validationErrors.length > 0) &&
<span>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={'error-tooltip'}>
{validationErrors.join(' ')}
</Tooltip>
}
>
<i className="fa fa-exclamation-circle text-danger" />
</OverlayTrigger>
<div>
<div className="pull-left">
<ControlLabel>
{hasError ?
<strong className="text-danger">{label}</strong> :
<span>{label}</span>
}
{' '}
</span>
{(validationErrors.length > 0) &&
<span>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={'error-tooltip'}>
{validationErrors.join(' ')}
</Tooltip>
}
>
<i className="fa fa-exclamation-circle text-danger" />
</OverlayTrigger>
{' '}
</span>
}
{description &&
<span>
<InfoTooltipWithTrigger label={label} tooltip={description} />
{' '}
</span>
}
{renderTrigger &&
<span>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={'rendertrigger-tooltip'}>
Takes effect on chart immediatly
</Tooltip>
}
>
<i className="fa fa-bolt text-muted" />
</OverlayTrigger>
{' '}
</span>
}
</ControlLabel>
</div>
{rightNode &&
<div className="pull-right">
{rightNode}
</div>
}
{description &&
<InfoTooltipWithTrigger label={label} tooltip={description} />
}
</ControlLabel>
<div className="clearfix" />
</div>
);
}

View File

@@ -4,10 +4,11 @@ import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import { Panel, Alert } from 'react-bootstrap';
import visTypes, { sectionsToRender } from '../stores/visTypes';
import { sectionsToRender } from '../stores/visTypes';
import ControlPanelSection from './ControlPanelSection';
import FieldSetRow from './FieldSetRow';
import FieldSet from './FieldSet';
import fields from '../stores/fields';
const propTypes = {
datasource_type: PropTypes.string.isRequired,
@@ -23,44 +24,19 @@ const propTypes = {
class ControlPanelsContainer extends React.Component {
constructor(props) {
super(props);
this.fieldOverrides = this.fieldOverrides.bind(this);
this.getFieldData = this.getFieldData.bind(this);
this.removeAlert = this.removeAlert.bind(this);
this.getFieldData = this.getFieldData.bind(this);
}
componentWillMount() {
const datasource_id = this.props.form_data.datasource;
const datasource_type = this.props.datasource_type;
if (datasource_id) {
this.props.actions.fetchDatasourceMetadata(datasource_id, datasource_type);
getFieldData(fieldName) {
const mapF = fields[fieldName].mapStateToProps;
if (mapF) {
return Object.assign({}, this.props.fields[fieldName], mapF(this.props.exploreState));
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.form_data.datasource !== this.props.form_data.datasource) {
if (nextProps.form_data.datasource) {
this.props.actions.fetchDatasourceMetadata(
nextProps.form_data.datasource, nextProps.datasource_type);
}
}
}
getFieldData(fs) {
const fieldOverrides = this.fieldOverrides();
let fieldData = this.props.fields[fs] || {};
if (fieldOverrides.hasOwnProperty(fs)) {
const overrideData = fieldOverrides[fs];
fieldData = Object.assign({}, fieldData, overrideData);
}
if (fieldData.mapStateToProps) {
Object.assign(fieldData, fieldData.mapStateToProps(this.props.exploreState));
}
return fieldData;
return this.props.fields[fieldName];
}
sectionsToRender() {
return sectionsToRender(this.props.form_data.viz_type, this.props.datasource_type);
}
fieldOverrides() {
const viz = visTypes[this.props.form_data.viz_type];
return viz.fieldOverrides || {};
}
removeAlert() {
this.props.actions.removeControlPanelAlert();
}
@@ -78,7 +54,7 @@ class ControlPanelsContainer extends React.Component {
/>
</Alert>
}
{!this.props.isDatasourceMetaLoading && this.sectionsToRender().map((section) => (
{this.sectionsToRender().map((section) => (
<ControlPanelSection
key={section.label}
label={section.label}
@@ -94,7 +70,6 @@ class ControlPanelsContainer extends React.Component {
value={this.props.form_data[fieldName]}
validationErrors={this.props.fields[fieldName].validationErrors}
actions={this.props.actions}
prefix={section.prefix}
{...this.getFieldData(fieldName)}
/>
))}

View File

@@ -0,0 +1,59 @@
import React, { PropTypes } from 'react';
import ModalTrigger from './../../components/ModalTrigger';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { github } from 'react-syntax-highlighter/dist/styles';
const $ = window.$ = require('jquery');
const propTypes = {
queryEndpoint: PropTypes.string.isRequired,
};
export default class DisplayQueryButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
modalBody: <pre />,
};
}
beforeOpen() {
this.setState({
modalBody:
(<img
className="loading"
alt="Loading..."
src="/static/assets/images/loading.gif"
/>),
});
$.ajax({
type: 'GET',
url: this.props.queryEndpoint,
success: (data) => {
const modalBody = data.language ?
<SyntaxHighlighter language={data.language} style={github}>
{data.query}
</SyntaxHighlighter>
:
<pre>{data.query}</pre>;
this.setState({ modalBody });
},
error(data) {
this.setState({ modalBody: (<pre>{data.error}</pre>) });
},
});
}
render() {
return (
<ModalTrigger
isButton
triggerNode={<span>Query</span>}
modalTitle="Query"
bsSize="large"
beforeOpen={this.beforeOpen.bind(this)}
modalBody={this.state.modalBody}
/>
);
}
}
DisplayQueryButton.propTypes = propTypes;

View File

@@ -0,0 +1,53 @@
import React, { PropTypes } from 'react';
import cx from 'classnames';
import URLShortLinkButton from './URLShortLinkButton';
import EmbedCodeButton from './EmbedCodeButton';
import DisplayQueryButton from './DisplayQueryButton';
const propTypes = {
canDownload: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
slice: PropTypes.object,
queryEndpoint: PropTypes.string,
};
export default function ExploreActionButtons({ canDownload, slice, queryEndpoint }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload,
});
if (slice) {
return (
<div className="btn-group results" role="group">
<URLShortLinkButton slice={slice} />
<EmbedCodeButton slice={slice} />
<a
href={slice.data.json_endpoint}
className="btn btn-default btn-sm"
title="Export to .json"
target="_blank"
>
<i className="fa fa-file-code-o"></i> .json
</a>
<a
href={slice.data.csv_endpoint}
className={exportToCSVClasses}
title="Export to .csv format"
target="_blank"
>
<i className="fa fa-file-text-o"></i> .csv
</a>
<DisplayQueryButton
queryEndpoint={queryEndpoint}
/>
</div>
);
}
return (
<DisplayQueryButton queryEndpoint={queryEndpoint} />
);
}
ExploreActionButtons.propTypes = propTypes;

View File

@@ -1,24 +1,27 @@
/* eslint camelcase: 0 */
import React from 'react';
import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import ChartContainer from './ChartContainer';
import ControlPanelsContainer from './ControlPanelsContainer';
import SaveModal from './SaveModal';
import QueryAndSaveBtns from '../../explore/components/QueryAndSaveBtns';
import { autoQueryFields } from '../stores/fields';
import QueryAndSaveBtns from './QueryAndSaveBtns';
import { getExploreUrl } from '../exploreUtils';
import { getFormDataFromFields } from '../stores/store';
const propTypes = {
form_data: React.PropTypes.object.isRequired,
actions: React.PropTypes.object.isRequired,
datasource_type: React.PropTypes.string.isRequired,
chartStatus: React.PropTypes.string.isRequired,
fields: React.PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
chartStatus: PropTypes.string.isRequired,
fields: PropTypes.object.isRequired,
forcedHeight: PropTypes.string,
form_data: PropTypes.object.isRequired,
standalone: PropTypes.bool.isRequired,
triggerQuery: PropTypes.bool.isRequired,
queryRequest: PropTypes.object,
};
class ExploreViewContainer extends React.Component {
constructor(props) {
super(props);
@@ -29,17 +32,23 @@ class ExploreViewContainer extends React.Component {
}
componentDidMount() {
this.props.actions.fetchDatasources();
window.addEventListener('resize', this.handleResize.bind(this));
this.runQuery();
}
componentWillReceiveProps(nextProps) {
const refreshChart = Object.keys(nextProps.form_data).some((field) => (
nextProps.form_data[field] !== this.props.form_data[field]
&& autoQueryFields.indexOf(field) !== -1)
);
if (refreshChart) {
this.onQuery();
componentWillReceiveProps(np) {
if (np.fields.viz_type.value !== this.props.fields.viz_type.value) {
this.props.actions.resetFields();
this.props.actions.triggerQuery();
}
if (np.fields.datasource.value !== this.props.fields.datasource.value) {
this.props.actions.fetchDatasourceMetadata(np.form_data.datasource, true);
}
}
componentDidUpdate() {
if (this.props.triggerQuery) {
this.runQuery();
}
}
@@ -48,19 +57,26 @@ class ExploreViewContainer extends React.Component {
}
onQuery() {
// remove alerts when query
this.props.actions.removeControlPanelAlert();
this.props.actions.removeChartAlert();
this.runQuery();
history.pushState(
{},
document.title,
getExploreUrl(this.props.form_data, this.props.datasource_type)
);
// remove alerts when query
this.props.actions.removeControlPanelAlert();
this.props.actions.removeChartAlert();
getExploreUrl(this.props.form_data));
}
onStop() {
this.props.actions.chartUpdateStopped(this.props.queryRequest);
}
getHeight() {
const navHeight = 90;
if (this.props.forcedHeight) {
return this.props.forcedHeight + 'px';
}
const navHeight = this.props.standalone ? 0 : 90;
return `${window.innerHeight - navHeight}px`;
}
@@ -101,8 +117,18 @@ class ExploreViewContainer extends React.Component {
}
return errorMessage;
}
renderChartContainer() {
return (
<ChartContainer
actions={this.props.actions}
height={this.state.height}
/>);
}
render() {
if (this.props.standalone) {
return this.renderChartContainer();
}
return (
<div
id="explore-container"
@@ -117,7 +143,6 @@ class ExploreViewContainer extends React.Component {
onHide={this.toggleModal.bind(this)}
actions={this.props.actions}
form_data={this.props.form_data}
datasource_type={this.props.datasource_type}
/>
}
<div className="row">
@@ -126,7 +151,8 @@ class ExploreViewContainer extends React.Component {
canAdd="True"
onQuery={this.onQuery.bind(this)}
onSave={this.toggleModal.bind(this)}
disabled={this.props.chartStatus === 'loading'}
onStop={this.onStop.bind(this)}
loading={this.props.chartStatus === 'loading'}
errorMessage={this.renderErrorMessage()}
/>
<br />
@@ -134,14 +160,10 @@ class ExploreViewContainer extends React.Component {
actions={this.props.actions}
form_data={this.props.form_data}
datasource_type={this.props.datasource_type}
onQuery={this.onQuery.bind(this)}
/>
</div>
<div className="col-sm-8">
<ChartContainer
actions={this.props.actions}
height={this.state.height}
/>
{this.renderChartContainer()}
</div>
</div>
</div>
@@ -152,11 +174,16 @@ class ExploreViewContainer extends React.Component {
ExploreViewContainer.propTypes = propTypes;
function mapStateToProps(state) {
const form_data = getFormDataFromFields(state.fields);
return {
chartStatus: state.chartStatus,
datasource_type: state.datasource_type,
fields: state.fields,
form_data: state.viz.form_data,
form_data,
standalone: state.standalone,
triggerQuery: state.triggerQuery,
forcedHeight: state.forced_height,
queryRequest: state.queryRequest,
};
}

View File

@@ -1,17 +1,19 @@
import React, { PropTypes } from 'react';
import TextField from './TextField';
import CheckboxField from './CheckboxField';
import TextAreaField from './TextAreaField';
import SelectField from './SelectField';
import FilterField from './FilterField';
import ControlHeader from './ControlHeader';
import FilterField from './FilterField';
import HiddenField from './HiddenField';
import SelectField from './SelectField';
import TextAreaField from './TextAreaField';
import TextField from './TextField';
const fieldMap = {
TextField,
CheckboxField,
TextAreaField,
SelectField,
FilterField,
HiddenField,
SelectField,
TextAreaField,
TextField,
};
const fieldTypes = Object.keys(fieldMap);
@@ -25,6 +27,8 @@ const propTypes = {
places: PropTypes.number,
validators: PropTypes.array,
validationErrors: PropTypes.array,
renderTrigger: PropTypes.bool,
rightNode: PropTypes.node,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
@@ -33,6 +37,7 @@ const propTypes = {
};
const defaultProps = {
renderTrigger: false,
validators: [],
validationErrors: [],
};
@@ -65,12 +70,15 @@ export default class FieldSet extends React.PureComponent {
}
render() {
const FieldType = fieldMap[this.props.type];
const divStyle = this.props.hidden ? { display: 'none' } : null;
return (
<div>
<div style={divStyle}>
<ControlHeader
label={this.props.label}
description={this.props.description}
renderTrigger={this.props.renderTrigger}
validationErrors={this.props.validationErrors}
rightNode={this.props.rightNode}
/>
<FieldType
onChange={this.onChange}

View File

@@ -6,14 +6,15 @@ import SelectField from './SelectField';
const propTypes = {
choices: PropTypes.array,
opChoices: PropTypes.array,
changeFilter: PropTypes.func,
removeFilter: PropTypes.func,
filter: PropTypes.object.isRequired,
datasource: PropTypes.object,
having: PropTypes.bool,
};
const defaultProps = {
having: false,
changeFilter: () => {},
removeFilter: () => {},
choices: [],
@@ -21,6 +22,11 @@ const defaultProps = {
};
export default class Filter extends React.Component {
constructor(props) {
super(props);
this.opChoices = this.props.having ? ['==', '!=', '>', '<', '>=', '<=']
: ['in', 'not in'];
}
fetchFilterValues(col) {
if (!this.props.datasource) {
return;
@@ -61,24 +67,27 @@ export default class Filter extends React.Component {
if (!filter.choices) {
this.fetchFilterValues(filter.col);
}
}
if (this.props.having) {
// druid having filter
return (
<SelectField
multi
freeForm
name="filter-value"
<input
type="text"
onChange={this.changeFilter.bind(this, 'val')}
value={filter.value}
choices={filter.choices}
onChange={this.changeFilter.bind(this, 'value')}
className="form-control input-sm"
placeholder="Filter value"
/>
);
}
return (
<input
type="text"
onChange={this.changeFilter.bind(this, 'value')}
value={filter.value}
className="form-control input-sm"
placeholder="Filter value"
<SelectField
multi
freeForm
name="filter-value"
value={filter.val}
choices={filter.choices || []}
onChange={this.changeFilter.bind(this, 'val')}
/>
);
}
@@ -102,7 +111,7 @@ export default class Filter extends React.Component {
<Select
id="select-op"
placeholder="Select operator"
options={this.props.opChoices.map((o) => ({ value: o, label: o }))}
options={this.opChoices.map((o) => ({ value: o, label: o }))}
value={filter.op}
onChange={this.changeFilter.bind(this, 'op')}
/>

View File

@@ -3,7 +3,7 @@ import { Button, Row, Col } from 'react-bootstrap';
import Filter from './Filter';
const propTypes = {
prefix: PropTypes.string,
name: PropTypes.string,
choices: PropTypes.array,
onChange: PropTypes.func,
value: PropTypes.array,
@@ -11,25 +11,18 @@ const propTypes = {
};
const defaultProps = {
prefix: 'flt',
choices: [],
onChange: () => {},
value: [],
};
export default class FilterField extends React.Component {
constructor(props) {
super(props);
this.opChoices = props.prefix === 'flt' ?
['in', 'not in'] : ['==', '!=', '>', '<', '>=', '<='];
}
addFilter() {
const newFilters = Object.assign([], this.props.value);
newFilters.push({
prefix: this.props.prefix,
col: null,
op: 'in',
value: this.props.datasource.filter_select ? [] : '',
val: this.props.datasource.filter_select ? [] : '',
});
this.props.onChange(newFilters);
}
@@ -46,22 +39,19 @@ export default class FilterField extends React.Component {
render() {
const filters = [];
this.props.value.forEach((filter, i) => {
// only display filters with current prefix
if (filter.prefix === this.props.prefix) {
const filterBox = (
<div key={i}>
<Filter
filter={filter}
choices={this.props.choices}
opChoices={this.opChoices}
datasource={this.props.datasource}
removeFilter={this.removeFilter.bind(this, i)}
changeFilter={this.changeFilter.bind(this, i)}
/>
</div>
);
filters.push(filterBox);
}
const filterBox = (
<div key={i}>
<Filter
having={this.props.name === 'having_filters'}
filter={filter}
choices={this.props.choices}
datasource={this.props.datasource}
removeFilter={this.removeFilter.bind(this, i)}
changeFilter={this.changeFilter.bind(this, i)}
/>
</div>
);
filters.push(filterBox);
});
return (
<div>

View File

@@ -0,0 +1,24 @@
import React, { PropTypes } from 'react';
import { FormControl } from 'react-bootstrap';
const propTypes = {
onChange: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
const defaultProps = {
onChange: () => {},
};
export default class HiddenField extends React.PureComponent {
render() {
// This wouldn't be necessary but might as well
return <FormControl type="hidden" value={this.props.value} />;
}
}
HiddenField.propTypes = propTypes;
HiddenField.defaultProps = defaultProps;

View File

@@ -7,38 +7,50 @@ const propTypes = {
canAdd: PropTypes.string.isRequired,
onQuery: PropTypes.func.isRequired,
onSave: PropTypes.func,
disabled: PropTypes.bool,
onStop: PropTypes.func,
loading: PropTypes.bool,
errorMessage: PropTypes.string,
};
const defaultProps = {
onStop: () => {},
onSave: () => {},
disabled: false,
};
export default function QueryAndSaveBtns({ canAdd, onQuery, onSave, disabled, errorMessage }) {
export default function QueryAndSaveBtns(
{ canAdd, onQuery, onSave, onStop, loading, errorMessage }) {
const saveClasses = classnames({
'disabled disabledButton': canAdd !== 'True',
});
const qryButtonStyle = errorMessage ? 'danger' : 'primary';
const qryButtonDisabled = errorMessage ? true : disabled;
const saveButtonDisabled = errorMessage ? true : loading;
const qryOrStopButton = loading ? (
<Button
onClick={onStop}
bsStyle="warning"
>
<i className="fa fa-stop-circle-o" /> Stop
</Button>
) : (
<Button
className="query"
onClick={onQuery}
bsStyle={qryButtonStyle}
>
<i className="fa fa-bolt" /> Query
</Button>
);
return (
<div>
<ButtonGroup className="query-and-save">
<Button
id="query_button"
onClick={onQuery}
disabled={qryButtonDisabled}
bsStyle={qryButtonStyle}
>
<i className="fa fa-bolt" /> Query
</Button>
{qryOrStopButton}
<Button
className={saveClasses}
data-target="#save_modal"
data-toggle="modal"
disabled={qryButtonDisabled}
disabled={saveButtonDisabled}
onClick={onSave}
>
<i className="fa fa-plus-circle"></i> Save as

View File

@@ -1,20 +1,20 @@
/* eslint camel-case: 0 */
/* eslint camelcase: 0 */
import React, { PropTypes } from 'react';
import $ from 'jquery';
import { Modal, Alert, Button, Radio } from 'react-bootstrap';
import Select from 'react-select';
import { connect } from 'react-redux';
import { getParamObject } from '../exploreUtils';
const propTypes = {
can_edit: PropTypes.bool,
can_overwrite: PropTypes.bool,
onHide: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired,
form_data: PropTypes.object,
datasource_type: PropTypes.string.isRequired,
user_id: PropTypes.string.isRequired,
dashboards: PropTypes.array.isRequired,
alert: PropTypes.string,
slice: PropTypes.object,
datasource: PropTypes.object,
};
class SaveModal extends React.Component {
@@ -26,7 +26,7 @@ class SaveModal extends React.Component {
newSliceName: '',
dashboards: [],
alert: null,
action: 'overwrite',
action: 'saveas',
addToDash: 'noSave',
};
}
@@ -58,13 +58,13 @@ class SaveModal extends React.Component {
saveOrOverwrite(gotodash) {
this.setState({ alert: null });
this.props.actions.removeSaveModalAlert();
const params = getParamObject(
this.props.form_data, this.props.datasource_type, this.state.action === 'saveas');
const sliceParams = {};
params.datasource_name = this.props.form_data.datasource_name;
let sliceName = null;
sliceParams.action = this.state.action;
if (this.props.slice.slice_id) {
sliceParams.slice_id = this.props.slice.slice_id;
}
if (sliceParams.action === 'saveas') {
sliceName = this.state.newSliceName;
if (sliceName === '') {
@@ -73,7 +73,7 @@ class SaveModal extends React.Component {
}
sliceParams.slice_name = sliceName;
} else {
sliceParams.slice_name = this.props.form_data.slice_name;
sliceParams.slice_name = this.props.slice.slice_name;
}
const addToDash = this.state.addToDash;
@@ -100,9 +100,13 @@ class SaveModal extends React.Component {
dashboard = null;
}
sliceParams.goto_dash = gotodash;
const baseUrl = '/superset/explore/' +
`${this.props.datasource_type}/${this.props.form_data.datasource}/`;
const saveUrl = `${baseUrl}?${$.param(params, true)}&${$.param(sliceParams, true)}`;
const baseUrl = `/superset/explore/${this.props.datasource.type}/${this.props.datasource.id}/`;
sliceParams.datasource_name = this.props.datasource.name;
const saveUrl = `${baseUrl}?form_data=` +
`${encodeURIComponent(JSON.stringify(this.props.form_data))}` +
`&${$.param(sliceParams, true)}`;
this.props.actions.saveSlice(saveUrl);
this.props.onHide();
}
@@ -136,11 +140,11 @@ class SaveModal extends React.Component {
</Alert>
}
<Radio
disabled={!this.props.can_edit}
disabled={!this.props.can_overwrite}
checked={this.state.action === 'overwrite'}
onChange={this.changeAction.bind(this, 'overwrite')}
>
{`Overwrite slice ${this.props.form_data.slice_name}`}
{`Overwrite slice ${this.props.slice.slice_name}`}
</Radio>
<Radio
@@ -223,7 +227,9 @@ SaveModal.propTypes = propTypes;
function mapStateToProps(state) {
return {
can_edit: state.can_edit,
datasource: state.datasource,
slice: state.slice,
can_overwrite: state.can_overwrite,
user_id: state.user_id,
dashboards: state.dashboards,
alert: state.saveModalAlert,

View File

@@ -5,8 +5,8 @@ const propTypes = {
choices: PropTypes.array,
clearable: PropTypes.bool,
description: PropTypes.string,
editUrl: PropTypes.string,
freeForm: PropTypes.bool,
isLoading: PropTypes.bool,
label: PropTypes.string,
multi: PropTypes.bool,
name: PropTypes.string.isRequired,
@@ -18,21 +18,26 @@ const defaultProps = {
choices: [],
clearable: true,
description: null,
editUrl: null,
freeForm: false,
isLoading: false,
label: null,
multi: false,
onChange: () => {},
value: '',
};
export default class SelectField extends React.Component {
export default class SelectField extends React.PureComponent {
constructor(props) {
super(props);
this.state = { options: this.getOptions() };
this.state = { options: this.getOptions(props) };
this.onChange = this.onChange.bind(this);
this.renderOption = this.renderOption.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.choices !== this.props.choices) {
const options = this.getOptions(nextProps);
this.setState({ options });
}
}
onChange(opt) {
let optionValue = opt ? opt.value : null;
// if multi, return options values as an array
@@ -41,8 +46,8 @@ export default class SelectField extends React.Component {
}
this.props.onChange(optionValue);
}
getOptions() {
const options = this.props.choices.map((c) => {
getOptions(props) {
const options = props.choices.map((c) => {
const label = c.length > 1 ? c[1] : c[0];
const newOptions = {
value: c[0],
@@ -51,19 +56,19 @@ export default class SelectField extends React.Component {
if (c[2]) newOptions.imgSrc = c[2];
return newOptions;
});
if (this.props.freeForm) {
if (props.freeForm) {
// For FreeFormSelect, insert value into options if not exist
const values = this.props.choices.map((c) => c[0]);
if (this.props.value) {
if (typeof this.props.value === 'object') {
this.props.value.forEach((v) => {
const values = props.choices.map((c) => c[0]);
if (props.value) {
if (typeof props.value === 'object') {
props.value.forEach((v) => {
if (values.indexOf(v) === -1) {
options.push({ value: v, label: v });
}
});
} else {
if (values.indexOf(this.props.value) === -1) {
options.push({ value: this.props.value, label: this.props.value });
if (values.indexOf(props.value) === -1) {
options.push({ value: props.value, label: props.value });
}
}
}
@@ -91,6 +96,7 @@ export default class SelectField extends React.Component {
value: this.props.value,
autosize: false,
clearable: this.props.clearable,
isLoading: this.props.isLoading,
onChange: this.onChange,
optionRenderer: this.renderOption,
};
@@ -100,9 +106,6 @@ export default class SelectField extends React.Component {
return (
<div>
{selectWrap}
{this.props.editUrl &&
<a href={`${this.props.editUrl}/${this.props.value}`}>edit</a>
}
</div>
);
}

View File

@@ -1,55 +1,19 @@
/* eslint camelcase: 0 */
const $ = require('jquery');
function formatFilters(filters) {
// outputs an object of url params of filters
// prefix can be 'flt' or 'having'
const params = {};
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
params[`${filter.prefix}_col_${i + 1}`] = filter.col;
params[`${filter.prefix}_op_${i + 1}`] = filter.op;
if (filter.value.constructor === Array) {
params[`${filter.prefix}_eq_${i + 1}`] = filter.value.join(',');
} else {
params[`${filter.prefix}_eq_${i + 1}`] = filter.value;
}
}
return params;
}
export function getParamObject(form_data, datasource_type, saveNewSlice) {
const data = {
// V2 tag temporarily for updating url
// Todo: remove after launch
V2: true,
datasource_id: form_data.datasource,
datasource_type,
};
Object.keys(form_data).forEach((field) => {
// filter out null fields
if (form_data[field] !== null && field !== 'datasource' && field !== 'filters'
&& !(saveNewSlice && field === 'slice_name')) {
data[field] = form_data[field];
}
});
const filterParams = formatFilters(form_data.filters);
Object.assign(data, filterParams);
return data;
}
export function getExploreUrl(form_data, datasource_type, endpoint = 'base') {
const data = getParamObject(form_data, datasource_type);
const params = `${datasource_type}/` +
`${form_data.datasource}/?${$.param(data, true)}`;
export function getExploreUrl(form_data, dummy, endpoint = 'base') {
const [datasource_id, datasource_type] = form_data.datasource.split('__');
let params = `${datasource_type}/${datasource_id}/`;
params += '?form_data=' + encodeURIComponent(JSON.stringify(form_data));
switch (endpoint) {
case 'base':
return `/superset/explore/${params}`;
case 'json':
return `/superset/explore_json/${params}`;
case 'csv':
return `/superset/explore/${params}&csv=true`;
return `/superset/explore_json/${params}&csv=true`;
case 'standalone':
return `/superset/explore/${params}&standalone=true`;
case 'query':
return `/superset/explore_json/${params}&query=true`;
default:
return `/superset/explore/${params}`;
}

View File

@@ -7,6 +7,8 @@ import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { now } from '../modules/dates';
import { initEnhancer } from '../reduxUtils';
import { getFieldsState, getFormDataFromFields } from './stores/store';
// jquery and bootstrap required to make bootstrap dropdown menu's work
const $ = window.$ = require('jquery'); // eslint-disable-line
@@ -14,58 +16,30 @@ const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
require('bootstrap');
require('./main.css');
import { initialState } from './stores/store';
const exploreViewContainer = document.getElementById('js-explore-view-container');
const bootstrapData = JSON.parse(exploreViewContainer.getAttribute('data-bootstrap'));
const fields = getFieldsState(bootstrapData, bootstrapData.form_data);
delete bootstrapData.form_data;
import { exploreReducer } from './reducers/exploreReducer';
// Initial state
const bootstrappedState = Object.assign(
initialState(bootstrapData.viz.form_data.viz_type, bootstrapData.datasource_type), {
can_edit: bootstrapData.can_edit,
can_download: bootstrapData.can_download,
datasources: bootstrapData.datasources,
datasource_type: bootstrapData.datasource_type,
viz: bootstrapData.viz,
user_id: bootstrapData.user_id,
chartUpdateStartTime: now(),
chartUpdateEndTime: null,
bootstrapData, {
chartStatus: 'loading',
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
dashboards: [],
fields,
latestQueryFormData: getFormDataFromFields(fields),
filterColumnOpts: [],
isDatasourceMetaLoading: false,
isStarred: false,
queryResponse: null,
triggerQuery: true,
triggerRender: false,
}
);
bootstrappedState.viz.form_data.datasource = parseInt(bootstrapData.datasource_id, 10);
bootstrappedState.viz.form_data.datasource_name = bootstrapData.datasource_name;
function parseFilters(form_data, prefix = 'flt') {
const filters = [];
for (let i = 0; i <= 10; i++) {
if (form_data[`${prefix}_col_${i}`] && form_data[`${prefix}_op_${i}`]) {
filters.push({
prefix,
col: form_data[`${prefix}_col_${i}`],
op: form_data[`${prefix}_op_${i}`],
value: form_data[`${prefix}_eq_${i}`],
});
}
/* eslint no-param-reassign: 0 */
delete form_data[`${prefix}_col_${i}`];
delete form_data[`${prefix}_op_${i}`];
delete form_data[`${prefix}_eq_${i}`];
}
return filters;
}
function getFilters(form_data, datasource_type) {
if (datasource_type === 'table') {
return parseFilters(form_data);
}
return parseFilters(form_data).concat(parseFilters(form_data, 'having'));
}
bootstrappedState.viz.form_data.filters =
getFilters(bootstrappedState.viz.form_data, bootstrapData.datasource_type);
const store = createStore(exploreReducer, bootstrappedState,
compose(applyMiddleware(thunk), initEnhancer(false))

View File

@@ -1,5 +1,5 @@
/* eslint camelcase: 0 */
import { defaultFormData } from '../stores/store';
import { getFieldsState, getFormDataFromFields } from '../stores/store';
import * as actions from '../actions/exploreActions';
import { now } from '../../modules/dates';
@@ -9,15 +9,15 @@ export const exploreReducer = function (state, action) {
return Object.assign({}, state, { isStarred: action.isStarred });
},
[actions.FETCH_STARTED]() {
[actions.FETCH_DATASOURCE_STARTED]() {
return Object.assign({}, state, { isDatasourceMetaLoading: true });
},
[actions.FETCH_SUCCEEDED]() {
[actions.FETCH_DATASOURCE_SUCCEEDED]() {
return Object.assign({}, state, { isDatasourceMetaLoading: false });
},
[actions.FETCH_FAILED]() {
[actions.FETCH_DATASOURCE_FAILED]() {
// todo(alanna) handle failure/error state
return Object.assign({}, state,
{
@@ -25,6 +25,28 @@ export const exploreReducer = function (state, action) {
controlPanelAlert: action.error,
});
},
[actions.SET_DATASOURCE]() {
return Object.assign({}, state, { datasource: action.datasource });
},
[actions.FETCH_DATASOURCES_STARTED]() {
return Object.assign({}, state, { isDatasourcesLoading: true });
},
[actions.FETCH_DATASOURCES_SUCCEEDED]() {
return Object.assign({}, state, { isDatasourcesLoading: false });
},
[actions.FETCH_DATASOURCES_FAILED]() {
// todo(alanna) handle failure/error state
return Object.assign({}, state,
{
isDatasourcesLoading: false,
controlPanelAlert: action.error,
});
},
[actions.SET_DATASOURCES]() {
return Object.assign({}, state, { datasources: action.datasources });
},
[actions.REMOVE_CONTROL_PANEL_ALERT]() {
return Object.assign({}, state, { controlPanelAlert: null });
},
@@ -36,32 +58,17 @@ export const exploreReducer = function (state, action) {
return Object.assign({}, state,
{ saveModalAlert: `fetching dashboards failed for ${action.userId}` });
},
[actions.SET_DATASOURCE]() {
return Object.assign({}, state, { datasource: action.datasource });
},
[actions.SET_FIELD_VALUE]() {
let newFormData = Object.assign({}, state.viz.form_data);
if (action.fieldName === 'datasource') {
newFormData = defaultFormData(state.viz.form_data.viz_type, action.datasource_type);
newFormData.datasource_name = action.label;
newFormData.slice_id = state.viz.form_data.slice_id;
newFormData.slice_name = state.viz.form_data.slice_name;
newFormData.viz_type = state.viz.form_data.viz_type;
}
newFormData[action.fieldName] = action.value;
const fields = Object.assign({}, state.fields);
const field = fields[action.fieldName];
const field = Object.assign({}, fields[action.fieldName]);
field.value = action.value;
field.validationErrors = action.validationErrors;
return Object.assign(
{},
state,
{
fields,
viz: Object.assign({}, state.viz, { form_data: newFormData }),
}
);
fields[action.fieldName] = field;
const changes = { fields };
if (field.renderTrigger) {
changes.triggerRender = true;
}
return Object.assign({}, state, changes);
},
[actions.CHART_UPDATE_SUCCEEDED]() {
return Object.assign(
@@ -79,6 +86,16 @@ export const exploreReducer = function (state, action) {
chartStatus: 'loading',
chartUpdateEndTime: null,
chartUpdateStartTime: now(),
triggerQuery: false,
queryRequest: action.queryRequest,
latestQueryFormData: getFormDataFromFields(state.fields),
});
},
[actions.CHART_UPDATE_STOPPED]() {
return Object.assign({}, state,
{
chartStatus: 'stopped',
chartAlert: 'Updating chart was stopped',
});
},
[actions.CHART_RENDERING_FAILED]() {
@@ -87,10 +104,15 @@ export const exploreReducer = function (state, action) {
chartAlert: 'An error occurred while rendering the visualization: ' + action.error,
});
},
[actions.TRIGGER_QUERY]() {
return Object.assign({}, state, {
triggerQuery: true,
});
},
[actions.CHART_UPDATE_FAILED]() {
return Object.assign({}, state, {
chartStatus: 'failed',
chartAlert: action.queryResponse.error,
chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.',
chartUpdateEndTime: now(),
queryResponse: action.queryResponse,
});
@@ -114,6 +136,13 @@ export const exploreReducer = function (state, action) {
[actions.REMOVE_SAVE_MODAL_ALERT]() {
return Object.assign({}, state, { saveModalAlert: null });
},
[actions.RESET_FIELDS]() {
const fields = getFieldsState(state, getFormDataFromFields(state.fields));
return Object.assign({}, state, { fields });
},
[actions.RENDER_TRIGGERED]() {
return Object.assign({}, state, { triggerRender: false });
},
};
if (action.type in actionHandlers) {
return actionHandlers[action.type]();

View File

@@ -1,4 +1,5 @@
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
import React from 'react';
import visTypes from './visTypes';
import * as v from '../validators';
@@ -26,21 +27,23 @@ export const TIME_STAMP_OPTIONS = [
['%H:%M:%S', '%H:%M:%S | 01:32:10'],
];
const MAP_DATASOURCE_TYPE_TO_EDIT_URL = {
table: '/tablemodelview/edit',
druid: '/druiddatasourcemodelview/edit',
};
export const fields = {
datasource: {
type: 'SelectField',
label: 'Datasource',
isLoading: true,
clearable: false,
default: null,
mapStateToProps: (state) => ({
choices: state.datasources || [],
editUrl: MAP_DATASOURCE_TYPE_TO_EDIT_URL[state.datasource_type],
}),
mapStateToProps: (state) => {
const datasources = state.datasources || [];
return {
choices: datasources,
isLoading: datasources.length === 0,
rightNode: state.datasource ?
<a href={state.datasource.edit_url}>edit</a>
: null,
};
},
description: '',
},
@@ -62,10 +65,10 @@ export const fields = {
multi: true,
label: 'Metrics',
validators: [v.nonEmpty],
default: field => field.choices !== null ? [field.choices[0][0]] : null,
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
}),
default: [],
description: 'One or many metrics to display',
},
@@ -83,10 +86,11 @@ export const fields = {
metric: {
type: 'SelectField',
label: 'Metric',
default: null,
clearable: false,
description: 'Choose the metric',
default: field => field.choices && field.choices.length > 0 ? field.choices[0][0] : null,
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
choices: (state.datasource) ? state.datasource.metrics_combo : null,
}),
},
@@ -185,6 +189,7 @@ export const fields = {
bar_stacked: {
type: 'CheckboxField',
label: 'Stacked Bars',
renderTrigger: true,
default: false,
description: null,
},
@@ -192,6 +197,7 @@ export const fields = {
show_markers: {
type: 'CheckboxField',
label: 'Show Markers',
renderTrigger: true,
default: false,
description: 'Show data points as circle markers on the lines',
},
@@ -200,6 +206,7 @@ export const fields = {
type: 'CheckboxField',
label: 'Bar Values',
default: false,
renderTrigger: true,
description: 'Show the value on top of the bar',
},
@@ -213,6 +220,7 @@ export const fields = {
show_controls: {
type: 'CheckboxField',
label: 'Extra Controls',
renderTrigger: true,
default: false,
description: 'Whether to show extra controls or not. Extra controls ' +
'include things like making mulitBar charts stacked ' +
@@ -222,6 +230,7 @@ export const fields = {
reduce_x_ticks: {
type: 'CheckboxField',
label: 'Reduce X ticks',
renderTrigger: true,
default: false,
description: 'Reduces the number of X axis ticks to be rendered. ' +
'If true, the x axis wont overflow and labels may be ' +
@@ -233,6 +242,7 @@ export const fields = {
include_series: {
type: 'CheckboxField',
label: 'Include Series',
renderTrigger: true,
default: false,
description: 'Include series name as an axis',
},
@@ -276,7 +286,9 @@ export const fields = {
type: 'SelectField',
multi: true,
label: 'Columns',
choices: [],
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.gb_cols : [],
}),
default: [],
description: 'One or many fields to pivot as columns',
},
@@ -408,28 +420,28 @@ export const fields = {
granularity_sqla: {
type: 'SelectField',
label: 'Time Column',
default: null,
default: field => field.choices && field.choices.length > 0 ? field.choices[0][0] : null,
description: 'The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' +
'column in the table or. Also note that the ' +
'filter below is applied against this column or ' +
'expression',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.all_cols : [],
choices: (state.datasource) ? state.datasource.granularity_sqla : [],
}),
},
time_grain_sqla: {
type: 'SelectField',
label: 'Time Grain',
default: 'Time Column',
default: field => field.choices && field.choices.length ? field.choices[0][0] : null,
description: 'The time granularity for the visualization. This ' +
'applies a date transformation to alter ' +
'your time column and defines a new time granularity. ' +
'The options here are defined on a per database ' +
'engine basis in the Superset source code.',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.time_grain_sqla : [],
choices: (state.datasource) ? state.datasource.time_grain_sqla : null,
}),
},
@@ -605,7 +617,7 @@ export const fields = {
default: null,
description: 'Metric assigned to the [X] axis',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.gb_cols : [],
choices: (state.datasource) ? state.datasource.metrics_combo : [],
}),
},
@@ -639,12 +651,14 @@ export const fields = {
x_axis_label: {
type: 'TextField',
label: 'X Axis Label',
renderTrigger: true,
default: '',
},
y_axis_label: {
type: 'TextField',
label: 'Y Axis Label',
renderTrigger: true,
default: '',
},
@@ -712,6 +726,7 @@ export const fields = {
type: 'SelectField',
freeForm: true,
label: 'X axis format',
renderTrigger: true,
default: 'smart_date',
choices: TIME_STAMP_OPTIONS,
description: D3_FORMAT_DOCS,
@@ -721,6 +736,7 @@ export const fields = {
type: 'SelectField',
freeForm: true,
label: 'Y axis format',
renderTrigger: true,
default: '.3s',
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
@@ -754,6 +770,7 @@ export const fields = {
line_interpolation: {
type: 'SelectField',
label: 'Line Style',
renderTrigger: true,
choices: formatSelectOptions(['linear', 'basis', 'cardinal',
'monotone', 'step-before', 'step-after']),
default: 'linear',
@@ -782,6 +799,7 @@ export const fields = {
pandas_aggfunc: {
type: 'SelectField',
label: 'Aggregation function',
clearable: false,
choices: formatSelectOptions([
'sum',
'mean',
@@ -815,6 +833,7 @@ export const fields = {
show_brush: {
type: 'CheckboxField',
label: 'Range Filter',
renderTrigger: true,
default: false,
description: 'Whether to display the time range interactive selector',
},
@@ -836,6 +855,7 @@ export const fields = {
include_search: {
type: 'CheckboxField',
label: 'Search Box',
renderTrigger: true,
default: false,
description: 'Whether to include a client side search box',
},
@@ -851,12 +871,14 @@ export const fields = {
type: 'CheckboxField',
label: 'Show Bubbles',
default: false,
renderTrigger: true,
description: 'Whether to display bubbles on top of countries',
},
show_legend: {
type: 'CheckboxField',
label: 'Legend',
renderTrigger: true,
default: true,
description: 'Whether to display the legend (toggles)',
},
@@ -864,6 +886,7 @@ export const fields = {
x_axis_showminmax: {
type: 'CheckboxField',
label: 'X bounds',
renderTrigger: true,
default: true,
description: 'Whether to display the min and max values of the X axis',
},
@@ -871,6 +894,7 @@ export const fields = {
rich_tooltip: {
type: 'CheckboxField',
label: 'Rich Tooltip',
renderTrigger: true,
default: true,
description: 'The rich tooltip shows a list of all series for that ' +
'point in time',
@@ -880,6 +904,7 @@ export const fields = {
type: 'CheckboxField',
label: 'Y Axis Zero',
default: false,
renderTrigger: true,
description: 'Force the Y axis to start at 0 instead of the minimum value',
},
@@ -887,6 +912,7 @@ export const fields = {
type: 'CheckboxField',
label: 'Y Log Scale',
default: false,
renderTrigger: true,
description: 'Use a log scale for the Y axis',
},
@@ -894,6 +920,7 @@ export const fields = {
type: 'CheckboxField',
label: 'X Log Scale',
default: false,
renderTrigger: true,
description: 'Use a log scale for the X axis',
},
@@ -1005,12 +1032,12 @@ export const fields = {
point_radius: {
type: 'SelectField',
label: 'Point Radius',
default: null,
default: 'Auto',
description: 'The radius of individual points (ones that are not in a cluster). ' +
'Either a numerical column or `Auto`, which scales the point based ' +
'on the largest cluster',
mapStateToProps: (state) => ({
choices: state.fields.point_radius.choices,
choices: [].concat([['Auto', 'Auto']], state.datasource.all_cols),
}),
},
@@ -1133,34 +1160,24 @@ export const fields = {
datasource: state.datasource,
}),
},
having_filters: {
type: 'FilterField',
label: '',
default: [],
description: '',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.metrics_combo
.concat(state.datasource.filterable_cols) : [],
datasource: state.datasource,
}),
},
slice_id: {
type: 'HiddenField',
label: 'Slice ID',
hidden: true,
description: 'The id of the active slice',
},
};
export default fields;
// Control Panel fields that re-render chart without need for 'Query button'
export const autoQueryFields = [
'datasource',
'viz_type',
'bar_stacked',
'show_markers',
'show_bar_value',
'order_bars',
'show_controls',
'reduce_x_ticks',
'include_series',
'pie_label_type',
'show_brush',
'include_search',
'show_bubbles',
'show_legend',
'x_axis_showminmax',
'rich_tooltip',
'y_axis_zero',
'y_log_scale',
'x_log_scale',
'donut',
'labels_outside',
'contribution',
'size',
'row_limit',
'max_bubble_size',
];

View File

@@ -1,56 +1,111 @@
/* eslint camelcase: 0 */
import { sectionsToRender } from './visTypes';
import fields from './fields';
import visTypes, { sectionsToRender } from './visTypes';
export function defaultFormData(vizType = 'table', datasourceType = 'table') {
const data = {
slice_name: null,
slice_id: null,
datasource_name: null,
filters: [],
};
const sections = sectionsToRender(vizType, datasourceType);
sections.forEach((section) => {
section.fieldSetRows.forEach((fieldSetRow) => {
fieldSetRow.forEach((k) => {
data[k] = fields[k].default;
});
});
export function getFormDataFromFields(fieldsState) {
const formData = {};
Object.keys(fieldsState).forEach(fieldName => {
formData[fieldName] = fieldsState[fieldName].value;
});
return data;
return formData;
}
export function defaultViz(vizType, datasourceType = 'table') {
return {
cached_key: null,
cached_timeout: null,
cached_dttm: null,
column_formats: null,
csv_endpoint: null,
is_cached: false,
data: [],
form_data: defaultFormData(vizType, datasourceType),
json_endpoint: null,
query: null,
standalone_endpoint: null,
};
export function getFieldNames(vizType, datasourceType) {
const fieldNames = [];
sectionsToRender(vizType, datasourceType).forEach(
section => section.fieldSetRows.forEach(
fsr => fsr.forEach(
f => fieldNames.push(f))));
return fieldNames;
}
export function initialState(vizType = 'table', datasourceType = 'table') {
return {
dashboards: [],
isDatasourceMetaLoading: false,
datasources: null,
datasource_type: null,
filterColumnOpts: [],
fields,
viz: defaultViz(vizType, datasourceType),
isStarred: false,
};
export function getFieldsState(state, form_data) {
/*
* Gets a new fields object to put in the state. The fields object
* is similar to the configuration field with only the fields
* related to the current viz_type, materializes mapStateToProps functions,
* adds value keys coming from form_data passed here. This can't be an action creator
* just yet because it's used in both the explore and dashboard views.
* */
// Getting a list of active field names for the current viz
const formData = Object.assign({}, form_data);
const vizType = formData.viz_type || 'table';
const fieldNames = getFieldNames(vizType, state.datasource.type);
const viz = visTypes[vizType];
const fieldOverrides = viz.fieldOverrides || {};
const fieldsState = {};
fieldNames.forEach((k) => {
const field = Object.assign({}, fields[k], fieldOverrides[k]);
if (field.mapStateToProps) {
Object.assign(field, field.mapStateToProps(state));
delete field.mapStateToProps;
}
// If the value is not valid anymore based on choices, clear it
if (field.type === 'SelectField' && field.choices && k !== 'datasource' && formData[k]) {
const choiceValues = field.choices.map(c => c[0]);
if (field.multi && formData[k].length > 0 && choiceValues.indexOf(formData[k][0]) < 0) {
delete formData[k];
} else if (!field.multi && !field.freeForm && choiceValues.indexOf(formData[k]) < 0) {
delete formData[k];
}
}
// Removing invalid filters that point to a now inexisting column
if (field.type === 'FilterField' && field.choices) {
const choiceValues = field.choices.map(c => c[0]);
formData[k] = field.value.filter(flt => choiceValues.indexOf(flt.col) > 0);
}
if (typeof field.default === 'function') {
field.default = field.default(field);
}
field.value = formData[k] !== undefined ? formData[k] : field.default;
fieldsState[k] = field;
});
return fieldsState;
}
export function applyDefaultFormData(form_data) {
const datasourceType = form_data.datasource.split('__')[1];
const vizType = form_data.viz_type || 'table';
const viz = visTypes[vizType];
const fieldNames = getFieldNames(vizType, datasourceType);
const fieldOverrides = viz.fieldOverrides || {};
const formData = {};
fieldNames.forEach(k => {
const field = Object.assign({}, fields[k]);
if (fieldOverrides[k]) {
Object.assign(field, fieldOverrides[k]);
}
if (form_data[k] === undefined) {
if (typeof field.default === 'function') {
formData[k] = field.default(fields[k]);
} else {
formData[k] = field.default;
}
} else {
formData[k] = form_data[k];
}
});
return formData;
}
// Control Panel fields that re-render chart without need for 'Query button'
export const autoQueryFields = [
'datasource',
'viz_type',
];
const defaultFields = Object.assign({}, fields);
Object.keys(fields).forEach((f) => {
defaultFields[f].value = fields[f].default;
});
const defaultState = {
fields: defaultFields,
form_data: getFormDataFromFields(defaultFields),
};
export { defaultFields, defaultState };

View File

@@ -12,6 +12,7 @@ export const commonControlPanelSections = {
fieldSetRows: [
['datasource'],
['viz_type'],
['slice_id'],
],
},
sqlaTimeSeries: {
@@ -60,15 +61,13 @@ export const commonControlPanelSections = {
'Leave the value field empty to filter empty strings or nulls' +
'For filters with comma in values, wrap them in single quotes' +
"as in <NY, 'Tahoe, CA', DC>",
prefix: 'flt',
fieldSetRows: [['filters']],
},
{
label: 'Result Filters',
description: 'The filters to apply after post-aggregation.' +
'Leave the value field empty to filter empty strings or nulls',
prefix: 'having',
fieldSetRows: [['filters']],
fieldSetRows: [['having_filters']],
},
],
};
@@ -250,8 +249,7 @@ const visTypes = {
label: 'Options',
fieldSetRows: [
['table_timestamp_format'],
['row_limit'],
['page_length'],
['row_limit', 'page_length'],
['include_search', 'table_filter'],
],
},
@@ -433,6 +431,7 @@ const visTypes = {
},
big_number_total: {
label: 'Big Number',
controlPanelSections: [
{
label: null,
@@ -758,12 +757,11 @@ export function sectionsToRender(vizType, datasourceType) {
const { datasourceAndVizType, sqlClause, filters } = commonControlPanelSections;
const filtersToRender =
datasourceType === 'table' ? filters[0] : filters;
const sections = [].concat(
return [].concat(
datasourceAndVizType,
timeSection,
viz.controlPanelSections,
sqlClause,
filtersToRender
);
return sections;
}

View File

@@ -4,6 +4,8 @@ const utils = require('./utils');
// vis sources
/* eslint camel-case: 0 */
import vizMap from '../../visualizations/main.js';
import { getExploreUrl } from '../explorev2/exploreUtils';
import { applyDefaultFormData } from '../explorev2/stores/store';
/* eslint wrap-iife: 0*/
const px = function () {
@@ -55,12 +57,14 @@ const px = function () {
}
const Slice = function (data, controller) {
let timer;
const token = $('#' + data.token);
const containerId = data.token + '_con';
const token = $('#token_' + data.slice_id);
const containerId = 'con_' + data.slice_id;
const selector = '#' + containerId;
const container = $(selector);
const sliceId = data.slice_id;
const origJsonEndpoint = data.json_endpoint;
const formData = applyDefaultFormData(data.form_data);
const jsonEndpoint = getExploreUrl(formData, 'table', 'json');
const origJsonEndpoint = jsonEndpoint;
let dttm = 0;
const stopwatch = function () {
dttm += 10;
@@ -70,12 +74,13 @@ const px = function () {
let qrystr = '';
slice = {
data,
formData,
container,
containerId,
selector,
querystring() {
const parser = document.createElement('a');
parser.href = data.json_endpoint;
parser.href = jsonEndpoint;
if (controller.type === 'dashboard') {
parser.href = origJsonEndpoint;
let flts = controller.effectiveExtraFilters(sliceId);
@@ -100,7 +105,7 @@ const px = function () {
},
jsonEndpoint() {
const parser = document.createElement('a');
parser.href = data.json_endpoint;
parser.href = jsonEndpoint;
let endpoint = parser.pathname + this.querystring();
if (endpoint.charAt(0) !== '/') {
// Known issue for IE <= 11:
@@ -114,8 +119,11 @@ const px = function () {
d3format(col, number) {
// uses the utils memoized d3format function and formats based on
// column level defined preferences
const format = data.column_formats[col];
return utils.d3format(format, number);
if (data.column_formats) {
const format = data.column_formats[col];
return utils.d3format(format, number);
}
return utils.d3format('.3s', number);
},
/* eslint no-shadow: 0 */
always(data) {
@@ -224,7 +232,7 @@ const px = function () {
$('#timer').addClass('label-warning');
$.getJSON(this.jsonEndpoint(), queryResponse => {
try {
vizMap[data.form_data.viz_type](this, queryResponse);
vizMap[formData.viz_type](this, queryResponse);
this.done(queryResponse);
} catch (e) {
this.error('An error occurred while rendering the visualization: ' + e);

View File

@@ -133,14 +133,14 @@ export function formatSelectOptionsForRange(start, end) {
// returns [[1,1], [2,2], [3,3], [4,4], [5,5]]
const options = [];
for (let i = start; i <= end; i++) {
options.push([i.toString(), i.toString()]);
options.push([i, i.toString()]);
}
return options;
}
export function formatSelectOptions(options) {
return options.map((opt) =>
[opt.toString(), opt.toString()]
[opt, opt.toString()]
);
}

View File

@@ -1,17 +0,0 @@
const $ = window.$ = require('jquery');
/* eslint no-unused-vars: 0 */
const jQuery = window.jQuery = $;
const px = require('./modules/superset.js');
const utils = require('./modules/utils.js');
require('bootstrap');
const standaloneController = Object.assign(
{}, utils.controllerInterface, { type: 'standalone' });
$(document).ready(function () {
const data = $('.slice').data('slice');
const slice = px.Slice(data, standaloneController);
slice.render();
slice.bindResizeToWindowResize();
});

View File

@@ -1,5 +0,0 @@
require('../node_modules/select2/select2.css');
require('../node_modules/select2-bootstrap-css/select2-bootstrap.min.css');
require('../node_modules/jquery-ui/themes/base/jquery-ui.css');
require('select2');
require('../vendor/select2.sortable.js');

View File

@@ -0,0 +1,25 @@
/* eslint no-console: 0 */
import fs from 'fs';
import path from 'path';
import { fields } from './explorev2/stores/fields';
function exportFile(fileLocation, content) {
fs.writeFile(fileLocation, content, function (err) {
if (err) {
console.log(`File ${fileLocation} was not saved... :(`);
} else {
console.log(`File ${fileLocation} was saved!`);
}
});
}
function main() {
const APP_DIR = path.resolve(__dirname, './');
const dir = APP_DIR + '/../dist/';
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const blob = { fields };
exportFile(APP_DIR + '/../backendSync.json', JSON.stringify(blob, null, 2));
}
main();