diff --git a/superset/assets/javascripts/components/FaveStar.jsx b/superset/assets/javascripts/components/FaveStar.jsx
new file mode 100644
index 000000000..4e6afa288
--- /dev/null
+++ b/superset/assets/javascripts/components/FaveStar.jsx
@@ -0,0 +1,44 @@
+import React, { PropTypes } from 'react';
+import cx from 'classnames';
+import TooltipWrapper from './TooltipWrapper';
+
+const propTypes = {
+ sliceId: PropTypes.string.isRequired,
+ actions: PropTypes.object.isRequired,
+ isStarred: PropTypes.bool.isRequired,
+};
+
+export default class FaveStar extends React.Component {
+ componentDidMount() {
+ this.props.actions.fetchFaveStar(this.props.sliceId);
+ }
+
+ onClick(e) {
+ e.preventDefault();
+ this.props.actions.saveFaveStar(this.props.sliceId, this.props.isStarred);
+ }
+
+ render() {
+ const iconClassNames = cx('fa', {
+ 'fa-star': this.props.isStarred,
+ 'fa-star-o': !this.props.isStarred,
+ });
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+FaveStar.propTypes = propTypes;
diff --git a/superset/assets/javascripts/components/TooltipWrapper.jsx b/superset/assets/javascripts/components/TooltipWrapper.jsx
new file mode 100644
index 000000000..56f0c148e
--- /dev/null
+++ b/superset/assets/javascripts/components/TooltipWrapper.jsx
@@ -0,0 +1,28 @@
+import React, { PropTypes } from 'react';
+import { Tooltip, OverlayTrigger } from 'react-bootstrap';
+import { slugify } from '../modules/utils';
+
+const propTypes = {
+ label: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ placement: PropTypes.string,
+};
+
+const defaultProps = {
+ placement: 'top',
+};
+
+export default function TooltipWrapper({ label, tooltip, children, placement }) {
+ return (
+ {tooltip}}
+ >
+ {children}
+
+ );
+}
+
+TooltipWrapper.propTypes = propTypes;
+TooltipWrapper.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js
index d944b839c..427c6bb70 100644
--- a/superset/assets/javascripts/explorev2/actions/exploreActions.js
+++ b/superset/assets/javascripts/explorev2/actions/exploreActions.js
@@ -1,5 +1,8 @@
/* eslint camelcase: 0 */
const $ = window.$ = require('jquery');
+
+const FAVESTAR_BASE_URL = '/superset/favstar/slice';
+
export const SET_FIELD_OPTIONS = 'SET_FIELD_OPTIONS';
export function setFieldOptions(options) {
return { type: SET_FIELD_OPTIONS, options };
@@ -48,6 +51,33 @@ export function fetchFieldOptions(datasourceId, datasourceType) {
};
}
+export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
+export function toggleFaveStar(isStarred) {
+ return { type: TOGGLE_FAVE_STAR, isStarred };
+}
+
+export const FETCH_FAVE_STAR = 'FETCH_FAVE_STAR';
+export function fetchFaveStar(sliceId) {
+ return function (dispatch) {
+ const url = `${FAVESTAR_BASE_URL}/${sliceId}/count`;
+ $.get(url, (data) => {
+ if (data.count > 0) {
+ dispatch(toggleFaveStar(true));
+ }
+ });
+ };
+}
+
+export const SAVE_FAVE_STAR = 'SAVE_FAVE_STAR';
+export function saveFaveStar(sliceId, isStarred) {
+ return function (dispatch) {
+ const urlSuffix = isStarred ? 'unselect' : 'select';
+ const url = `${FAVESTAR_BASE_URL}/${sliceId}/${urlSuffix}/`;
+ $.get(url);
+ dispatch(toggleFaveStar(!isStarred));
+ };
+}
+
export const ADD_FILTER = 'ADD_FILTER';
export function addFilter(filter) {
return { type: ADD_FILTER, filter };
diff --git a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
index a58009477..23cb2b62d 100644
--- a/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
+++ b/superset/assets/javascripts/explorev2/components/ChartContainer.jsx
@@ -5,9 +5,13 @@ import { Panel } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
import ExploreActionButtons from '../../explore/components/ExploreActionButtons';
+import FaveStar from '../../components/FaveStar';
+import TooltipWrapper from '../../components/TooltipWrapper';
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,
@@ -19,6 +23,7 @@ const propTypes = {
column_formats: PropTypes.object,
data: PropTypes.any,
isChartLoading: PropTypes.bool,
+ isStarred: PropTypes.bool.isRequired,
};
class ChartContainer extends React.Component {
@@ -150,6 +155,25 @@ class ChartContainer extends React.Component {
className="panel-title"
>
{this.props.slice_name}
+
+
+
+
+
+
+
+
+
diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
index 2bee4daba..e12d3172f 100644
--- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js
@@ -4,6 +4,10 @@ import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils
export const exploreReducer = function (state, action) {
const actionHandlers = {
+ [actions.TOGGLE_FAVE_STAR]() {
+ return Object.assign({}, state, { isStarred: action.isStarred });
+ },
+
[actions.FETCH_STARTED]() {
return Object.assign({}, state, { isDatasourceMetaLoading: true });
},
diff --git a/superset/assets/javascripts/explorev2/stores/store.js b/superset/assets/javascripts/explorev2/stores/store.js
index 504f7029b..fc07f502a 100644
--- a/superset/assets/javascripts/explorev2/stores/store.js
+++ b/superset/assets/javascripts/explorev2/stores/store.js
@@ -1679,5 +1679,6 @@ export function initialState(vizType = 'table') {
datasource_type: null,
fields,
viz: defaultViz(vizType),
+ isStarred: false,
};
}
diff --git a/superset/assets/stylesheets/exploreV2/exploreV2.css b/superset/assets/stylesheets/exploreV2/exploreV2.css
index b46e1262a..4745c2c42 100644
--- a/superset/assets/stylesheets/exploreV2/exploreV2.css
+++ b/superset/assets/stylesheets/exploreV2/exploreV2.css
@@ -15,3 +15,8 @@
margin-right: 0px;
margin-bottom: 100px;
}
+
+.fave-unfave-icon, .edit-desc-icon {
+ padding: 0 0 0 .5em;
+ font-size: 14px;
+}