diff --git a/superset-frontend/src/pages/Tags/index.tsx b/superset-frontend/src/pages/Tags/index.tsx index b0c998c3f32f8..30bdc993431ba 100644 --- a/superset-frontend/src/pages/Tags/index.tsx +++ b/superset-frontend/src/pages/Tags/index.tsx @@ -59,6 +59,17 @@ function TagList(props: TagListProps) { const { addDangerToast, addSuccessToast, user } = props; const { userId } = user; + const initialFilters = useMemo( + () => [ + { + id: 'type', + operator: 'custom_tag', + value: true, + }, + ], + [], + ); + const { state: { loading, @@ -70,7 +81,14 @@ function TagList(props: TagListProps) { fetchData, toggleBulkSelect, refreshData, - } = useListViewResource('tag', t('tag'), addDangerToast); + } = useListViewResource( + 'tag', + t('tag'), + addDangerToast, + undefined, + undefined, + initialFilters, + ); const [showTagModal, setShowTagModal] = useState(false); const [tagToEdit, setTagToEdit] = useState(null); diff --git a/superset/tags/api.py b/superset/tags/api.py index c0df921e3ebf1..ad25ffe7c936d 100644 --- a/superset/tags/api.py +++ b/superset/tags/api.py @@ -40,6 +40,7 @@ from superset.daos.tag import TagDAO from superset.exceptions import MissingUserContextException from superset.extensions import event_logger +from superset.tags.filters import UserCreatedTagTypeFilter from superset.tags.models import ObjectType, Tag from superset.tags.schemas import ( delete_tags_schema, @@ -119,6 +120,8 @@ class TagRestApi(BaseSupersetModelRestApi): } allowed_rel_fields = {"created_by", "changed_by"} + search_filters = {"type": [UserCreatedTagTypeFilter]} + add_model_schema = TagPostSchema() edit_model_schema = TagPutSchema() tag_get_response_schema = TagGetResponseSchema() diff --git a/superset/tags/filters.py b/superset/tags/filters.py new file mode 100644 index 0000000000000..ff6be712d3368 --- /dev/null +++ b/superset/tags/filters.py @@ -0,0 +1,39 @@ +# 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. +from flask_babel import lazy_gettext as _ +from sqlalchemy.orm import Query + +from superset.tags.models import Tag, TagType +from superset.views.base import BaseFilter + + +class UserCreatedTagTypeFilter(BaseFilter): # pylint: disable=too-few-public-methods + """ + Filter for tag type. + When set to True, only user-created tags are returned. + When set to False, only system tags are returned. + """ + + name = _("Is custom tag") + arg_name = "custom_tag" + + def apply(self, query: Query, value: bool) -> Query: + if value: + return query.filter(Tag.type == TagType.custom) + if value is False: + return query.filter(Tag.type != TagType.custom) + return query diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py index 863288a3e73ec..af58eaa195a00 100644 --- a/tests/integration_tests/tags/api_tests.py +++ b/tests/integration_tests/tags/api_tests.py @@ -17,6 +17,7 @@ # isort:skip_file """Unit tests for Superset""" import json +import prison from datetime import datetime from flask import g @@ -30,6 +31,7 @@ from superset.models.sql_lab import SavedQuery from superset.tags.models import user_favorite_tag_table from unittest.mock import patch +from urllib import parse import tests.integration_tests.test_app @@ -175,6 +177,50 @@ def test_get_list_tag(self): # check expected columns assert data["list_columns"] == TAGS_LIST_COLUMNS + def test_get_list_tag_filtered(self): + """ + Query API: Test get list query applying filters for + type == "custom" and type != "custom" + """ + tags = [ + {"name": "Test custom Tag", "type": "custom"}, + {"name": "type:dashboard", "type": "type"}, + {"name": "owner:1", "type": "owner"}, + {"name": "Another Tag", "type": "custom"}, + {"name": "favorited_by:1", "type": "favorited_by"}, + ] + + for tag in tags: + self.insert_tag( + name=tag["name"], + tag_type=tag["type"], + ) + self.login(username="admin") + + # Only user-created tags + query = { + "filters": [ + { + "col": "type", + "opr": "custom_tag", + "value": True, + } + ], + } + uri = f"api/v1/tag/?{parse.urlencode({'q': prison.dumps(query)})}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == 2 + + # Only system tags + query["filters"][0]["value"] = False + uri = f"api/v1/tag/?{parse.urlencode({'q': prison.dumps(query)})}" + rv = self.client.get(uri) + self.assertEqual(rv.status_code, 200) + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == 3 + # test add tagged objects @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")