mirror of
https://github.com/apache/superset.git
synced 2026-04-20 08:34:37 +00:00
[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:
committed by
GitHub
parent
3b023e5eaa
commit
0cc8eff1c3
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import CopyToClipboard from './../../components/CopyToClipboard';
|
||||
import { Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default class EmbedCodeButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
height: '400',
|
||||
width: '600',
|
||||
};
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const value = e.currentTarget.value;
|
||||
const name = e.currentTarget.name;
|
||||
const data = {};
|
||||
data[name] = value;
|
||||
this.setState(data);
|
||||
}
|
||||
|
||||
generateEmbedHTML() {
|
||||
const srcLink = (
|
||||
window.location.origin +
|
||||
this.props.slice.data.standalone_endpoint +
|
||||
`&height=${this.state.height}`
|
||||
);
|
||||
return (
|
||||
'<iframe\n' +
|
||||
` width="${this.state.width}"\n` +
|
||||
` height="${this.state.height}"\n` +
|
||||
' seamless\n' +
|
||||
' frameBorder="0"\n' +
|
||||
' scrolling="no"\n' +
|
||||
` src="${srcLink}"\n` +
|
||||
'>\n' +
|
||||
'</iframe>'
|
||||
);
|
||||
}
|
||||
|
||||
renderPopover() {
|
||||
const html = this.generateEmbedHTML();
|
||||
return (
|
||||
<Popover id="embed-code-popover">
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-sm-10">
|
||||
<textarea
|
||||
name="embedCode"
|
||||
value={html}
|
||||
rows="4"
|
||||
readOnly
|
||||
className="form-control input-sm"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<CopyToClipboard
|
||||
shouldShowText={false}
|
||||
text={html}
|
||||
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className="row">
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-height">Height</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
defaultValue={this.state.height}
|
||||
name="height"
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 col-sm-12">
|
||||
<div className="form-group">
|
||||
<small>
|
||||
<label className="control-label" htmlFor="embed-width">Width</label>
|
||||
</small>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
type="text"
|
||||
defaultValue={this.state.width}
|
||||
name="width"
|
||||
onChange={this.handleInputChange}
|
||||
id="embed-width"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
rootClose
|
||||
placement="left"
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<span className="btn btn-default btn-sm">
|
||||
<i className="fa fa-code"></i>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmbedCodeButton.propTypes = propTypes;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { ButtonGroup, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import Button from '../../components/Button';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const propTypes = {
|
||||
canAdd: PropTypes.string.isRequired,
|
||||
onQuery: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func,
|
||||
onStop: PropTypes.func,
|
||||
loading: PropTypes.bool,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onStop: () => {},
|
||||
onSave: () => {},
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default function QueryAndSaveBtns(
|
||||
{ canAdd, onQuery, onSave, onStop, loading, errorMessage }) {
|
||||
const saveClasses = classnames({
|
||||
'disabled disabledButton': canAdd !== 'True',
|
||||
});
|
||||
const qryButtonStyle = errorMessage ? 'danger' : 'primary';
|
||||
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">
|
||||
{qryOrStopButton}
|
||||
<Button
|
||||
className={saveClasses}
|
||||
data-target="#save_modal"
|
||||
data-toggle="modal"
|
||||
disabled={saveButtonDisabled}
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="fa fa-plus-circle"></i> Save as
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{errorMessage &&
|
||||
<span>
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id={'query-error-tooltip'}>
|
||||
{errorMessage}
|
||||
</Tooltip>}
|
||||
>
|
||||
<i className="fa fa-exclamation-circle text-danger fa-lg" />
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryAndSaveBtns.propTypes = propTypes;
|
||||
QueryAndSaveBtns.defaultProps = defaultProps;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Popover, OverlayTrigger } from 'react-bootstrap';
|
||||
import CopyToClipboard from './../../components/CopyToClipboard';
|
||||
import { getShortUrl } from '../../../utils/common';
|
||||
|
||||
const propTypes = {
|
||||
slice: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default class URLShortLinkButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shortUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
onShortUrlSuccess(data) {
|
||||
this.setState({
|
||||
shortUrl: data,
|
||||
});
|
||||
}
|
||||
|
||||
getCopyUrl() {
|
||||
const longUrl = window.location.pathname + window.location.search;
|
||||
getShortUrl(longUrl, this.onShortUrlSuccess.bind(this));
|
||||
}
|
||||
|
||||
renderPopover() {
|
||||
const emailBody = `Check out this slice: ${this.state.shortUrl}`;
|
||||
return (
|
||||
<Popover id="shorturl-popover">
|
||||
<CopyToClipboard
|
||||
text={this.state.shortUrl}
|
||||
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
|
||||
/>
|
||||
|
||||
<a href={`mailto:?Subject=Superset%20Slice%20&Body=${emailBody}`}>
|
||||
<i className="fa fa-envelope"></i>
|
||||
</a>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
rootClose
|
||||
placement="left"
|
||||
onEnter={this.getCopyUrl.bind(this)}
|
||||
overlay={this.renderPopover()}
|
||||
>
|
||||
<span className="btn btn-default btn-sm">
|
||||
<i className="fa fa-link"></i>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
URLShortLinkButton.propTypes = propTypes;
|
||||
Reference in New Issue
Block a user