diff --git a/superset/assets/.eslintrc b/superset/assets/.eslintrc
index e49a4e0f1bbb4..a79d50d4d9db7 100644
--- a/superset/assets/.eslintrc
+++ b/superset/assets/.eslintrc
@@ -8,6 +8,7 @@
},
"globals": {
"document": true,
+ "window": true,
},
"rules": {
"prefer-template": 0,
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 0620e0952e820..b830080b44d34 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -108,6 +108,7 @@
"react-split-pane": "^0.1.66",
"react-sticky": "^6.0.2",
"react-syntax-highlighter": "^7.0.4",
+ "react-tag-autocomplete": "^5.5.1",
"react-virtualized": "9.19.1",
"react-virtualized-select": "^2.4.0",
"reactable": "1.0.2",
@@ -121,7 +122,8 @@
"supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40",
"underscore": "^1.8.3",
"urijs": "^1.18.10",
- "viewport-mercator-project": "^5.0.0"
+ "viewport-mercator-project": "^5.0.0",
+ "whatwg-fetch": "^2.0.4"
},
"devDependencies": {
"babel-cli": "^6.14.0",
diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx
index 46c6fdb90600f..408458ac28000 100644
--- a/superset/assets/spec/javascripts/welcome/App_spec.jsx
+++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx
@@ -13,10 +13,10 @@ describe('App', () => {
React.isValidElement(),
).to.equal(true);
});
- it('renders 4 Tab, Panel, and Row components', () => {
+ it('renders Tab, Panel, and Row components', () => {
const wrapper = shallow();
- expect(wrapper.find(Tab)).to.have.length(3);
- expect(wrapper.find(Panel)).to.have.length(3);
- expect(wrapper.find(Row)).to.have.length(3);
+ expect(wrapper.find(Tab)).to.have.length(4);
+ expect(wrapper.find(Panel)).to.have.length(4);
+ expect(wrapper.find(Row)).to.have.length(5);
});
});
diff --git a/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx b/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
index 8a3387ad54635..843735ec124b3 100644
--- a/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
+++ b/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
@@ -64,8 +64,9 @@ export default class TemplateParamsEditor extends React.Component {
Assign a set of parameters as JSON
below
(example: {'{"my_table": "foo"}'}
),
and they become available
- in your SQL (example: SELECT * FROM {'{{ my_table }}'}
)
- by using
+ in your SQL (example: SELECT * FROM {'{{ my_table }}'}
)
+ by using
+ {' '}
{
- const redirectUrl = new URL(window.location);
- redirectUrl.pathname = '/superset/sqllab';
- for (const k of redirectUrl.searchParams.keys()) {
- redirectUrl.searchParams.delete(k);
- }
- redirectUrl.searchParams.set('datasourceKey', formData.datasource);
- redirectUrl.searchParams.set('sql', response.query);
- window.open(redirectUrl.href, '_blank');
+ const redirectUrl = new URI(window.location);
+ redirectUrl
+ .pathname('/superset/sqllab')
+ .search({ datasourceKey: formData.datasource, sql: response.query });
+ window.open(redirectUrl.href(), '_blank');
},
error: () => notify.error(t("The SQL couldn't be loaded")),
});
diff --git a/superset/assets/src/components/ObjectTags.css b/superset/assets/src/components/ObjectTags.css
new file mode 100644
index 0000000000000..4c4a8d5b57f42
--- /dev/null
+++ b/superset/assets/src/components/ObjectTags.css
@@ -0,0 +1,195 @@
+/**
+ *
+ *
+ *
+ *
+ *
+ */
+.react-tags {
+ position: relative;
+ display: inline-block;
+ padding: 1px 0 0 1px;
+ margin: 0 10px;
+ border: 1px solid #F5F5F5;
+ border-radius: 1px;
+
+ /* shared font styles */
+ font-size: 12px;
+ line-height: 1.2;
+
+ /* clicking anywhere will focus the input */
+ cursor: text;
+}
+
+.react-tags-rw {
+ display: inline-block;
+ margin: 0 10px;
+ font-size: 12px;
+ line-height: 1.2;
+}
+
+.react-tags.is-focused {
+ border-color: #F0F0F0;
+}
+
+.react-tags__selected {
+ display: inline;
+}
+
+.react-tags__selected-tag {
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 6px 8px;
+ border: 1px solid #F5F5F5;
+ border-radius: 2px;
+ background: #F1F1F1;
+
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.react-tags__selected-tag:after {
+ content: '\2715';
+ color: #AAA;
+ margin-left: 8px;
+}
+
+.react-tags__selected-tag:hover,
+.react-tags__selected-tag:focus {
+ border-color: #B1B1B1;
+}
+
+.react-tags__search {
+ display: inline-block;
+
+ /* match tag layout */
+ padding: 7px 2px;
+ margin-bottom: 0;
+
+ /* prevent autoresize overflowing the container */
+ max-width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+
+ .react-tags__search {
+ /* this will become the offsetParent for suggestions */
+ position: relative;
+ }
+
+}
+
+.react-tags__search input {
+ /* prevent autoresize overflowing the container */
+ max-width: 100%;
+
+ /* remove styles and layout from this element */
+ margin: 0;
+ margin-left: 2px;
+ padding: 0;
+ border: 0;
+ outline: none;
+
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.react-tags__search input::-ms-clear {
+ display: none;
+}
+
+.react-tags__suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+ z-index: 9999;
+}
+
+@media screen and (min-width: 30em) {
+
+ .react-tags__suggestions {
+ width: 240px;
+ }
+
+}
+
+.react-tags__suggestions ul {
+ margin: 4px -1px;
+ padding: 0;
+ list-style: none;
+ background: white;
+ border: 1px solid #D1D1D1;
+ border-radius: 2px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+}
+
+.react-tags__suggestions li {
+ border-bottom: 1px solid #ddd;
+ padding: 6px 8px;
+}
+
+.react-tags__suggestions li mark {
+ text-decoration: underline;
+ background: none;
+ font-weight: 600;
+}
+
+.react-tags__suggestions li:hover {
+ cursor: pointer;
+ background: #eee;
+}
+
+.react-tags__suggestions li.is-active {
+ background: #b7cfe0;
+}
+
+.react-tags__suggestions li.is-disabled {
+ opacity: 0.5;
+ cursor: auto;
+}
+
+.react-tags span.label {
+ margin-left: 5px;
+ padding: 0.3em 0.6em 0.3em;
+}
+
+.react-tags a.deco-none {
+ color: inherit;
+ text-decoration:none;
+}
+
+.react-tags-rw span.label {
+ margin-left: 5px;
+}
+
+.react-tags-rw a.deco-none {
+ color: inherit;
+ text-decoration:none;
+}
+
+.react-tags span.glyphicon {
+ cursor: pointer;
+ margin-left: 3px;
+ top: 2px;
+}
diff --git a/superset/assets/src/components/ObjectTags.jsx b/superset/assets/src/components/ObjectTags.jsx
new file mode 100644
index 0000000000000..ab8114f34bcfc
--- /dev/null
+++ b/superset/assets/src/components/ObjectTags.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactTags from 'react-tag-autocomplete';
+import { Glyphicon, Label } from 'react-bootstrap';
+import TooltipWrapper from './TooltipWrapper';
+
+import './ObjectTags.css';
+
+import { t } from '../locales';
+
+const propTypes = {
+ fetchTags: PropTypes.func,
+ fetchSuggestions: PropTypes.func,
+ deleteTag: PropTypes.func,
+ addTag: PropTypes.func,
+ editable: PropTypes.bool,
+ onChange: PropTypes.func,
+};
+
+const defaultProps = {
+ fetchTags: (callback) => { callback([]); },
+ fetchSuggestions: (callback) => { callback([]); },
+ deleteTag: (tag, callback) => { callback(); },
+ addTag: (tag, callback) => { callback(); },
+ editable: true,
+ onChange: () => {},
+};
+
+export default class ObjectTags extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ tags: [],
+ suggestions: [],
+ };
+
+ this.handleDelete = this.handleDelete.bind(this);
+ this.handleAddition = this.handleAddition.bind(this);
+ }
+
+ componentDidMount() {
+ this.props.fetchTags(tags => this.setState({ tags }));
+ this.props.fetchSuggestions(suggestions => this.setState({ suggestions }));
+ }
+
+ handleDelete(i) {
+ const tags = this.state.tags.slice(0);
+ const tag = tags.splice(i, 1)[0].name;
+ this.props.deleteTag(tag, () => this.setState({ tags }));
+ this.props.onChange(tags);
+ }
+
+ handleAddition(tag) {
+ const tags = [...this.state.tags, tag];
+ this.props.addTag(tag.name, () => this.setState({ tags }));
+ this.props.onChange(tags);
+ }
+
+ renderEditableTags() {
+ const Tag = props => (
+
+ );
+ const {
+ fetchTags,
+ fetchSuggestions,
+ deleteTag,
+ addTag,
+ editable,
+ onChange,
+ ...rest
+ } = this.props;
+ return (
+
+ );
+ }
+
+ renderReadOnlyTags() {
+ return (
+
+ );
+ }
+
+ render() {
+ if (this.props.editable) {
+ return this.renderEditableTags();
+ }
+ return this.renderReadOnlyTags();
+ }
+}
+
+ObjectTags.propTypes = propTypes;
+ObjectTags.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index a21ec11de7ba3..723c075bb91eb 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -10,6 +10,7 @@ import NewDivider from './gridComponents/new/NewDivider';
import NewHeader from './gridComponents/new/NewHeader';
import NewRow from './gridComponents/new/NewRow';
import NewTabs from './gridComponents/new/NewTabs';
+import NewTags from './gridComponents/new/NewTags';
import NewMarkdown from './gridComponents/new/NewMarkdown';
import SliceAdder from '../containers/SliceAdder';
import { t } from '../../locales';
@@ -93,6 +94,7 @@ class BuilderComponentPane extends React.PureComponent {
+
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tags.jsx b/superset/assets/src/dashboard/components/gridComponents/Tags.jsx
new file mode 100644
index 0000000000000..6763279c5e78f
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/Tags.jsx
@@ -0,0 +1,309 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { ListGroup, ListGroupItem, Panel } from 'react-bootstrap';
+import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import moment from 'moment';
+import { unsafe } from 'reactable';
+import 'whatwg-fetch';
+
+import DeleteComponentButton from '../DeleteComponentButton';
+import DragDroppable from '../dnd/DragDroppable';
+import HoverMenu from '../menu/HoverMenu';
+import IconButton from '../IconButton';
+import ResizableContainer from '../resizable/ResizableContainer';
+import SelectControl from '../../../explore/components/controls/SelectControl';
+import WithPopoverMenu from '../menu/WithPopoverMenu';
+import { componentShape } from '../../util/propShapes';
+import { fetchObjects, fetchSuggestions } from '../../../tags';
+import { ROW_TYPE, COLUMN_TYPE } from '../../util/componentTypes';
+import {
+ GRID_MIN_COLUMN_COUNT,
+ GRID_MIN_ROW_UNITS,
+ GRID_BASE_UNIT,
+ STANDARD_TAGS,
+ TAGGED_CONTENT_TYPES,
+} from '../../util/constants';
+
+const HEADER_HEIGHT = 48;
+
+const propTypes = {
+ id: PropTypes.string.isRequired,
+ parentId: PropTypes.string.isRequired,
+ component: componentShape.isRequired,
+ parentComponent: componentShape.isRequired,
+ index: PropTypes.number.isRequired,
+ depth: PropTypes.number.isRequired,
+ editMode: PropTypes.bool.isRequired,
+
+ // grid related
+ availableColumnCount: PropTypes.number.isRequired,
+ columnWidth: PropTypes.number.isRequired,
+ onResizeStart: PropTypes.func.isRequired,
+ onResize: PropTypes.func.isRequired,
+ onResizeStop: PropTypes.func.isRequired,
+
+ // dnd
+ deleteComponent: PropTypes.func.isRequired,
+ handleComponentDrop: PropTypes.func.isRequired,
+ updateComponents: PropTypes.func.isRequired,
+};
+
+const defaultProps = {};
+
+function linkFormatter(cell, row) {
+ const url = `${cell}`;
+ return (
+
+ {row.name}
+
+ );
+}
+
+function changedOnFormatter(cell) {
+ const date = new Date(cell);
+ return unsafe(moment.utc(date).fromNow());
+}
+
+class Tags extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isFocused: false,
+ isConfiguring: false,
+ data: [],
+ tagSuggestions: STANDARD_TAGS,
+ };
+
+ this.handleChangeFocus = this.handleChangeFocus.bind(this);
+ this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
+ this.toggleConfiguring = this.toggleConfiguring.bind(this);
+ this.handleUpdateMeta = this.handleUpdateMeta.bind(this);
+ this.handleChangeTags = this.handleUpdateMeta.bind(this, 'tags');
+ this.handleChangeTypes = this.handleUpdateMeta.bind(this, 'types');
+
+ this.fetchResults = this.fetchResults.bind(this);
+ this.fetchTagSuggestions = this.fetchTagSuggestions.bind(this);
+ }
+
+ componentDidMount() {
+ this.fetchResults(this.props.component);
+ this.fetchTagSuggestions();
+ }
+
+ handleChangeFocus(nextFocus) {
+ this.setState(() => ({ isFocused: nextFocus }));
+ }
+
+ handleUpdateMeta(metaKey, nextValue) {
+ const { updateComponents, component } = this.props;
+ if (nextValue && component.meta[metaKey] !== nextValue) {
+ const nextComponent = {
+ ...component,
+ meta: {
+ ...component.meta,
+ [metaKey]: nextValue,
+ },
+ };
+ updateComponents({ [component.id]: nextComponent });
+ this.fetchResults(nextComponent);
+ }
+ }
+
+ fetchResults(component) {
+ const tags = component.meta.tags || [];
+ const types = component.meta.types || TAGGED_CONTENT_TYPES;
+ fetchObjects({ tags: tags.join(','), types: types.join(',') }, data =>
+ this.setState({ data }),
+ );
+ }
+
+ fetchTagSuggestions() {
+ fetchSuggestions({ includeTypes: false }, suggestions => {
+ const tagSuggestions = STANDARD_TAGS.concat(
+ suggestions.map(tag => tag.name),
+ );
+ this.setState({ tagSuggestions });
+ });
+ }
+
+ handleDeleteComponent() {
+ const { deleteComponent, id, parentId } = this.props;
+ deleteComponent(id, parentId);
+ }
+
+ toggleConfiguring() {
+ this.setState({ isConfiguring: !this.state.isConfiguring });
+ }
+
+ renderEditMode() {
+ const { component } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ renderPreviewMode() {
+ const component = this.props.component;
+ const height = component.meta.height * GRID_BASE_UNIT - HEADER_HEIGHT;
+ return (
+
+
+ ID
+
+
+ Name
+
+
+ Type
+
+
+ Creator
+
+
+ Changed on
+
+
+ );
+ }
+
+ render() {
+ const { isFocused, isConfiguring } = this.state;
+
+ const {
+ component,
+ parentComponent,
+ index,
+ depth,
+ availableColumnCount,
+ columnWidth,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ handleComponentDrop,
+ editMode,
+ } = this.props;
+
+ // inherit the size of parent columns
+ const widthMultiple =
+ parentComponent.type === COLUMN_TYPE
+ ? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
+ : component.meta.width || GRID_MIN_COLUMN_COUNT;
+
+ const buttonClass = isConfiguring ? 'fa fa-table' : 'fa fa-cog';
+
+ return (
+
+ {({ dropIndicatorProps, dragSourceRef }) => (
+
+
+
+
+ {isConfiguring
+ ? this.renderEditMode()
+ : this.renderPreviewMode()}
+ {editMode && (
+
+
+
+
+ )}
+
+
+
+ {dropIndicatorProps && }
+
+ )}
+
+ );
+ }
+}
+
+Tags.propTypes = propTypes;
+Tags.defaultProps = defaultProps;
+
+export default Tags;
diff --git a/superset/assets/src/dashboard/components/gridComponents/index.js b/superset/assets/src/dashboard/components/gridComponents/index.js
index c56bed01cdc4e..d775268f856fc 100644
--- a/superset/assets/src/dashboard/components/gridComponents/index.js
+++ b/superset/assets/src/dashboard/components/gridComponents/index.js
@@ -7,6 +7,7 @@ import {
ROW_TYPE,
TAB_TYPE,
TABS_TYPE,
+ TAGS_TYPE,
} from '../../util/componentTypes';
import ChartHolder from './ChartHolder';
@@ -17,6 +18,7 @@ import Header from './Header';
import Row from './Row';
import Tab from './Tab';
import Tabs from './Tabs';
+import Tags from './Tags';
export { default as ChartHolder } from './ChartHolder';
export { default as Markdown } from './Markdown';
@@ -26,6 +28,7 @@ export { default as Header } from './Header';
export { default as Row } from './Row';
export { default as Tab } from './Tab';
export { default as Tabs } from './Tabs';
+export { default as Tags } from './Tags';
export default {
[CHART_TYPE]: ChartHolder,
@@ -36,4 +39,5 @@ export default {
[ROW_TYPE]: Row,
[TAB_TYPE]: Tab,
[TABS_TYPE]: Tabs,
+ [TAGS_TYPE]: Tags,
};
diff --git a/superset/assets/src/dashboard/components/gridComponents/new/NewTags.jsx b/superset/assets/src/dashboard/components/gridComponents/new/NewTags.jsx
new file mode 100644
index 0000000000000..7ab41a7199ea2
--- /dev/null
+++ b/superset/assets/src/dashboard/components/gridComponents/new/NewTags.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { TAGS_TYPE } from '../../../util/componentTypes';
+import { NEW_TAGS_ID } from '../../../util/constants';
+import DraggableNewComponent from './DraggableNewComponent';
+
+export default function DraggableNewTags() {
+ return (
+
+ );
+}
diff --git a/superset/assets/src/dashboard/util/componentIsResizable.js b/superset/assets/src/dashboard/util/componentIsResizable.js
index 45812d762b58e..2d07902d61a25 100644
--- a/superset/assets/src/dashboard/util/componentIsResizable.js
+++ b/superset/assets/src/dashboard/util/componentIsResizable.js
@@ -1,5 +1,13 @@
-import { COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE } from './componentTypes';
+import {
+ COLUMN_TYPE,
+ CHART_TYPE,
+ MARKDOWN_TYPE,
+ TAGS_TYPE,
+} from './componentTypes';
export default function componentIsResizable(entity) {
- return [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE].indexOf(entity.type) > -1;
+ return (
+ [COLUMN_TYPE, CHART_TYPE, MARKDOWN_TYPE, TAGS_TYPE].indexOf(entity.type) >
+ -1
+ );
}
diff --git a/superset/assets/src/dashboard/util/componentTypes.js b/superset/assets/src/dashboard/util/componentTypes.js
index 47478e6119d8b..25f363b7511ea 100644
--- a/superset/assets/src/dashboard/util/componentTypes.js
+++ b/superset/assets/src/dashboard/util/componentTypes.js
@@ -10,6 +10,7 @@ export const NEW_COMPONENT_SOURCE_TYPE = 'NEW_COMPONENT_SOURCE';
export const ROW_TYPE = 'ROW';
export const TABS_TYPE = 'TABS';
export const TAB_TYPE = 'TAB';
+export const TAGS_TYPE = 'TAGS';
export default {
CHART_TYPE,
@@ -24,4 +25,5 @@ export default {
ROW_TYPE,
TABS_TYPE,
TAB_TYPE,
+ TAGS_TYPE,
};
diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js
index b26cbff85fdbb..c8ca8ffaafea0 100644
--- a/superset/assets/src/dashboard/util/constants.js
+++ b/superset/assets/src/dashboard/util/constants.js
@@ -13,6 +13,7 @@ export const NEW_MARKDOWN_ID = 'NEW_MARKDOWN_ID';
export const NEW_ROW_ID = 'NEW_ROW_ID';
export const NEW_TAB_ID = 'NEW_TAB_ID';
export const NEW_TABS_ID = 'NEW_TABS_ID';
+export const NEW_TAGS_ID = 'NEW_TAGS_ID';
// grid constants
export const DASHBOARD_ROOT_DEPTH = 0;
@@ -41,6 +42,13 @@ export const UNDO_LIMIT = 50;
export const SAVE_TYPE_OVERWRITE = 'overwrite';
export const SAVE_TYPE_NEWDASHBOARD = 'newDashboard';
+// objects that can be tagged
+export const TAGGED_CONTENT_TYPES = ['dashboard', 'chart', 'query'];
+export const STANDARD_TAGS = [
+ ['owner:{{ current_user_id() }}', 'Owned by me'],
+ ['favorited_by:{{ current_user_id() }}', 'Favorited by me'],
+];
+
// default dashboard layout data size limit
// could be overwritten by server-side config
export const DASHBOARD_POSITION_DATA_LIMIT = 65535;
diff --git a/superset/assets/src/dashboard/util/getDetailedComponentWidth.js b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
index ee3096d6710e7..88337560aa962 100644
--- a/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
+++ b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
@@ -5,6 +5,7 @@ import {
COLUMN_TYPE,
MARKDOWN_TYPE,
CHART_TYPE,
+ TAGS_TYPE,
} from './componentTypes';
function getTotalChildWidth({ id, components }) {
@@ -67,7 +68,8 @@ export default function getDetailedComponentWidth({
});
} else if (
component.type === MARKDOWN_TYPE ||
- component.type === CHART_TYPE
+ component.type === CHART_TYPE ||
+ component.type === TAGS_TYPE
) {
result.minimumWidth = GRID_MIN_COLUMN_COUNT;
}
diff --git a/superset/assets/src/dashboard/util/isValidChild.js b/superset/assets/src/dashboard/util/isValidChild.js
index c975496baa40c..b11e0dc67b45a 100644
--- a/superset/assets/src/dashboard/util/isValidChild.js
+++ b/superset/assets/src/dashboard/util/isValidChild.js
@@ -25,6 +25,7 @@ import {
ROW_TYPE,
TABS_TYPE,
TAB_TYPE,
+ TAGS_TYPE,
} from './componentTypes';
import { DASHBOARD_ROOT_DEPTH as rootDepth } from './constants';
@@ -50,12 +51,14 @@ const parentMaxDepthLookup = {
[HEADER_TYPE]: depthOne,
[ROW_TYPE]: depthOne,
[TABS_TYPE]: depthOne,
+ [TAGS_TYPE]: depthOne,
},
[ROW_TYPE]: {
[CHART_TYPE]: depthFour,
[MARKDOWN_TYPE]: depthFour,
[COLUMN_TYPE]: depthFour,
+ [TAGS_TYPE]: depthFour,
},
[TABS_TYPE]: {
@@ -70,6 +73,7 @@ const parentMaxDepthLookup = {
[HEADER_TYPE]: depthTwo,
[ROW_TYPE]: depthTwo,
[TABS_TYPE]: depthTwo,
+ [TAGS_TYPE]: depthTwo,
},
[COLUMN_TYPE]: {
@@ -78,6 +82,7 @@ const parentMaxDepthLookup = {
[MARKDOWN_TYPE]: depthFive,
[ROW_TYPE]: depthThree,
[DIVIDER_TYPE]: depthThree,
+ [TAGS_TYPE]: depthFive,
},
// these have no valid children
@@ -85,6 +90,7 @@ const parentMaxDepthLookup = {
[DIVIDER_TYPE]: {},
[HEADER_TYPE]: {},
[MARKDOWN_TYPE]: {},
+ [TAGS_TYPE]: {},
};
export default function isValidChild({ parentType, childType, parentDepth }) {
diff --git a/superset/assets/src/dashboard/util/newComponentFactory.js b/superset/assets/src/dashboard/util/newComponentFactory.js
index 18b433b0135eb..620235072c405 100644
--- a/superset/assets/src/dashboard/util/newComponentFactory.js
+++ b/superset/assets/src/dashboard/util/newComponentFactory.js
@@ -9,6 +9,7 @@ import {
ROW_TYPE,
TABS_TYPE,
TAB_TYPE,
+ TAGS_TYPE,
} from './componentTypes';
import {
@@ -33,6 +34,7 @@ const typeToDefaultMetaData = {
[ROW_TYPE]: { background: BACKGROUND_TRANSPARENT },
[TABS_TYPE]: null,
[TAB_TYPE]: { text: 'New Tab' },
+ [TAGS_TYPE]: { width: 3, height: 30 },
};
function uuid(type) {
diff --git a/superset/assets/src/dashboard/util/shouldWrapChildInRow.js b/superset/assets/src/dashboard/util/shouldWrapChildInRow.js
index e7e648cf1bf64..56a317b814bb7 100644
--- a/superset/assets/src/dashboard/util/shouldWrapChildInRow.js
+++ b/superset/assets/src/dashboard/util/shouldWrapChildInRow.js
@@ -4,6 +4,7 @@ import {
COLUMN_TYPE,
MARKDOWN_TYPE,
TAB_TYPE,
+ TAGS_TYPE,
} from './componentTypes';
const typeToWrapChildLookup = {
@@ -11,12 +12,14 @@ const typeToWrapChildLookup = {
[CHART_TYPE]: true,
[COLUMN_TYPE]: true,
[MARKDOWN_TYPE]: true,
+ [TAGS_TYPE]: true,
},
[TAB_TYPE]: {
[CHART_TYPE]: true,
[COLUMN_TYPE]: true,
[MARKDOWN_TYPE]: true,
+ [TAGS_TYPE]: true,
},
};
diff --git a/superset/assets/src/explore/components/ExploreChartHeader.jsx b/superset/assets/src/explore/components/ExploreChartHeader.jsx
index 8c9ea91f2a590..063444b859ff0 100644
--- a/superset/assets/src/explore/components/ExploreChartHeader.jsx
+++ b/superset/assets/src/explore/components/ExploreChartHeader.jsx
@@ -10,6 +10,13 @@ import FaveStar from '../../components/FaveStar';
import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
import CachedLabel from '../../components/CachedLabel';
+import ObjectTags from '../../components/ObjectTags';
+import {
+ addTag,
+ deleteTag,
+ fetchSuggestions,
+ fetchTags,
+} from '../../tags';
import { t } from '../../locales';
const CHART_STATUS_MAP = {
@@ -32,9 +39,35 @@ const propTypes = {
};
class ExploreChartHeader extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.fetchTags = fetchTags.bind(this, {
+ objectType: 'chart',
+ objectId: props.chart.id,
+ includeTypes: false,
+ });
+ this.fetchSuggestions = fetchSuggestions.bind(this, {
+ includeTypes: false,
+ });
+ this.deleteTag = deleteTag.bind(this, {
+ objectType: 'chart',
+ objectId: props.chart.id,
+ });
+ this.addTag = addTag.bind(this, {
+ objectType: 'chart',
+ objectId: props.chart.id,
+ includeTypes: false,
+ });
+ }
+
runQuery() {
- this.props.actions.runQuery(this.props.form_data, true,
- this.props.timeout, this.props.chart.id);
+ this.props.actions.runQuery(
+ this.props.form_data,
+ true,
+ this.props.timeout,
+ this.props.chart.id,
+ );
}
updateChartTitleOrSaveSlice(newTitle) {
@@ -43,17 +76,23 @@ class ExploreChartHeader extends React.PureComponent {
slice_name: newTitle,
action: isNewSlice ? 'saveas' : 'overwrite',
};
- this.props.actions.saveSlice(this.props.form_data, params)
- .then((data) => {
- if (isNewSlice) {
- this.props.actions.createNewSlice(
- data.can_add, data.can_download, data.can_overwrite,
- data.slice, data.form_data);
- this.props.addHistory({ isReplace: true, title: `[chart] ${data.slice.slice_name}` });
- } else {
- this.props.actions.updateChartTitle(newTitle);
- }
- });
+ this.props.actions.saveSlice(this.props.form_data, params).then((data) => {
+ if (isNewSlice) {
+ this.props.actions.createNewSlice(
+ data.can_add,
+ data.can_download,
+ data.can_overwrite,
+ data.slice,
+ data.form_data,
+ );
+ this.props.addHistory({
+ isReplace: true,
+ title: `[chart] ${data.slice.slice_name}`,
+ });
+ } else {
+ this.props.actions.updateChartTitle(newTitle);
+ }
+ });
}
renderChartTitle() {
@@ -73,58 +112,69 @@ class ExploreChartHeader extends React.PureComponent {
chartUpdateEndTime,
chartUpdateStartTime,
latestQueryFormData,
- queryResponse } = this.props.chart;
- const chartSucceeded = ['success', 'rendered'].indexOf(this.props.chart.chartStatus) > 0;
+ queryResponse,
+ } = this.props.chart;
+ const chartSucceeded =
+ ['success', 'rendered'].indexOf(this.props.chart.chartStatus) > 0;
return (
-