diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index 524d5a6d1c14f..c869874be86b2 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -61,6 +61,7 @@ export enum FeatureFlag {
SQL_VALIDATORS_BY_ENGINE = 'SQL_VALIDATORS_BY_ENGINE',
THUMBNAILS = 'THUMBNAILS',
USE_ANALAGOUS_COLORS = 'USE_ANALAGOUS_COLORS',
+ TAGGING_SYSTEM = 'TAGGING_SYSTEM',
UX_BETA = 'UX_BETA',
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
SSH_TUNNELING = 'SSH_TUNNELING',
diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts
index 641cb05801515..0c1314f2637c6 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -118,4 +118,7 @@ export enum FilterOperator {
datasetIsCertified = 'dataset_is_certified',
dashboardHasCreatedBy = 'dashboard_has_created_by',
chartHasCreatedBy = 'chart_has_created_by',
+ dashboardTags = 'dashboard_tags',
+ chartTags = 'chart_tags',
+ savedQueryTags = 'saved_query_tags',
}
diff --git a/superset-frontend/src/components/Tags/Tag.test.tsx b/superset-frontend/src/components/Tags/Tag.test.tsx
new file mode 100644
index 0000000000000..0ff7b2e85a3f3
--- /dev/null
+++ b/superset-frontend/src/components/Tags/Tag.test.tsx
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { render } from 'spec/helpers/testing-library';
+import TagType from 'src/types/TagType';
+import Tag from './Tag';
+
+const mockedProps: TagType = {
+ name: 'example-tag',
+ id: 1,
+ onDelete: undefined,
+ editable: false,
+ onClick: undefined,
+};
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
new file mode 100644
index 0000000000000..ecd2cb135a0b6
--- /dev/null
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { styled } from '@superset-ui/core';
+import TagType from 'src/types/TagType';
+import AntdTag from 'antd/lib/tag';
+import React, { useMemo } from 'react';
+import { Tooltip } from 'src/components/Tooltip';
+
+const StyledTag = styled(AntdTag)`
+ ${({ theme }) => `
+ margin-top: ${theme.gridUnit}px;
+ margin-bottom: ${theme.gridUnit}px;
+ font-size: ${theme.typography.sizes.s}px;
+ `};
+`;
+
+const Tag = ({
+ name,
+ id,
+ index = undefined,
+ onDelete = undefined,
+ editable = false,
+ onClick = undefined,
+}: TagType) => {
+ const isLongTag = useMemo(() => name.length > 20, [name]);
+
+ const handleClose = () => (index ? onDelete?.(index) : null);
+
+ const tagElem = (
+ <>
+ {editable ? (
+
+ {isLongTag ? `${name.slice(0, 20)}...` : name}
+
+ ) : (
+
+ {id ? (
+
+ {isLongTag ? `${name.slice(0, 20)}...` : name}
+
+ ) : isLongTag ? (
+ `${name.slice(0, 20)}...`
+ ) : (
+ name
+ )}
+
+ )}
+ >
+ );
+
+ return isLongTag ? (
+
+ {tagElem}
+
+ ) : (
+ tagElem
+ );
+};
+
+export default Tag;
diff --git a/superset-frontend/src/components/Tags/TagsList.stories.tsx b/superset-frontend/src/components/Tags/TagsList.stories.tsx
new file mode 100644
index 0000000000000..0bfe27b42a17a
--- /dev/null
+++ b/superset-frontend/src/components/Tags/TagsList.stories.tsx
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import TagType from 'src/types/TagType';
+import { TagsList } from '.';
+import { TagsListProps } from './TagsList';
+
+export default {
+ title: 'Tags',
+ component: TagsList,
+};
+
+export const InteractiveTags = ({ tags, editable, maxTags }: TagsListProps) => (
+
+);
+
+const tags: TagType[] = [
+ { name: 'tag1' },
+ { name: 'tag2' },
+ { name: 'tag3' },
+ { name: 'tag4' },
+ { name: 'tag5' },
+ { name: 'tag6' },
+];
+
+const editable = true;
+
+const maxTags = 3;
+
+InteractiveTags.args = {
+ tags,
+ editable,
+ maxTags,
+};
+
+InteractiveTags.story = {
+ parameters: {
+ knobs: {
+ disable: true,
+ },
+ },
+};
diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx b/superset-frontend/src/components/Tags/TagsList.test.tsx
new file mode 100644
index 0000000000000..f67dbce294663
--- /dev/null
+++ b/superset-frontend/src/components/Tags/TagsList.test.tsx
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { render, waitFor } from 'spec/helpers/testing-library';
+import TagsList, { TagsListProps } from './TagsList';
+
+const testTags = [
+ {
+ name: 'example-tag1',
+ id: 1,
+ },
+ {
+ name: 'example-tag2',
+ id: 2,
+ },
+ {
+ name: 'example-tag3',
+ id: 3,
+ },
+ {
+ name: 'example-tag4',
+ id: 4,
+ },
+ {
+ name: 'example-tag5',
+ id: 5,
+ },
+];
+
+const mockedProps: TagsListProps = {
+ tags: testTags,
+ onDelete: undefined,
+ maxTags: 5,
+};
+
+const getElementsByClassName = (className: string) =>
+ document.querySelectorAll(className)! as NodeListOf;
+
+const findAllTags = () => waitFor(() => getElementsByClassName('.ant-tag'));
+
+test('should render', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+});
+
+test('should render 5 elements', async () => {
+ render();
+ const tagsListItems = await findAllTags();
+ expect(tagsListItems).toHaveLength(5);
+ expect(tagsListItems[0]).toHaveTextContent(testTags[0].name);
+ expect(tagsListItems[1]).toHaveTextContent(testTags[1].name);
+ expect(tagsListItems[2]).toHaveTextContent(testTags[2].name);
+ expect(tagsListItems[3]).toHaveTextContent(testTags[3].name);
+ expect(tagsListItems[4]).toHaveTextContent(testTags[4].name);
+});
+
+test('should render 3 elements when maxTags is set to 3', async () => {
+ render();
+ const tagsListItems = await findAllTags();
+ expect(tagsListItems).toHaveLength(3);
+ expect(tagsListItems[2]).toHaveTextContent('+3...');
+});
diff --git a/superset-frontend/src/components/Tags/TagsList.tsx b/superset-frontend/src/components/Tags/TagsList.tsx
new file mode 100644
index 0000000000000..102e6d2ced31f
--- /dev/null
+++ b/superset-frontend/src/components/Tags/TagsList.tsx
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useMemo, useState } from 'react';
+import { styled } from '@superset-ui/core';
+import TagType from 'src/types/TagType';
+import Tag from './Tag';
+
+export type TagsListProps = {
+ tags: TagType[];
+ editable?: boolean;
+ /**
+ * OnDelete:
+ * Only applies when editable is true
+ * Callback for when a tag is deleted
+ */
+ onDelete?: ((index: number) => void) | undefined;
+ maxTags?: number | undefined;
+};
+
+const TagsDiv = styled.div`
+ max-width: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+`;
+
+const TagsList = ({
+ tags,
+ editable = false,
+ onDelete,
+ maxTags,
+}: TagsListProps) => {
+ const [tempMaxTags, setTempMaxTags] = useState(maxTags);
+
+ const handleDelete = (index: number) => {
+ onDelete?.(index);
+ };
+
+ const expand = () => setTempMaxTags(undefined);
+
+ const collapse = () => setTempMaxTags(maxTags);
+
+ const tagsIsLong: boolean | null = useMemo(
+ () => (tempMaxTags ? tags.length > tempMaxTags : null),
+ [tags.length, tempMaxTags],
+ );
+
+ const extraTags: number | null = useMemo(
+ () =>
+ typeof tempMaxTags === 'number' ? tags.length - tempMaxTags + 1 : null,
+ [tagsIsLong, tags.length, tempMaxTags],
+ );
+
+ return (
+
+ {tagsIsLong && typeof tempMaxTags === 'number' ? (
+ <>
+ {tags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
+
+ ))}
+ {tags.length > tempMaxTags ? (
+
+ ) : null}
+ >
+ ) : (
+ <>
+ {tags.map((tag: TagType, index) => (
+
+ ))}
+ {maxTags ? (
+ tags.length > maxTags ? (
+
+ ) : null
+ ) : null}
+ >
+ )}
+
+ );
+};
+
+export default TagsList;
diff --git a/superset-frontend/src/components/Tags/index.tsx b/superset-frontend/src/components/Tags/index.tsx
new file mode 100644
index 0000000000000..d9178e7a26f23
--- /dev/null
+++ b/superset-frontend/src/components/Tags/index.tsx
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { default as TagsList } from './TagsList';
+export { default as Tag } from './Tag';
diff --git a/superset-frontend/src/components/Tags/utils.tsx b/superset-frontend/src/components/Tags/utils.tsx
new file mode 100644
index 0000000000000..690a9b44066d0
--- /dev/null
+++ b/superset-frontend/src/components/Tags/utils.tsx
@@ -0,0 +1,93 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SupersetClient, t } from '@superset-ui/core';
+import Tag from 'src/types/TagType';
+
+import rison from 'rison';
+import { cacheWrapper } from 'src/utils/cacheWrapper';
+import {
+ ClientErrorObject,
+ getClientErrorObject,
+} from 'src/utils/getClientErrorObject';
+
+const localCache = new Map();
+
+const cachedSupersetGet = cacheWrapper(
+ SupersetClient.get,
+ localCache,
+ ({ endpoint }) => endpoint || '',
+);
+
+type SelectTagsValue = {
+ value: string | number | undefined;
+ label: string;
+ key: string | number | undefined;
+};
+
+export const tagToSelectOption = (
+ item: Tag & { table_name: string },
+): SelectTagsValue => ({
+ value: item.name,
+ label: item.name,
+ key: item.name,
+});
+
+export const loadTags = async (
+ search: string,
+ page: number,
+ pageSize: number,
+) => {
+ const searchColumn = 'name';
+ const query = rison.encode({
+ filters: [{ col: searchColumn, opr: 'ct', value: search }],
+ page,
+ page_size: pageSize,
+ order_column: searchColumn,
+ order_direction: 'asc',
+ });
+
+ const getErrorMessage = ({ error, message }: ClientErrorObject) => {
+ let errorText = message || error || t('An error has occurred');
+ if (message === 'Forbidden') {
+ errorText = t('You do not have permission to edit this dashboard');
+ }
+ return errorText;
+ };
+
+ return cachedSupersetGet({
+ endpoint: `/api/v1/tag/?q=${query}`,
+ })
+ .then(response => {
+ const data: {
+ label: string;
+ value: string | number;
+ }[] = response.json.result
+ .filter((item: Tag & { table_name: string }) => item.type === 1)
+ .map(tagToSelectOption);
+ return {
+ data,
+ totalCount: response.json.count,
+ };
+ })
+ .catch(async error => {
+ const errorMessage = getErrorMessage(await getClientErrorObject(error));
+ throw new Error(errorMessage);
+ });
+};
diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx
index 770cc67d70e37..1c87abe7868b1 100644
--- a/superset-frontend/src/dashboard/components/Header/index.jsx
+++ b/superset-frontend/src/dashboard/components/Header/index.jsx
@@ -529,15 +529,18 @@ class Header extends React.PureComponent {
isStarred: this.props.isStarred,
showTooltip: true,
}}
- titlePanelAdditionalItems={
-
- }
+ titlePanelAdditionalItems={[
+ !editMode && (
+
+ ),
+ ]}
rightPanelAdditionalItems={