diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index e96f5d3ec6c03..a043e001bea8b 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -35,13 +35,9 @@ import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions';
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
-import { fetchTags, OBJECT_TYPES } from 'src/tags';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
-import { TagsList } from 'src/components/Tags';
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
-const MAX_TAGS = 3;
-
const propTypes = {
actions: PropTypes.object.isRequired,
canOverwrite: PropTypes.bool.isRequired,
@@ -154,23 +150,6 @@ export const ExploreChartHeader = ({
const oldSliceName = slice?.slice_name;
- const [tags, setTags] = useState([]);
-
- useEffect(() => {
- if (!isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) return;
- fetchTags(
- {
- objectType: OBJECT_TYPES.CHART,
- objectId: slice.slice_id,
- includeTypes: false,
- },
- tags => setTags(tags),
- () => {
- /** handle error */
- },
- );
- }, [slice]);
-
return (
<>
) : null,
- isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) ? (
-
- tag.type
- ? tag.type === 1 || tag.type === 'TagTypes.custom'
- : true,
- )}
- maxTags={MAX_TAGS}
- />
- ) : null,
]}
rightPanelAdditionalItems={
Date: Tue, 9 Aug 2022 11:58:01 -0400
Subject: [PATCH 29/76] Tags display in multiselect in properties modal
---
.../components/PropertiesModal/index.tsx | 106 ++++++++++--------
.../components/PropertiesModal/index.tsx | 106 +++++++++++-------
2 files changed, 125 insertions(+), 87 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index dd5b9ff8e0cfd..e487543e6136a 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Input } from 'src/components/Input';
import { FormItem } from 'src/components/Form';
import jsonStringify from 'json-stringify-pretty-compact';
@@ -105,10 +105,19 @@ const PropertiesModal = ({
const [roles, setRoles] = useState([]);
const saveLabel = onlyApply ? t('Apply') : t('Save');
const [tags, setTags] = useState([]);
- const [newTags, setNewTags] = useState([]);
- const [oldTags, setOldTags] = useState([]);
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
+ const tagsAsSelectValues = useMemo(() => {
+ const selectTags = tags.map((tag) => {
+ return {
+ value:tag.name,
+ label:tag.name
+ }
+ });
+ return selectTags;
+ }, [tags.length])
+
+
const handleErrorResponse = async (response: Response) => {
const { error, statusText, message } = await getClientErrorObject(response);
let errorText = error || statusText || t('An error has occurred');
@@ -349,29 +358,21 @@ const PropertiesModal = ({
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
// update tags
- newTags.map((tag: TagType) =>
- addTag(
+ try {
+ fetchTags(
{
objectType: OBJECT_TYPES.DASHBOARD,
objectId: dashboardId,
includeTypes: false,
},
- tag.name,
- () => {},
- () => {},
- ),
- );
- oldTags.map((tag: TagType) =>
- deleteTag(
- {
- objectType: OBJECT_TYPES.DASHBOARD,
- objectId: dashboardId,
+ (currentTags: TagType[]) => updateTags(currentTags, tags),
+ () => {
+ /* TODO: handle error */
},
- tag,
- () => {},
- () => {},
- ),
- );
+ );
+ } catch (error: any) {
+ console.log(error);
+ }
}
const moreOnSubmitProps: { roles?: Roles } = {};
@@ -575,24 +576,47 @@ const PropertiesModal = ({
}
}, [dashboardId]);
- const handleAddTag = (values: { label: string; value: number }[]) => {
- values.map((value: { label: string; value: number }) => {
- const tag = { name: value.label };
- if (tags.some(t => t.name === tag.name)) {
- return;
+ const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
+ // update the tags for this object
+ // add tags that are in new tags, but not in old tags
+ newTags.map((tag: TagType) => {
+ if (!oldTags.some(t => t.name === tag.name)) {
+ addTag(
+ {
+ objectType: OBJECT_TYPES.DASHBOARD,
+ objectId: dashboardId,
+ includeTypes: false,
+ },
+ tag.name,
+ () => {},
+ () => {},
+ );
}
- setTags([...tags, tag]);
- setNewTags([...newTags, tag]);
});
- };
+ // delete tags that are in old tags, but not in new tags
+ oldTags.map((tag: TagType) => {
+ if (!newTags.some(t => t.name === tag.name)) {
+ deleteTag(
+ {
+ objectType: OBJECT_TYPES.DASHBOARD,
+ objectId: dashboardId,
+ },
+ tag,
+ () => {},
+ () => {},
+ )
+ }
+ });
+ }
- const handleDeleteTag = (tagIndex: number) => {
- setOldTags([...oldTags, tags[tagIndex]]);
- setTags([
- ...tags.slice(0, tagIndex),
- ...tags.slice(tagIndex + 1, tags.length),
- ]);
- };
+ const handleChangeTags = (values: { label: string; value: number }[]) => {
+ // triggered whenever a new tag is selected or a tag was deselected
+ // on new tag selected, add the tag
+
+ const uniqueTags = [...new Set(values.map((v => v.label)))];
+ setTags([...uniqueTags.map(t => ({name: t}))])
+ return;
+ }
return (
@@ -714,14 +738,6 @@ const PropertiesModal = ({
{t('A list of tags that have been applied to this chart.')}
-
-
-
) : null}
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index e2cd7229f5161..7458754755891 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -74,8 +74,16 @@ function PropertiesModal({
);
const [tags, setTags] = useState([]);
- const [newTags, setNewTags] = useState([]);
- const [oldTags, setOldTags] = useState([]);
+
+ const tagsAsSelectValues = useMemo(() => {
+ const selectTags = tags.map((tag) => {
+ return {
+ value:tag.name,
+ label:tag.name
+ }
+ });
+ return selectTags;
+ }, [tags.length])
function showError({ error, statusText, message }: any) {
let errorText = error || statusText || t('An error has occurred');
@@ -164,29 +172,21 @@ function PropertiesModal({
}
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
// update tags
- newTags.map((tag: TagType) =>
- addTag(
+ try {
+ fetchTags(
{
objectType: OBJECT_TYPES.CHART,
objectId: slice.slice_id,
includeTypes: false,
},
- tag.name,
- () => {},
- () => {},
- ),
- );
- oldTags.map((tag: TagType) =>
- deleteTag(
- {
- objectType: OBJECT_TYPES.CHART,
- objectId: slice.slice_id,
+ (currentTags: TagType[]) => updateTags(currentTags, tags),
+ () => {
+ /* TODO: handle error */
},
- tag,
- () => {},
- () => {},
- ),
- );
+ );
+ } catch (error: any) {
+ console.log(error);
+ }
}
try {
@@ -243,24 +243,51 @@ function PropertiesModal({
}
}, [slice.slice_id]);
- const handleAddTag = (values: { label: string; value: number }[]) => {
- values.map((value: { label: string; value: number }) => {
- const tag = { name: value.label };
- if (tags.some(t => t.name === tag.name)) {
- return;
+ const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
+ // update the tags for this object
+ // add tags that are in new tags, but not in old tags
+ newTags.map((tag: TagType) => {
+ if (!oldTags.some(t => t.name === tag.name)) {
+ addTag(
+ {
+ objectType: OBJECT_TYPES.CHART,
+ objectId: slice.slice_id,
+ includeTypes: false,
+ },
+ tag.name,
+ () => {},
+ () => {},
+ );
}
- setTags([...tags, tag]);
- setNewTags([...newTags, tag]);
});
- };
+ // delete tags that are in old tags, but not in new tags
+ oldTags.map((tag: TagType) => {
+ if (!newTags.some(t => t.name === tag.name)) {
+ deleteTag(
+ {
+ objectType: OBJECT_TYPES.CHART,
+ objectId: slice.slice_id,
+ },
+ tag,
+ () => {},
+ () => {},
+ )
+ }
+ });
+ }
- const handleDeleteTag = (tagIndex: number) => {
- setOldTags([...oldTags, tags[tagIndex]]);
- setTags([
- ...tags.slice(0, tagIndex),
- ...tags.slice(tagIndex + 1, tags.length),
- ]);
- };
+ const handleChangeTags = (values: { label: string; value: number }[]) => {
+ // triggered whenever a new tag is selected or a tag was deselected
+ // on new tag selected, add the tag
+
+ const uniqueTags = [...new Set(values.map((v => v.label)))];
+ setTags([...uniqueTags.map(t => ({name: t}))])
+ return;
+ }
+
+ const handleClearTags = () => {
+ setTags([]);
+ }
return (
{t('A list of tags that have been applied to this chart.')}
-
)}
From 63be0302e9abd741b90376fadf56ca797481bf76 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 9 Aug 2022 14:07:06 -0400
Subject: [PATCH 30/76] CRUD tag search is now a select
---
superset-frontend/src/components/Tags/TagsList.tsx | 4 ++--
superset-frontend/src/views/CRUD/chart/ChartList.tsx | 6 +++++-
.../src/views/CRUD/dashboard/DashboardList.tsx | 7 +++++--
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/superset-frontend/src/components/Tags/TagsList.tsx b/superset-frontend/src/components/Tags/TagsList.tsx
index 4748f55cda4ff..4f763299da9d1 100644
--- a/superset-frontend/src/components/Tags/TagsList.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.tsx
@@ -30,8 +30,8 @@ export type TagsListProps = {
* Only applies when editable is true
* Callback for when a tag is deleted
*/
- onDelete: ((index: number) => void) | undefined;
- maxTags: number | undefined;
+ onDelete?: ((index: number) => void) | undefined;
+ maxTags?: number | undefined;
};
const TagsDiv = styled.div`
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 8249d9465c6f7..7b73e7b25c06e 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -64,6 +64,8 @@ import setupPlugins from 'src/setup/setupPlugins';
import InfoTooltip from 'src/components/InfoTooltip';
import CertifiedBadge from 'src/components/CertifiedBadge';
import ChartCard from './ChartCard';
+import { OBJECT_TYPES } from 'src/tags';
+import { loadTags } from 'src/components/ObjectTags';
const FlexRowContainer = styled.div`
align-items: center;
@@ -602,8 +604,10 @@ function ChartList(props: ChartListProps) {
filters_list.push({
Header: t('Tags'),
id: 'tags',
- input: 'search',
+ input: 'select',
operator: FilterOperator.chartTags,
+ unfilteredLabel: t('All'),
+ fetchSelects: loadTags
});
}
filters_list.push({
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index e943ff8a1abeb..e2e56c08b5441 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -53,6 +53,7 @@ import Dashboard from 'src/dashboard/containers/Dashboard';
import CertifiedBadge from 'src/components/CertifiedBadge';
import DashboardCard from './DashboardCard';
import { DashboardStatus } from './types';
+import { loadTags } from 'src/components/ObjectTags';
const PAGE_SIZE = 25;
const PASSWORDS_NEEDED_MESSAGE = t(
@@ -551,8 +552,10 @@ function DashboardList(props: DashboardListProps) {
filters_list.push({
Header: t('Tags'),
id: 'tags',
- input: 'search',
- operator: FilterOperator.dashboardTags,
+ input: 'select',
+ operator: FilterOperator.chartTags,
+ unfilteredLabel: t('All'),
+ fetchSelects: loadTags
});
}
filters_list.push({
From f794b69748d8c8398e7072cee4dad38660aa83df Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 30 Aug 2022 08:54:13 -0400
Subject: [PATCH 31/76] added styles and removed comments
---
.../legacy-preset-chart-deckgl/package.json | 1 -
.../src/components/ObjectTags/ObjectTags.css | 149 -----------------
.../src/components/ObjectTags/index.tsx | 1 -
.../src/components/ObjectTags/styles.ts | 155 ++++++++++++++++++
superset-frontend/src/components/Tags/Tag.tsx | 12 +-
.../src/components/Tags/TagsList.test.tsx | 1 -
.../components/PropertiesModal/index.tsx | 2 +-
.../components/ExploreChartHeader/index.jsx | 5 -
.../src/views/CRUD/tags/TagsTable.tsx | 2 +-
9 files changed, 161 insertions(+), 167 deletions(-)
delete mode 100644 superset-frontend/src/components/ObjectTags/ObjectTags.css
create mode 100644 superset-frontend/src/components/ObjectTags/styles.ts
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
index 7278bb705cbf9..7b11af60566e5 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json
@@ -48,7 +48,6 @@
"react-dom": "^16.13.1",
"react-map-gl": "^4.0.10",
"mapbox-gl": "*"
-
},
"publishConfig": {
"access": "public"
diff --git a/superset-frontend/src/components/ObjectTags/ObjectTags.css b/superset-frontend/src/components/ObjectTags/ObjectTags.css
deleted file mode 100644
index 069f247179532..0000000000000
--- a/superset-frontend/src/components/ObjectTags/ObjectTags.css
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * 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.
- */
-.ant-tag {
- color: black;
-}
-
-.react-tags {
- position: relative;
- display: inline-block;
- padding: 1px 0 0 1px;
- margin: 0 10px;
- border: 0px 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__selected {
- display: inline;
-}
-
-.react-tags__selected-tag {
- display: inline-block;
- box-sizing: border-box;
- margin: 0;
- padding: 6px 8px;
- border: 0px solid #f5f5f5;
- border-radius: 2px;
- background: #f1f1f1;
-
- /* match the font styles */
- font-size: inherit;
- line-height: inherit;
-}
-
-.react-tags__search {
- display: inline-block;
-
- /* new tag border layout */
- border: 1px dashed #d9d9d9;
-
- /* match tag layout */
- line-height: 20px;
- margin-bottom: 0;
- padding: 0 7px;
-
- /* prevent autoresize overflowing the container */
- max-width: 100%;
-}
-
-.react-tags__search:focus-within {
- border: 1px solid #000000;
-}
-
-@media screen and (min-width: 30em) {
- .react-tags__search {
- /* this will become the offsetParent for suggestions */
- position: relative;
- }
-}
-
-.react-tags__search input {
- max-width: 150%;
-
- /* remove styles and layout from this element */
- margin: 0;
- margin-left: 0;
- 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;
-}
diff --git a/superset-frontend/src/components/ObjectTags/index.tsx b/superset-frontend/src/components/ObjectTags/index.tsx
index 035281da6adff..4ae38022dbfca 100644
--- a/superset-frontend/src/components/ObjectTags/index.tsx
+++ b/superset-frontend/src/components/ObjectTags/index.tsx
@@ -93,7 +93,6 @@ export const loadTags = async (
return cachedSupersetGet({
endpoint: `/api/v1/tag/?q=${query}`,
- // endpoint: `/api/v1/tags/?q=${query}`,
})
.then(response => {
const data: {
diff --git a/superset-frontend/src/components/ObjectTags/styles.ts b/superset-frontend/src/components/ObjectTags/styles.ts
new file mode 100644
index 0000000000000..18fea1ca9a9e9
--- /dev/null
+++ b/superset-frontend/src/components/ObjectTags/styles.ts
@@ -0,0 +1,155 @@
+/**
+ * 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 { css, SupersetTheme } from '@superset-ui/core';
+
+ export const objectTagsStyles = (theme: SupersetTheme) => css`
+ .ant-tag {
+ color: ${theme.colors.grayscale.dark2};
+ }
+
+ .react-tags {
+ position: relative;
+ display: inline-block;
+ padding: 1px 0 0 1px;
+ margin: 0 ${theme.gridUnit * 2.5}px;
+ border: 0px solid #f5f5f5;
+ border-radius: 1px;
+
+ /* shared font styles */
+ font-size: ${theme.gridUnit * 3}px;
+ line-height: 1.2;
+
+ /* clicking anywhere will focus the input */
+ cursor: text;
+ }
+
+ .react-tags__selected {
+ display: inline;
+ }
+
+ .react-tags__selected-tag {
+ display: inline-block;
+ box-sizing: border-box;
+ margin: 0;
+ padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
+ border: 0px solid #f5f5f5;
+ border-radius: ${theme.borderRadius}px;
+ background: #f1f1f1;
+
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ .react-tags__search {
+ display: inline-block;
+
+ /* new tag border layout */
+ border: 1px dashed #d9d9d9;
+
+ /* match tag layout */
+ line-height: ${theme.gridUnit * 5}px;
+ margin-bottom: 0;
+ padding: 0 ${theme.gridUnit * 1.75}px;
+
+ /* prevent autoresize overflowing the container */
+ max-width: 100%;
+ }
+
+ .react-tags__search:focus-within {
+ border: 1px solid ${theme.colors.grayscale.dark2};
+ }
+
+ @media screen and (min-width: ${theme.gridUnit * 7.5}em) {
+ .react-tags__search {
+ /* this will become the offsetParent for suggestions */
+ position: relative;
+ }
+ }
+
+ .react-tags__search input {
+ max-width: 150%;
+
+ /* remove styles and layout from this element */
+ margin: 0;
+ margin-left: 0;
+ 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: ${theme.zIndex.max};
+ }
+
+ @media screen and (min-width: ${theme.gridUnit * 7.5}em) {
+ .react-tags__suggestions {
+ width: ${theme.gridUnit * 60}px;
+ }
+ }
+
+ .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: ${ theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
+ }
+
+ .react-tags__suggestions li mark {
+ text-decoration: underline;
+ background: none;
+ font-weight: ${theme.typography.weights.bold};
+ }
+
+ .react-tags__suggestions li:hover {
+ cursor: pointer;
+ background: #eee;
+ }
+
+ .react-tags__suggestions li.is-active {
+ background: #b7cfe0;
+ }
+
+ .react-tags__suggestions li.is-disabled {
+ opacity: calc(${theme.opacity.mediumHeavy});
+ cursor: auto;
+ }
+ `;
+
\ No newline at end of file
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
index 0dbd62edbdda2..f75b19f0db020 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -23,14 +23,10 @@ import AntdTag from 'antd/lib/tag';
import React, { useMemo } from 'react';
import { Tooltip } from 'src/components/Tooltip';
-const customTagStyler = (theme: SupersetTheme) => `
- margin-top: ${theme.gridUnit * 1}px;
- margin-bottom: ${theme.gridUnit * 1}px;
- font-size: ${theme.typography.sizes.s}px;
-`;
-
const StyledTag = styled(AntdTag)`
- ${({ theme }) => customTagStyler(theme)}}
+ margin-top: ${({ theme }) => theme.gridUnit}px;
+ margin-bottom: ${({ theme }) => theme.gridUnit}px;
+ font-size: ${({ theme }) => theme.typography.sizes.s}px;
`;
const Tag = ({
@@ -59,7 +55,7 @@ const Tag = ({
) : (
{id ? (
-
+
{isLongTag ? `${name.slice(0, 20)}...` : name}
) : isLongTag ? (
diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx b/superset-frontend/src/components/Tags/TagsList.test.tsx
index d9817e6458134..4e2ffd2567d5d 100644
--- a/superset-frontend/src/components/Tags/TagsList.test.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.test.tsx
@@ -54,7 +54,6 @@ const findAllTags = () => screen.getAllByRole('link')! as HTMLElement[];
test('should render', () => {
const { container } = render();
expect(container).toBeInTheDocument();
- // console.log(screen.getAllByRole("tag"));
});
test('should render 5 elements', () => {
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index e487543e6136a..1ae440a4d695f 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -717,7 +717,7 @@ const PropertiesModal = ({
- {t('Tags')}
+ {t('Tags')}
{isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) ? (
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index a043e001bea8b..cf35e79e7d052 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -61,11 +61,6 @@ const saveButtonStyles = theme => css`
}
`;
-// const StyledButtons = styled.span`
-// display: flex;
-// align-items: center;
-// `;
-
export const ExploreChartHeader = ({
dashboardId,
slice,
diff --git a/superset-frontend/src/views/CRUD/tags/TagsTable.tsx b/superset-frontend/src/views/CRUD/tags/TagsTable.tsx
index 1afb4d29715c9..ae8fc44674f0b 100644
--- a/superset-frontend/src/views/CRUD/tags/TagsTable.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagsTable.tsx
@@ -85,7 +85,7 @@ export default function TagsTable({ search = '' }: TagsTableProps) {
const data = objects[type].map((o: TaggedObject) => ({
[type]: {o.name},
// eslint-disable-next-line react/no-danger
- creator: ,
+ creator: o.creator
,
modified: moment.utc(o.changed_on).fromNow(),
}));
return (
From 10a5cefbca9b439187e7f361ef272a5b65d334ac Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 31 Aug 2022 09:00:14 -0400
Subject: [PATCH 32/76] added missing error handling
---
.../src/components/ObjectTags/index.tsx | 14 ++++++++------
.../dashboard/components/PropertiesModal/index.tsx | 6 +++---
.../explore/components/PropertiesModal/index.tsx | 8 ++++----
3 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/superset-frontend/src/components/ObjectTags/index.tsx b/superset-frontend/src/components/ObjectTags/index.tsx
index 4ae38022dbfca..88dfe69ba58e7 100644
--- a/superset-frontend/src/components/ObjectTags/index.tsx
+++ b/superset-frontend/src/components/ObjectTags/index.tsx
@@ -21,7 +21,7 @@ import React, { useEffect, useState } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
import Tag from 'src/types/TagType';
-import './ObjectTags.css';
+import { objectTagsStyles } from './styles';
import { TagsList } from 'src/components/Tags';
import rison from 'rison';
import { cacheWrapper } from 'src/utils/cacheWrapper';
@@ -128,11 +128,11 @@ export const ObjectTags = ({
{ objectType, objectId, includeTypes },
(tags: Tag[]) => setTags(tags),
() => {
- /* TODO: handle error */
+ throw new Error(t('An Error occured when fetching tags'));
},
);
} catch (error: any) {
- console.log(error);
+ throw new Error(t('An Error occured when fetching tags'));
}
}, [objectType, objectId, includeTypes]);
@@ -141,8 +141,8 @@ export const ObjectTags = ({
{ objectType, objectId },
tags[tagIndex],
() => setTags(tags.filter((_, i) => i !== tagIndex)),
- () => {
- /* TODO: handle error */
+ (error) => {
+ throw new Error(t('An Error occured when deleting the tag'));
},
);
onChange?.(tags);
@@ -150,7 +150,9 @@ export const ObjectTags = ({
return (
-
+
updateTags(currentTags, tags),
- () => {
- /* TODO: handle error */
+ (error) => {
+ handleErrorResponse(error);
},
);
} catch (error: any) {
@@ -568,7 +568,7 @@ const PropertiesModal = ({
},
(tags: TagType[]) => setTags(tags),
() => {
- /* TODO: handle error */
+ handleErrorResponse(error);
},
);
} catch (error: any) {
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index 7458754755891..2cf2628e01510 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -180,8 +180,8 @@ function PropertiesModal({
includeTypes: false,
},
(currentTags: TagType[]) => updateTags(currentTags, tags),
- () => {
- /* TODO: handle error */
+ (error) => {
+ showError(error)
},
);
} catch (error: any) {
@@ -234,8 +234,8 @@ function PropertiesModal({
includeTypes: false,
},
(tags: TagType[]) => setTags(tags),
- () => {
- /* TODO: handle error */
+ (error) => {
+ showError(error);
},
);
} catch (error: any) {
From 4f03a3f0139db7e4342da5580ba8bb8d1456e0e5 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 15 Sep 2022 09:24:05 -0400
Subject: [PATCH 33/76] changed tags view to all entities view
---
superset-frontend/src/tags.ts | 10 +-
.../Tags.tsx => allentities/AllEntities.tsx} | 32 +-
.../AllEntitiesTable.tsx} | 44 +-
superset-frontend/src/views/routes.tsx | 8 +-
superset/initialization/__init__.py | 11 +-
superset/views/all_entities.py | 255 +++++++++
superset/views/tags.py | 510 +++++++++---------
7 files changed, 565 insertions(+), 305 deletions(-)
rename superset-frontend/src/views/CRUD/{tags/Tags.tsx => allentities/AllEntities.tsx} (78%)
rename superset-frontend/src/views/CRUD/{tags/TagsTable.tsx => allentities/AllEntitiesTable.tsx} (78%)
create mode 100644 superset/views/all_entities.py
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index 16d0d1c14a776..e0f1fd5b61792 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -37,7 +37,7 @@ export function fetchTags(
if (objectType === undefined || objectId === undefined) {
throw new Error('Need to specify objectType and objectId');
}
- SupersetClient.get({ endpoint: `/tagview/tags/${objectType}/${objectId}/` })
+ SupersetClient.get({ endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/` })
.then(({ json }) =>
callback(
json.filter((tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes),
@@ -51,7 +51,7 @@ export function fetchSuggestions(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- SupersetClient.get({ endpoint: '/tagview/tags/suggestions/' })
+ SupersetClient.get({ endpoint: '/taggedobjectview/tags/suggestions/' })
.then(({ json }) =>
callback(
json.filter((tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes),
@@ -70,7 +70,7 @@ export function deleteTag(
throw new Error('Need to specify objectType and objectId');
}
SupersetClient.delete({
- endpoint: `/tagview/tags/${objectType}/${objectId}/`,
+ endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
body: JSON.stringify([tag.name]),
parseMethod: 'text',
})
@@ -95,7 +95,7 @@ export function addTag(
return;
}
SupersetClient.post({
- endpoint: `/tagview/tags/${objectType}/${objectId}/`,
+ endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
body: JSON.stringify([tag]),
parseMethod: 'text',
})
@@ -108,7 +108,7 @@ export function fetchObjects(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- let url = `/tagview/tagged_objects/?tags=${tags}`;
+ let url = `/taggedobjectview/tagged_objects/?tags=${tags}`;
if (types) {
url += `&types=${types}`;
}
diff --git a/superset-frontend/src/views/CRUD/tags/Tags.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
similarity index 78%
rename from superset-frontend/src/views/CRUD/tags/Tags.tsx
rename to superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
index b5fba5a1b2b78..fda791ff9a654 100644
--- a/superset-frontend/src/views/CRUD/tags/Tags.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
@@ -16,16 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { styled } from '@superset-ui/core';
-import Tag from 'src/types/TagType';
+import Tag, { TagType } from 'src/types/TagType';
import { StringParam, useQueryParam } from 'use-query-params';
import withToasts from 'src/components/MessageToasts/withToasts';
import SelectControl from 'src/explore/components/controls/SelectControl';
import { fetchSuggestions } from 'src/tags';
-import TagsTable from './TagsTable';
+import AllEntitiesTable from './AllEntitiesTable';
+import { loadTags } from 'src/components/ObjectTags';
+import { AsyncSelect, Select } from 'src/components';
+import { LabeledValue } from 'antd/lib/select';
-const TagsContainer = styled.div`
+const AllEntitiesContainer = styled.div`
background-color: ${({ theme }) => theme.colors.grayscale.light4};
.select-control {
margin-left: ${({ theme }) => theme.gridUnit * 4}px;
@@ -40,7 +43,7 @@ const TagsContainer = styled.div`
}
`;
-const TagsNav = styled.div`
+const AllEntitiesNav = styled.div`
height: 50px;
background-color: ${({ theme }) => theme.colors.grayscale.light5};
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
@@ -50,7 +53,8 @@ const TagsNav = styled.div`
}
`;
-function Tags() {
+function AllEntities() {
+
const [tagSuggestions, setTagSuggestions] = useState();
const [tagsQuery, setTagsQuery] = useQueryParam('tags', StringParam);
@@ -73,12 +77,12 @@ function Tags() {
};
return (
-
-
- Tags
-
+
+
+ All Entities
+
-
Search tags
+
search by tags
-
-
+
+
);
}
-export default withToasts(Tags);
+export default withToasts(AllEntities);
diff --git a/superset-frontend/src/views/CRUD/tags/TagsTable.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
similarity index 78%
rename from superset-frontend/src/views/CRUD/tags/TagsTable.tsx
rename to superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
index ae8fc44674f0b..ef9514ec7ffd7 100644
--- a/superset-frontend/src/views/CRUD/tags/TagsTable.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
@@ -20,10 +20,12 @@ import React, { useState, useEffect } from 'react';
import moment from 'moment';
import { t, styled } from '@superset-ui/core';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
-import { fetchObjects } from '../../../tags';
+import { fetchObjects, fetchTags } from '../../../tags';
import Loading from '../../../components/Loading';
+import { Tag, TagsList } from 'src/components/Tags';
+import { loadTags } from 'src/components/ObjectTags';
-const TagsTableContainer = styled.div`
+const AllEntitiesTableContainer = styled.div`
text-align: left;
border-radius: ${({ theme }) => theme.gridUnit * 1}px 0;
margin: 0 ${({ theme }) => theme.gridUnit * 4}px;
@@ -51,11 +53,11 @@ interface TaggedObjects {
query: TaggedObject[];
}
-interface TagsTableProps {
+interface AllEntitiesTableProps {
search?: string;
}
-export default function TagsTable({ search = '' }: TagsTableProps) {
+export default function AllEntitiesTable({ search = '' }: AllEntitiesTableProps) {
const [objects, setObjects] = useState({
dashboard: [],
chart: [],
@@ -63,22 +65,20 @@ export default function TagsTable({ search = '' }: TagsTableProps) {
});
useEffect(() => {
- const fetchResults = (search: string) => {
- fetchObjects(
- { tags: search, types: null },
- (data: TaggedObject[]) => {
- const objects = { dashboard: [], chart: [], query: [] };
- data.forEach(object => {
- objects[object.type].push(object);
- });
- setObjects(objects);
- },
- (error: Response) => {
- console.log(error.json());
- },
- );
- };
- fetchResults(search);
+ fetchObjects(
+ { tags: search, types: null },
+ (data: TaggedObject[]) => {
+ const objects = { dashboard: [], chart: [], query: [] };
+ data.forEach(object => {
+ objects[object.type].push(object);
+ });
+ setObjects(objects);
+ },
+ (error: Response) => {
+ console.log(error.json());
+ },
+ );
+
}, [search]);
const renderTable = (type: any) => {
@@ -108,7 +108,7 @@ export default function TagsTable({ search = '' }: TagsTableProps) {
if (objects) {
return (
-
+
{t('Dashboards')}
{renderTable('dashboard')}
@@ -117,7 +117,7 @@ export default function TagsTable({ search = '' }: TagsTableProps) {
{t('Queries')}
{renderTable('query')}
-
+
);
}
return ;
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index e76f7f0aea23f..2ab6fc6c48180 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -103,8 +103,8 @@ const SavedQueryList = lazy(
/* webpackChunkName: "SavedQueryList" */ 'src/views/CRUD/data/savedquery/SavedQueryList'
),
);
-const TagsPage = lazy(
- () => import(/* webpackChunkName: "Tags" */ 'src/views/CRUD/tags/Tags'),
+const AllEntitiesPage = lazy(
+ () => import(/* webpackChunkName: "AllEntities" */ 'src/views/CRUD/allentities/AllEntities'),
);
type Routes = {
@@ -197,8 +197,8 @@ export const routes: Routes = [
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
routes.push({
- path: '/superset/tags/',
- Component: TagsPage,
+ path: '/superset/all_entities/',
+ Component: AllEntitiesPage,
});
}
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 36915d64cc5af..d596ef419d466 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -184,8 +184,9 @@ def init_views(self) -> None:
TableSchemaView,
TabStateView,
)
- from superset.views.tags import TagModelView, TagView
from superset.views.users.api import CurrentUserRestApi
+ from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
+
#
# Setup API views
@@ -307,7 +308,7 @@ def init_views(self) -> None:
appbuilder.add_view_no_menu(TableModelView)
appbuilder.add_view_no_menu(TableSchemaView)
appbuilder.add_view_no_menu(TabStateView)
- appbuilder.add_view_no_menu(TagView)
+ appbuilder.add_view_no_menu(TaggedObjectView)
appbuilder.add_view_no_menu(ReportView)
#
@@ -369,9 +370,9 @@ def init_views(self) -> None:
)
appbuilder.add_separator("Data")
appbuilder.add_view(
- TagModelView,
- "Tags",
- label=__("Tags"),
+ TaggedObjectsModelView,
+ "All Entities",
+ label=__("All Entities"),
icon="",
category="",
category_icon="",
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
new file mode 100644
index 0000000000000..0c57da097284b
--- /dev/null
+++ b/superset/views/all_entities.py
@@ -0,0 +1,255 @@
+# 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 __future__ import absolute_import, division, print_function, unicode_literals
+
+from typing import Any, Dict, List
+
+import simplejson as json
+from flask import request, Response
+from flask_appbuilder import expose
+from flask_appbuilder.hooks import before_request
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from flask_appbuilder.security.decorators import has_access, has_access_api
+from jinja2.sandbox import SandboxedEnvironment
+from sqlalchemy import and_, func
+from werkzeug.exceptions import NotFound
+
+from superset import db, is_feature_enabled, utils
+from superset.jinja_context import ExtraCache
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from superset.models.sql_lab import SavedQuery
+from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.superset_typing import FlaskResponse
+from superset.views.base import SupersetModelView
+
+from .base import BaseSupersetView, json_success
+
+
+def process_template(content: str) -> str:
+ env = SandboxedEnvironment()
+ template = env.from_string(content)
+ context = {
+ "current_user_id": ExtraCache.current_user_id,
+ "current_username": ExtraCache.current_username,
+ }
+ return template.render(context)
+
+class TaggedObjectsModelView(SupersetModelView):
+ route_base = "/superset/all_entities"
+ datamodel = SQLAInterface(Tag)
+ class_permission_name = "Tags"
+
+ @has_access
+ @expose("/")
+ def list(self) -> FlaskResponse:
+ if not is_feature_enabled("TAGGING_SYSTEM"):
+ return super().list()
+
+ return super().render_app_template()
+
+class TaggedObjectView(BaseSupersetView):
+ @staticmethod
+ def is_enabled() -> bool:
+ return is_feature_enabled("TAGGING_SYSTEM")
+
+ @before_request
+ def ensure_enabled(self) -> None:
+ if not self.is_enabled():
+ raise NotFound()
+
+ @has_access_api
+ @expose("/tags/suggestions/", methods=["GET"])
+ def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
+ query = (
+ db.session.query(TaggedObject)
+ .join(Tag)
+ .with_entities(TaggedObject.tag_id, Tag.name)
+ .group_by(TaggedObject.tag_id, Tag.name)
+ .order_by(func.count().desc())
+ .all()
+ )
+ tags = [{"id": id, "name": name} for id, name in query]
+ return json_success(json.dumps(tags))
+
+ @has_access_api
+ @expose("/tags///", methods=["GET"])
+ def get( # pylint: disable=no-self-use
+ self, object_type: ObjectTypes, object_id: int
+ ) -> FlaskResponse:
+ """List all tags a given object has."""
+ if object_id == 0:
+ return json_success(json.dumps([]))
+
+ query = db.session.query(TaggedObject).filter(
+ and_(
+ TaggedObject.object_type == object_type,
+ TaggedObject.object_id == object_id,
+ )
+ )
+ tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
+ return json_success(json.dumps(tags))
+
+ @has_access_api
+ @expose("/tags///", methods=["POST"])
+ def post( # pylint: disable=no-self-use
+ self, object_type: ObjectTypes, object_id: int
+ ) -> FlaskResponse:
+ """Add new tags to an object."""
+ if object_id == 0:
+ return Response(status=404)
+
+ tagged_objects = []
+ for name in request.get_json(force=True):
+ if ":" in name:
+ type_name = name.split(":", 1)[0]
+ type_ = TagTypes[type_name]
+ else:
+ type_ = TagTypes.custom
+
+ tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
+ if not tag:
+ tag = Tag(name=name, type=type_)
+
+ tagged_objects.append(
+ TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+ )
+
+ db.session.add_all(tagged_objects)
+ db.session.commit()
+
+ return Response(status=201) # 201 CREATED
+
+ @has_access_api
+ @expose("/tags///", methods=["DELETE"])
+ def delete( # pylint: disable=no-self-use
+ self, object_type: ObjectTypes, object_id: int
+ ) -> FlaskResponse:
+ """Remove tags from an object."""
+ tag_names = request.get_json(force=True)
+ if not tag_names:
+ return Response(status=403)
+
+ db.session.query(TaggedObject).filter(
+ and_(
+ TaggedObject.object_type == object_type,
+ TaggedObject.object_id == object_id,
+ ),
+ TaggedObject.tag.has(Tag.name.in_(tag_names)),
+ ).delete(synchronize_session=False)
+ db.session.commit()
+
+ return Response(status=204) # 204 NO CONTENT
+
+ @has_access_api
+ @expose("/tagged_objects/", methods=["GET", "POST"])
+ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
+ tags = [
+ process_template(tag)
+ for tag in request.args.get("tags", "").split(",")
+ if tag
+ ]
+ if not tags:
+ return json_success(json.dumps([]))
+
+ # filter types
+ types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
+
+ results: List[Dict[str, Any]] = []
+
+ # dashboards
+ if not types or "dashboard" in types:
+ dashboards = (
+ db.session.query(Dashboard)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(Tag.name.in_(tags))
+ )
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.dashboard.name,
+ "name": obj.dashboard_title,
+ "url": obj.url,
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in dashboards
+ )
+
+ # charts
+ if not types or "chart" in types:
+ charts = (
+ db.session.query(Slice)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Slice.id,
+ TaggedObject.object_type == ObjectTypes.chart,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(Tag.name.in_(tags))
+ )
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.chart.name,
+ "name": obj.slice_name,
+ "url": obj.url,
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in charts
+ )
+
+ # saved queries
+ if not types or "query" in types:
+ saved_queries = (
+ db.session.query(SavedQuery)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == SavedQuery.id,
+ TaggedObject.object_type == ObjectTypes.query,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(Tag.name.in_(tags))
+ )
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.query.name,
+ "name": obj.label,
+ "url": obj.url(),
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in saved_queries
+ )
+
+ return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 009e2319320c6..5230349ec5f08 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -1,255 +1,255 @@
-# 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 __future__ import absolute_import, division, print_function, unicode_literals
-
-from typing import Any, Dict, List
-
-import simplejson as json
-from flask import request, Response
-from flask_appbuilder import expose
-from flask_appbuilder.hooks import before_request
-from flask_appbuilder.models.sqla.interface import SQLAInterface
-from flask_appbuilder.security.decorators import has_access, has_access_api
-from jinja2.sandbox import SandboxedEnvironment
-from sqlalchemy import and_, func
-from werkzeug.exceptions import NotFound
-
-from superset import db, is_feature_enabled, utils
-from superset.jinja_context import ExtraCache
-from superset.models.dashboard import Dashboard
-from superset.models.slice import Slice
-from superset.models.sql_lab import SavedQuery
-from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
-from superset.superset_typing import FlaskResponse
-from superset.views.base import SupersetModelView
-
-from .base import BaseSupersetView, json_success
-
-
-def process_template(content: str) -> str:
- env = SandboxedEnvironment()
- template = env.from_string(content)
- context = {
- "current_user_id": ExtraCache.current_user_id,
- "current_username": ExtraCache.current_username,
- }
- return template.render(context)
-
-class TagModelView(SupersetModelView):
- route_base = "/superset/tags"
- datamodel = SQLAInterface(Tag)
- class_permission_name = "Tags"
-
- @has_access
- @expose("/")
- def list(self) -> FlaskResponse:
- if not is_feature_enabled("TAGGING_SYSTEM"):
- return super().list()
-
- return super().render_app_template()
-
-class TagView(BaseSupersetView):
- @staticmethod
- def is_enabled() -> bool:
- return is_feature_enabled("TAGGING_SYSTEM")
-
- @before_request
- def ensure_enabled(self) -> None:
- if not self.is_enabled():
- raise NotFound()
-
- @has_access_api
- @expose("/tags/suggestions/", methods=["GET"])
- def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
- query = (
- db.session.query(TaggedObject)
- .join(Tag)
- .with_entities(TaggedObject.tag_id, Tag.name)
- .group_by(TaggedObject.tag_id, Tag.name)
- .order_by(func.count().desc())
- .all()
- )
- tags = [{"id": id, "name": name} for id, name in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["GET"])
- def get( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """List all tags a given object has."""
- if object_id == 0:
- return json_success(json.dumps([]))
-
- query = db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- )
- )
- tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["POST"])
- def post( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """Add new tags to an object."""
- if object_id == 0:
- return Response(status=404)
-
- tagged_objects = []
- for name in request.get_json(force=True):
- if ":" in name:
- type_name = name.split(":", 1)[0]
- type_ = TagTypes[type_name]
- else:
- type_ = TagTypes.custom
-
- tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
- if not tag:
- tag = Tag(name=name, type=type_)
-
- tagged_objects.append(
- TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
- )
-
- db.session.add_all(tagged_objects)
- db.session.commit()
-
- return Response(status=201) # 201 CREATED
-
- @has_access_api
- @expose("/tags///", methods=["DELETE"])
- def delete( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """Remove tags from an object."""
- tag_names = request.get_json(force=True)
- if not tag_names:
- return Response(status=403)
-
- db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- ),
- TaggedObject.tag.has(Tag.name.in_(tag_names)),
- ).delete(synchronize_session=False)
- db.session.commit()
-
- return Response(status=204) # 204 NO CONTENT
-
- @has_access_api
- @expose("/tagged_objects/", methods=["GET", "POST"])
- def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
- tags = [
- process_template(tag)
- for tag in request.args.get("tags", "").split(",")
- if tag
- ]
- if not tags:
- return json_success(json.dumps([]))
-
- # filter types
- types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
-
- results: List[Dict[str, Any]] = []
-
- # dashboards
- if not types or "dashboard" in types:
- dashboards = (
- db.session.query(Dashboard)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.dashboard.name,
- "name": obj.dashboard_title,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in dashboards
- )
-
- # charts
- if not types or "chart" in types:
- charts = (
- db.session.query(Slice)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.chart.name,
- "name": obj.slice_name,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in charts
- )
-
- # saved queries
- if not types or "query" in types:
- saved_queries = (
- db.session.query(SavedQuery)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == SavedQuery.id,
- TaggedObject.object_type == ObjectTypes.query,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.query.name,
- "name": obj.label,
- "url": obj.url(),
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in saved_queries
- )
-
- return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
+# # 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 __future__ import absolute_import, division, print_function, unicode_literals
+
+# from typing import Any, Dict, List
+
+# import simplejson as json
+# from flask import request, Response
+# from flask_appbuilder import expose
+# from flask_appbuilder.hooks import before_request
+# from flask_appbuilder.models.sqla.interface import SQLAInterface
+# from flask_appbuilder.security.decorators import has_access, has_access_api
+# from jinja2.sandbox import SandboxedEnvironment
+# from sqlalchemy import and_, func
+# from werkzeug.exceptions import NotFound
+
+# from superset import db, is_feature_enabled, utils
+# from superset.jinja_context import ExtraCache
+# from superset.models.dashboard import Dashboard
+# from superset.models.slice import Slice
+# from superset.models.sql_lab import SavedQuery
+# from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+# from superset.superset_typing import FlaskResponse
+# from superset.views.base import SupersetModelView
+
+# from .base import BaseSupersetView, json_success
+
+
+# def process_template(content: str) -> str:
+# env = SandboxedEnvironment()
+# template = env.from_string(content)
+# context = {
+# "current_user_id": ExtraCache.current_user_id,
+# "current_username": ExtraCache.current_username,
+# }
+# return template.render(context)
+
+# class TagModelView(SupersetModelView):
+# route_base = "/superset/tags"
+# datamodel = SQLAInterface(Tag)
+# class_permission_name = "Tags"
+
+# @has_access
+# @expose("/")
+# def list(self) -> FlaskResponse:
+# if not is_feature_enabled("TAGGING_SYSTEM"):
+# return super().list()
+
+# return super().render_app_template()
+
+# class TagView(BaseSupersetView):
+# @staticmethod
+# def is_enabled() -> bool:
+# return is_feature_enabled("TAGGING_SYSTEM")
+
+# @before_request
+# def ensure_enabled(self) -> None:
+# if not self.is_enabled():
+# raise NotFound()
+
+# @has_access_api
+# @expose("/tags/suggestions/", methods=["GET"])
+# def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
+# query = (
+# db.session.query(TaggedObject)
+# .join(Tag)
+# .with_entities(TaggedObject.tag_id, Tag.name)
+# .group_by(TaggedObject.tag_id, Tag.name)
+# .order_by(func.count().desc())
+# .all()
+# )
+# tags = [{"id": id, "name": name} for id, name in query]
+# return json_success(json.dumps(tags))
+
+# @has_access_api
+# @expose("/tags///", methods=["GET"])
+# def get( # pylint: disable=no-self-use
+# self, object_type: ObjectTypes, object_id: int
+# ) -> FlaskResponse:
+# """List all tags a given object has."""
+# if object_id == 0:
+# return json_success(json.dumps([]))
+
+# query = db.session.query(TaggedObject).filter(
+# and_(
+# TaggedObject.object_type == object_type,
+# TaggedObject.object_id == object_id,
+# )
+# )
+# tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
+# return json_success(json.dumps(tags))
+
+# @has_access_api
+# @expose("/tags///", methods=["POST"])
+# def post( # pylint: disable=no-self-use
+# self, object_type: ObjectTypes, object_id: int
+# ) -> FlaskResponse:
+# """Add new tags to an object."""
+# if object_id == 0:
+# return Response(status=404)
+
+# tagged_objects = []
+# for name in request.get_json(force=True):
+# if ":" in name:
+# type_name = name.split(":", 1)[0]
+# type_ = TagTypes[type_name]
+# else:
+# type_ = TagTypes.custom
+
+# tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
+# if not tag:
+# tag = Tag(name=name, type=type_)
+
+# tagged_objects.append(
+# TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+# )
+
+# db.session.add_all(tagged_objects)
+# db.session.commit()
+
+# return Response(status=201) # 201 CREATED
+
+# @has_access_api
+# @expose("/tags///", methods=["DELETE"])
+# def delete( # pylint: disable=no-self-use
+# self, object_type: ObjectTypes, object_id: int
+# ) -> FlaskResponse:
+# """Remove tags from an object."""
+# tag_names = request.get_json(force=True)
+# if not tag_names:
+# return Response(status=403)
+
+# db.session.query(TaggedObject).filter(
+# and_(
+# TaggedObject.object_type == object_type,
+# TaggedObject.object_id == object_id,
+# ),
+# TaggedObject.tag.has(Tag.name.in_(tag_names)),
+# ).delete(synchronize_session=False)
+# db.session.commit()
+
+# return Response(status=204) # 204 NO CONTENT
+
+# @has_access_api
+# @expose("/tagged_objects/", methods=["GET", "POST"])
+# def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
+# tags = [
+# process_template(tag)
+# for tag in request.args.get("tags", "").split(",")
+# if tag
+# ]
+# if not tags:
+# return json_success(json.dumps([]))
+
+# # filter types
+# types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
+
+# results: List[Dict[str, Any]] = []
+
+# # dashboards
+# if not types or "dashboard" in types:
+# dashboards = (
+# db.session.query(Dashboard)
+# .join(
+# TaggedObject,
+# and_(
+# TaggedObject.object_id == Dashboard.id,
+# TaggedObject.object_type == ObjectTypes.dashboard,
+# ),
+# )
+# .join(Tag, TaggedObject.tag_id == Tag.id)
+# .filter(Tag.name.in_(tags))
+# )
+# results.extend(
+# {
+# "id": obj.id,
+# "type": ObjectTypes.dashboard.name,
+# "name": obj.dashboard_title,
+# "url": obj.url,
+# "changed_on": obj.changed_on,
+# "created_by": obj.created_by_fk,
+# "creator": obj.creator(),
+# }
+# for obj in dashboards
+# )
+
+# # charts
+# if not types or "chart" in types:
+# charts = (
+# db.session.query(Slice)
+# .join(
+# TaggedObject,
+# and_(
+# TaggedObject.object_id == Slice.id,
+# TaggedObject.object_type == ObjectTypes.chart,
+# ),
+# )
+# .join(Tag, TaggedObject.tag_id == Tag.id)
+# .filter(Tag.name.in_(tags))
+# )
+# results.extend(
+# {
+# "id": obj.id,
+# "type": ObjectTypes.chart.name,
+# "name": obj.slice_name,
+# "url": obj.url,
+# "changed_on": obj.changed_on,
+# "created_by": obj.created_by_fk,
+# "creator": obj.creator(),
+# }
+# for obj in charts
+# )
+
+# # saved queries
+# if not types or "query" in types:
+# saved_queries = (
+# db.session.query(SavedQuery)
+# .join(
+# TaggedObject,
+# and_(
+# TaggedObject.object_id == SavedQuery.id,
+# TaggedObject.object_type == ObjectTypes.query,
+# ),
+# )
+# .join(Tag, TaggedObject.tag_id == Tag.id)
+# .filter(Tag.name.in_(tags))
+# )
+# results.extend(
+# {
+# "id": obj.id,
+# "type": ObjectTypes.query.name,
+# "name": obj.label,
+# "url": obj.url(),
+# "changed_on": obj.changed_on,
+# "created_by": obj.created_by_fk,
+# "creator": obj.creator(),
+# }
+# for obj in saved_queries
+# )
+
+# return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
From 211d62c00ac9c9ba059356eb13ab11b57d818d45 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 29 Sep 2022 13:56:42 -0400
Subject: [PATCH 34/76] added tags and allentities CRUD views
---
superset-frontend/src/tags.ts | 11 +
.../views/CRUD/allentities/AllEntities.tsx | 7 +-
.../CRUD/allentities/AllEntitiesTable.tsx | 6 +-
.../src/views/CRUD/chart/ChartList.tsx | 5 +-
.../views/CRUD/dashboard/DashboardList.tsx | 5 +-
.../src/views/CRUD/tags/TagCard.tsx | 152 ++++++
.../src/views/CRUD/tags/TagList.tsx | 482 ++++++++++++++++++
superset-frontend/src/views/CRUD/types.ts | 15 +-
superset-frontend/src/views/CRUD/utils.tsx | 4 +
superset-frontend/src/views/routes.tsx | 7 +
superset/initialization/__init__.py | 17 +-
superset/tags/api.py | 30 +-
superset/views/all_entities.py | 9 +-
superset/views/tags.py | 364 ++++---------
14 files changed, 822 insertions(+), 292 deletions(-)
create mode 100644 superset-frontend/src/views/CRUD/tags/TagCard.tsx
create mode 100644 superset-frontend/src/views/CRUD/tags/TagList.tsx
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index e0f1fd5b61792..4d0a1580c1125 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -25,6 +25,17 @@ export const OBJECT_TYPES = Object.freeze({
QUERY: 'query',
});
+export function fetchAllTags(
+ callback: (json: JsonObject) => void,
+ error: (response: Response) => void,
+) {
+ let url = `/tagview/tags/`;
+ SupersetClient.get({ endpoint: url })
+ .then(({ json }) => callback(json))
+ .catch(response => error(response));
+}
+
+
export function fetchTags(
{
objectType,
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
index fda791ff9a654..c9fb2fea149ea 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
@@ -16,17 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { styled } from '@superset-ui/core';
-import Tag, { TagType } from 'src/types/TagType';
+import Tag from 'src/types/TagType';
import { StringParam, useQueryParam } from 'use-query-params';
import withToasts from 'src/components/MessageToasts/withToasts';
import SelectControl from 'src/explore/components/controls/SelectControl';
import { fetchSuggestions } from 'src/tags';
import AllEntitiesTable from './AllEntitiesTable';
-import { loadTags } from 'src/components/ObjectTags';
-import { AsyncSelect, Select } from 'src/components';
-import { LabeledValue } from 'antd/lib/select';
const AllEntitiesContainer = styled.div`
background-color: ${({ theme }) => theme.colors.grayscale.light4};
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
index ef9514ec7ffd7..d5fb262e2e7ee 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
@@ -20,10 +20,8 @@ import React, { useState, useEffect } from 'react';
import moment from 'moment';
import { t, styled } from '@superset-ui/core';
import TableView, { EmptyWrapperType } from 'src/components/TableView';
-import { fetchObjects, fetchTags } from '../../../tags';
+import { fetchObjects } from '../../../tags';
import Loading from '../../../components/Loading';
-import { Tag, TagsList } from 'src/components/Tags';
-import { loadTags } from 'src/components/ObjectTags';
const AllEntitiesTableContainer = styled.div`
text-align: left;
@@ -85,7 +83,6 @@ export default function AllEntitiesTable({ search = '' }: AllEntitiesTableProps)
const data = objects[type].map((o: TaggedObject) => ({
[type]: {o.name},
// eslint-disable-next-line react/no-danger
- creator: o.creator
,
modified: moment.utc(o.changed_on).fromNow(),
}));
return (
@@ -99,7 +96,6 @@ export default function AllEntitiesTable({ search = '' }: AllEntitiesTableProps)
accessor: type,
Header: type.charAt(0).toUpperCase() + type.slice(1),
},
- { accessor: 'creator', Header: 'Creator' },
{ accessor: 'modified', Header: 'Modified' },
]}
/>
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 7b73e7b25c06e..96ee90167f419 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -28,6 +28,7 @@ import { uniqBy } from 'lodash';
import moment from 'moment';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
+ Actions,
createErrorHandler,
createFetchRelated,
handleChartDelete,
@@ -146,10 +147,6 @@ interface ChartListProps {
};
}
-const Actions = styled.div`
- color: ${({ theme }) => theme.colors.grayscale.base};
-`;
-
function ChartList(props: ChartListProps) {
const {
addDangerToast,
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index e2e56c08b5441..0f6e1031ed190 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -25,6 +25,7 @@ import {
createFetchRelated,
createErrorHandler,
handleDashboardDelete,
+ Actions,
} from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@@ -94,10 +95,6 @@ interface Dashboard {
created_by: object;
}
-const Actions = styled.div`
- color: ${({ theme }) => theme.colors.grayscale.base};
-`;
-
function DashboardList(props: DashboardListProps) {
const {
addDangerToast,
diff --git a/superset-frontend/src/views/CRUD/tags/TagCard.tsx b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
new file mode 100644
index 0000000000000..ed4d82eba7150
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
@@ -0,0 +1,152 @@
+/**
+ * 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 { Link, useHistory } from 'react-router-dom';
+ import { t, useTheme } from '@superset-ui/core';
+ import { handleDashboardDelete, CardStyles } from 'src/views/CRUD/utils';
+ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
+ import { AntdDropdown } from 'src/components';
+ import { Menu } from 'src/components/Menu';
+ import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+ import ListViewCard from 'src/components/ListViewCard';
+ import Icons from 'src/components/Icons';
+ import Label from 'src/components/Label';
+ import FacePile from 'src/components/FacePile';
+ import FaveStar from 'src/components/FaveStar';
+ import { Tag } from 'src/views/CRUD/types';
+
+ interface TagCardProps {
+ tag: Tag;
+ hasPerm: (name: string) => boolean;
+ bulkSelectEnabled: boolean;
+ refreshData: () => void;
+ loading: boolean;
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ openTagEditModal?: (t: Tag) => void;
+ tagFilter?: string;
+ userId?: string | number;
+ showThumbnails?: boolean;
+ }
+
+ function TagCard({
+ tag,
+ hasPerm,
+ bulkSelectEnabled,
+ tagFilter,
+ refreshData,
+ userId,
+ addDangerToast,
+ addSuccessToast,
+ openTagEditModal,
+ showThumbnails,
+ }: TagCardProps) {
+ const history = useHistory();
+ const canEdit = hasPerm('can_write');
+ const canDelete = hasPerm('can_write');
+ const canExport = hasPerm('can_export');
+
+ const theme = useTheme();
+ const menu = (
+
+ );
+ return (
+
+ >
+ ) : null
+ }
+ url={undefined}
+ linkComponent={Link}
+ imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
+ description={t('Modified %s', tag.changed_on)}
+ actions={
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ >
+
+
+
+
+ }
+ />
+
+ );
+ }
+
+ export default TagCard;
+
\ No newline at end of file
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
new file mode 100644
index 0000000000000..09d1751fba8d4
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -0,0 +1,482 @@
+/**
+ * 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 React, { useState, useMemo, useCallback } from 'react';
+ import rison from 'rison';
+ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
+ import {
+ createFetchRelated,
+ createErrorHandler,
+ handleDashboardDelete,
+ Actions,
+ } from 'src/views/CRUD/utils';
+ import { useListViewResource } from 'src/views/CRUD/hooks';
+ import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+ import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
+ import ListView, {
+ ListViewProps,
+ Filters,
+ FilterOperator,
+ } from 'src/components/ListView';
+ import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
+ import withToasts from 'src/components/MessageToasts/withToasts';
+ import Icons from 'src/components/Icons';
+ import PropertiesModal from 'src/dashboard/components/PropertiesModal';
+ import { Tooltip } from 'src/components/Tooltip';
+ import TagCard from './TagCard';
+import { Tag } from '../types';
+import FacePile from 'src/components/FacePile';
+import { Link } from 'react-router-dom';
+
+ const PAGE_SIZE = 25;
+
+ interface TagListProps {
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ user: {
+ userId: string | number;
+ firstName: string;
+ lastName: string;
+ };
+ }
+
+ function TagList(props: TagListProps) {
+ const {
+ addDangerToast,
+ addSuccessToast,
+ user: { userId },
+ } = props;
+
+ const {
+ state: {
+ loading,
+ resourceCount: tagCount,
+ resourceCollection: tags,
+ bulkSelectEnabled,
+ },
+ setResourceCollection: setTags,
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource(
+ 'tag',
+ t('tag'),
+ addDangerToast,
+ );
+
+ const [tagToEdit, setTagToEdit] = useState(
+ null,
+ );
+
+ // TODO: Fix usage of localStorage keying on the user id
+ const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
+
+ const canCreate = hasPerm('can_write');
+ const canEdit = hasPerm('can_write');
+ const canDelete = hasPerm('can_write');
+
+ const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+
+ function openTagEditModal(tag: Tag) {
+ setTagToEdit(tag);
+ }
+
+ function handleTagEdit(edits: Tag) {
+ return SupersetClient.get({
+ endpoint: `/api/v1/tag/${edits.id}`,
+ }).then(
+ ({ json = {} }) => {
+ setTags(
+ tags.map(tag => {
+ if (tag.id === json?.result?.id) {
+ const {
+ changed_by_name,
+ changed_by_url,
+ changed_by,
+ dashboard_title = '',
+ slug = '',
+ json_metadata = '',
+ changed_on_delta_humanized,
+ url = '',
+ certified_by = '',
+ certification_details = '',
+ owners,
+ tags,
+ } = json.result;
+ return {
+ ...tag,
+ changed_by_name,
+ changed_by_url,
+ changed_by,
+ dashboard_title,
+ slug,
+ json_metadata,
+ changed_on_delta_humanized,
+ url,
+ certified_by,
+ certification_details,
+ owners,
+ tags,
+ };
+ }
+ return tag;
+ }),
+ );
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('An error occurred while fetching dashboards: %s', errMsg),
+ ),
+ ),
+ );
+ }
+
+ function handleBulkTagDelete(tagsToDelete: Tag[]) {
+ // TODO fix bulk tag delete
+ return SupersetClient.delete({
+ endpoint: `/api/v1/tag/?q=${rison.encode(
+ tagsToDelete.map(({ id }) => id),
+ )}`,
+ }).then(
+ ({ json = {} }) => {
+ refreshData();
+ addSuccessToast(json.message);
+ },
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t('There was an issue deleting the selected tags: ', errMsg),
+ ),
+ ),
+ );
+ }
+
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: {
+ name: tagName,
+ },
+ },
+ }: any) => (
+
+ {tagName}
+
+ ),
+ Header: t('Name'),
+ accessor: 'name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by_name: changedByName,
+ changed_by_url: changedByUrl,
+ },
+ },
+ }: any) => {changedByName},
+ Header: t('Modified by'),
+ accessor: 'changed_by.first_name',
+ size: 'xl',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on_delta_humanized: changedOn },
+ },
+ }: any) => {changedOn},
+ Header: t('Modified'),
+ accessor: 'changed_on_delta_humanized',
+ size: 'xl',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { created_by: createdBy },
+ },
+ }: any) =>
+ createdBy ? : '',
+ Header: t('Created by'),
+ accessor: 'created_by',
+ disableSortBy: true,
+ size: 'xl',
+ },
+ // {
+ // Cell: ({
+ // row: {
+ // original: { num_tagged_objects: numTaggedObjects },
+ // },
+ // }: any) => numTaggedObjects,
+ // Header: t('Number of Tagged Entities'),
+ // accessor: 'num_tagged_objects',
+ // disableSortBy: true,
+ // },
+ {
+ Cell: ({ row: { original } }: any) => {
+ const handleDelete = () =>
+ handleDashboardDelete(
+ original,
+ refreshData,
+ addSuccessToast,
+ addDangerToast,
+ );
+ const handleEdit = () => openTagEditModal(original);
+
+ return (
+
+ {canDelete && (
+
+ {t('Are you sure you want to delete')}{' '}
+ {original.dashboard_title}?
+ >
+ }
+ onConfirm={handleDelete}
+ >
+ {confirmDelete => (
+
+
+ {/* fix icon name */}
+
+
+
+ )}
+
+ )}
+ {canEdit && (
+
+
+
+
+
+ )}
+
+ );
+ },
+ Header: t('Actions'),
+ id: 'actions',
+ hidden: !canEdit && !canDelete,
+ disableSortBy: true,
+ },
+ ],
+ [
+ userId,
+ canEdit,
+ canDelete,
+ refreshData,
+ addSuccessToast,
+ addDangerToast,
+ ],
+ );
+
+ const filters: Filters = useMemo(() => {
+ const filters_list = [
+ {
+ Header: t('Created by'),
+ id: 'created_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'tag',
+ 'created_by',
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t(
+ 'An error occurred while fetching tag created by values: %s',
+ errMsg,
+ ),
+ ),
+ ),
+ props.user,
+ ),
+ paginate: true,
+ },
+ {
+ Header: t('Search'),
+ id: 'name',
+ input: 'search',
+ operator: FilterOperator.titleOrSlug,
+ }
+ ] as Filters;
+ return filters_list;
+ }, [addDangerToast, props.user]);
+
+ const sortTypes = [
+ {
+ desc: false,
+ id: 'name',
+ label: t('Alphabetical'),
+ value: 'alphabetical',
+ },
+ {
+ desc: true,
+ id: 'changed_on_delta_humanized',
+ label: t('Recently modified'),
+ value: 'recently_modified',
+ },
+ {
+ desc: false,
+ id: 'changed_on_delta_humanized',
+ label: t('Least recently modified'),
+ value: 'least_recently_modified',
+ },
+ ];
+
+ const renderCard = useCallback(
+ (tag: Tag) => (
+
+ ),
+ [
+ addDangerToast,
+ addSuccessToast,
+ bulkSelectEnabled,
+ hasPerm,
+ loading,
+ userId,
+ refreshData,
+ userKey,
+ ],
+ );
+
+ const subMenuButtons: SubMenuProps['buttons'] = [];
+ if (canDelete) {
+ subMenuButtons.push({
+ name: t('Bulk select'),
+ buttonStyle: 'secondary',
+ 'data-test': 'bulk-select',
+ onClick: toggleBulkSelect,
+ });
+ }
+ if (canCreate) {
+ subMenuButtons.push({
+ name: (
+ <>
+ {t('Tag')}
+ >
+ ),
+ buttonStyle: 'primary',
+ onClick: () => {
+ // TODO Add Tags??
+ },
+ });
+ }
+ return (
+ <>
+
+
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ type: 'danger',
+ onSelect: confirmDelete,
+ });
+ }
+ return (
+ <>
+ {tagToEdit && (
+ setTagToEdit(null)}
+ onSubmit={handleTagEdit}
+ />
+ )}
+
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={sortTypes}
+ className="dashboard-list-view"
+ columns={columns}
+ count={tagCount}
+ data={tags}
+ disableBulkSelect={toggleBulkSelect}
+ fetchData={fetchData}
+ filters={filters}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ showThumbnails={
+ userKey
+ ? userKey.thumbnails
+ : isFeatureEnabled(FeatureFlag.THUMBNAILS)
+ }
+ renderCard={renderCard}
+ defaultViewMode={
+ isFeatureEnabled(FeatureFlag.LISTVIEWS_DEFAULT_CARD_VIEW)
+ ? 'card'
+ : 'table'
+ }
+ />
+ >
+ );
+ }}
+
+ >
+ );
+ }
+
+ export default withToasts(TagList);
+
\ No newline at end of file
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 0090697747ac5..3161e255ae49a 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -133,12 +133,15 @@ export enum QueryObjectColumns {
tracking_url = 'tracking_url',
}
-export type ImportResourceName =
- | 'chart'
- | 'dashboard'
- | 'database'
- | 'dataset'
- | 'saved_query';
+export interface Tag {
+ changed_by_name: string;
+ changed_by_url: string;
+ changed_on_delta_humanized: string;
+ changed_by: string;
+ name: string;
+ id: number;
+ created_by: object;
+}
export type DatabaseObject = Partial &
Pick;
diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx
index 64df8743035eb..82f54eff4bef9 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -64,6 +64,10 @@ import { Dashboard, Filters } from './types';
risonRef.next_id = new RegExp(idrx, 'g');
})();
+export const Actions = styled.div`
+ color: ${({ theme }) => theme.colors.grayscale.base};
+`;
+
const createFetchResourceMethod =
(method: string) =>
(
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index 2ab6fc6c48180..7c4c0889173a2 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -106,6 +106,9 @@ const SavedQueryList = lazy(
const AllEntitiesPage = lazy(
() => import(/* webpackChunkName: "AllEntities" */ 'src/views/CRUD/allentities/AllEntities'),
);
+const TagsPage = lazy(
+ () => import(/* webpackChunkName: "TagList" */ 'src/views/CRUD/tags/TagList'),
+);
type Routes = {
path: string;
@@ -200,6 +203,10 @@ if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
path: '/superset/all_entities/',
Component: AllEntitiesPage,
});
+ routes.push({
+ path: '/superset/tags/',
+ Component: TagsPage,
+ });
}
const frontEndRoutes = routes
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index d596ef419d466..74705b49d1e61 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -186,6 +186,7 @@ def init_views(self) -> None:
)
from superset.views.users.api import CurrentUserRestApi
from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
+ from superset.views.tags import TagModelView, TagView
#
@@ -309,6 +310,7 @@ def init_views(self) -> None:
appbuilder.add_view_no_menu(TableSchemaView)
appbuilder.add_view_no_menu(TabStateView)
appbuilder.add_view_no_menu(TaggedObjectView)
+ appbuilder.add_view_no_menu(TagView)
appbuilder.add_view_no_menu(ReportView)
#
@@ -374,7 +376,20 @@ def init_views(self) -> None:
"All Entities",
label=__("All Entities"),
icon="",
- category="",
+ category="Tagging",
+ category_label=__("Tagging"),
+ category_icon="",
+ menu_cond=lambda: feature_flag_manager.is_feature_enabled(
+ "TAGGING_SYSTEM"
+ ),
+ )
+ appbuilder.add_view(
+ TagModelView,
+ "Tags",
+ label=__("Tags"),
+ icon="",
+ category="Tagging",
+ category_label=__("Tagging"),
category_icon="",
menu_cond=lambda: feature_flag_manager.is_feature_enabled(
"TAGGING_SYSTEM"
diff --git a/superset/tags/api.py b/superset/tags/api.py
index ab3d3beb1ddde..473e0e7b16dc4 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -55,13 +55,30 @@ class TagRestApi(BaseSupersetModelRestApi):
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
list_columns = [
- "id",
- "name",
- "type",
+ "id",
+ "name",
+ "type",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_on_delta_humanized",
+ "created_by.first_name",
+ "created_by.last_name",
]
list_select_columns = list_columns
+ show_columns = [
+ "id",
+ "name",
+ "type",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_on_delta_humanized",
+ "created_by.first_name",
+ "created_by.last_name",
+ "created_by"
+ ]
+
add_model_schema = TagPostSchema()
tag_get_response_schema = TagGetResponseSchema()
@@ -76,10 +93,9 @@ class TagRestApi(BaseSupersetModelRestApi):
def __repr__(self) -> str:
"""Deterministic string representation of the API instance for etag_cache."""
- return "Superset.tags.api.TagRestApi@v{}{}".format(
- self.appbuilder.app.config["VERSION_STRING"],
- self.appbuilder.app.config["VERSION_SHA"],
- )
+ return 'Superset.tags.api.TagRestApi@v' \
+ f'{self.appbuilder.app.config["VERSION_STRING"]}' \
+ f'{self.appbuilder.app.config["VERSION_SHA"]}'
@expose("///", methods=["POST"])
@protect()
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index 0c57da097284b..739db5db1f546 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -163,8 +163,6 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
for tag in request.args.get("tags", "").split(",")
if tag
]
- if not tags:
- return json_success(json.dumps([]))
# filter types
types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
@@ -183,8 +181,9 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
+ .filter(not tags or Tag.name.in_(tags))
)
+
results.extend(
{
"id": obj.id,
@@ -210,7 +209,7 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
+ .filter(not tags or Tag.name.in_(tags))
)
results.extend(
{
@@ -237,7 +236,7 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
),
)
.join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
+ .filter(not tags or Tag.name.in_(tags))
)
results.extend(
{
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 5230349ec5f08..e1b87571a045c 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -1,255 +1,109 @@
-# # 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 __future__ import absolute_import, division, print_function, unicode_literals
-
-# from typing import Any, Dict, List
-
-# import simplejson as json
-# from flask import request, Response
-# from flask_appbuilder import expose
-# from flask_appbuilder.hooks import before_request
-# from flask_appbuilder.models.sqla.interface import SQLAInterface
-# from flask_appbuilder.security.decorators import has_access, has_access_api
-# from jinja2.sandbox import SandboxedEnvironment
-# from sqlalchemy import and_, func
-# from werkzeug.exceptions import NotFound
-
-# from superset import db, is_feature_enabled, utils
-# from superset.jinja_context import ExtraCache
-# from superset.models.dashboard import Dashboard
-# from superset.models.slice import Slice
-# from superset.models.sql_lab import SavedQuery
-# from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
-# from superset.superset_typing import FlaskResponse
-# from superset.views.base import SupersetModelView
-
-# from .base import BaseSupersetView, json_success
-
-
-# def process_template(content: str) -> str:
-# env = SandboxedEnvironment()
-# template = env.from_string(content)
-# context = {
-# "current_user_id": ExtraCache.current_user_id,
-# "current_username": ExtraCache.current_username,
-# }
-# return template.render(context)
-
-# class TagModelView(SupersetModelView):
-# route_base = "/superset/tags"
-# datamodel = SQLAInterface(Tag)
-# class_permission_name = "Tags"
-
-# @has_access
-# @expose("/")
-# def list(self) -> FlaskResponse:
-# if not is_feature_enabled("TAGGING_SYSTEM"):
-# return super().list()
-
-# return super().render_app_template()
-
-# class TagView(BaseSupersetView):
-# @staticmethod
-# def is_enabled() -> bool:
-# return is_feature_enabled("TAGGING_SYSTEM")
-
-# @before_request
-# def ensure_enabled(self) -> None:
-# if not self.is_enabled():
-# raise NotFound()
-
-# @has_access_api
-# @expose("/tags/suggestions/", methods=["GET"])
-# def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
-# query = (
-# db.session.query(TaggedObject)
-# .join(Tag)
-# .with_entities(TaggedObject.tag_id, Tag.name)
-# .group_by(TaggedObject.tag_id, Tag.name)
-# .order_by(func.count().desc())
-# .all()
-# )
-# tags = [{"id": id, "name": name} for id, name in query]
-# return json_success(json.dumps(tags))
-
-# @has_access_api
-# @expose("/tags///", methods=["GET"])
-# def get( # pylint: disable=no-self-use
-# self, object_type: ObjectTypes, object_id: int
-# ) -> FlaskResponse:
-# """List all tags a given object has."""
-# if object_id == 0:
-# return json_success(json.dumps([]))
-
-# query = db.session.query(TaggedObject).filter(
-# and_(
-# TaggedObject.object_type == object_type,
-# TaggedObject.object_id == object_id,
-# )
-# )
-# tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
-# return json_success(json.dumps(tags))
-
-# @has_access_api
-# @expose("/tags///", methods=["POST"])
-# def post( # pylint: disable=no-self-use
-# self, object_type: ObjectTypes, object_id: int
-# ) -> FlaskResponse:
-# """Add new tags to an object."""
-# if object_id == 0:
-# return Response(status=404)
-
-# tagged_objects = []
-# for name in request.get_json(force=True):
-# if ":" in name:
-# type_name = name.split(":", 1)[0]
-# type_ = TagTypes[type_name]
-# else:
-# type_ = TagTypes.custom
-
-# tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
-# if not tag:
-# tag = Tag(name=name, type=type_)
-
-# tagged_objects.append(
-# TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
-# )
-
-# db.session.add_all(tagged_objects)
-# db.session.commit()
-
-# return Response(status=201) # 201 CREATED
-
-# @has_access_api
-# @expose("/tags///", methods=["DELETE"])
-# def delete( # pylint: disable=no-self-use
-# self, object_type: ObjectTypes, object_id: int
-# ) -> FlaskResponse:
-# """Remove tags from an object."""
-# tag_names = request.get_json(force=True)
-# if not tag_names:
-# return Response(status=403)
-
-# db.session.query(TaggedObject).filter(
-# and_(
-# TaggedObject.object_type == object_type,
-# TaggedObject.object_id == object_id,
-# ),
-# TaggedObject.tag.has(Tag.name.in_(tag_names)),
-# ).delete(synchronize_session=False)
-# db.session.commit()
-
-# return Response(status=204) # 204 NO CONTENT
-
-# @has_access_api
-# @expose("/tagged_objects/", methods=["GET", "POST"])
-# def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
-# tags = [
-# process_template(tag)
-# for tag in request.args.get("tags", "").split(",")
-# if tag
-# ]
-# if not tags:
-# return json_success(json.dumps([]))
-
-# # filter types
-# types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
-
-# results: List[Dict[str, Any]] = []
-
-# # dashboards
-# if not types or "dashboard" in types:
-# dashboards = (
-# db.session.query(Dashboard)
-# .join(
-# TaggedObject,
-# and_(
-# TaggedObject.object_id == Dashboard.id,
-# TaggedObject.object_type == ObjectTypes.dashboard,
-# ),
-# )
-# .join(Tag, TaggedObject.tag_id == Tag.id)
-# .filter(Tag.name.in_(tags))
-# )
-# results.extend(
-# {
-# "id": obj.id,
-# "type": ObjectTypes.dashboard.name,
-# "name": obj.dashboard_title,
-# "url": obj.url,
-# "changed_on": obj.changed_on,
-# "created_by": obj.created_by_fk,
-# "creator": obj.creator(),
-# }
-# for obj in dashboards
-# )
-
-# # charts
-# if not types or "chart" in types:
-# charts = (
-# db.session.query(Slice)
-# .join(
-# TaggedObject,
-# and_(
-# TaggedObject.object_id == Slice.id,
-# TaggedObject.object_type == ObjectTypes.chart,
-# ),
-# )
-# .join(Tag, TaggedObject.tag_id == Tag.id)
-# .filter(Tag.name.in_(tags))
-# )
-# results.extend(
-# {
-# "id": obj.id,
-# "type": ObjectTypes.chart.name,
-# "name": obj.slice_name,
-# "url": obj.url,
-# "changed_on": obj.changed_on,
-# "created_by": obj.created_by_fk,
-# "creator": obj.creator(),
-# }
-# for obj in charts
-# )
-
-# # saved queries
-# if not types or "query" in types:
-# saved_queries = (
-# db.session.query(SavedQuery)
-# .join(
-# TaggedObject,
-# and_(
-# TaggedObject.object_id == SavedQuery.id,
-# TaggedObject.object_type == ObjectTypes.query,
-# ),
-# )
-# .join(Tag, TaggedObject.tag_id == Tag.id)
-# .filter(Tag.name.in_(tags))
-# )
-# results.extend(
-# {
-# "id": obj.id,
-# "type": ObjectTypes.query.name,
-# "name": obj.label,
-# "url": obj.url(),
-# "changed_on": obj.changed_on,
-# "created_by": obj.created_by_fk,
-# "creator": obj.creator(),
-# }
-# for obj in saved_queries
-# )
-
-# return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
+# 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 __future__ import absolute_import, division, print_function, unicode_literals
+
+from typing import Any, Dict, List
+
+import simplejson as json
+from flask import request, Response
+from flask_appbuilder import expose
+from flask_appbuilder.hooks import before_request
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from flask_appbuilder.security.decorators import has_access, has_access_api
+from jinja2.sandbox import SandboxedEnvironment
+from sqlalchemy import and_, func
+from werkzeug.exceptions import NotFound
+
+from superset import db, is_feature_enabled, utils
+from superset.jinja_context import ExtraCache
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from superset.models.sql_lab import SavedQuery
+from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.superset_typing import FlaskResponse
+from superset.views.base import SupersetModelView
+
+from .base import BaseSupersetView, json_success
+
+
+def process_template(content: str) -> str:
+ env = SandboxedEnvironment()
+ template = env.from_string(content)
+ context = {
+ "current_user_id": ExtraCache.current_user_id,
+ "current_username": ExtraCache.current_username,
+ }
+ return template.render(context)
+
+class TagModelView(SupersetModelView):
+ route_base = "/superset/tags"
+ datamodel = SQLAInterface(Tag)
+ class_permission_name = "Tags"
+
+ @has_access
+ @expose("/")
+ def list(self) -> FlaskResponse:
+ if not is_feature_enabled("TAGGING_SYSTEM"):
+ return super().list()
+
+ return super().render_app_template()
+
+class TagView(BaseSupersetView):
+ @staticmethod
+ def is_enabled() -> bool:
+ return is_feature_enabled("TAGGING_SYSTEM")
+
+ @before_request
+ def ensure_enabled(self) -> None:
+ if not self.is_enabled():
+ raise NotFound()
+
+ @has_access_api
+ @expose("/tags/", methods=["GET"])
+ def tags(self) -> FlaskResponse: # pylint: disable=no-self-use
+ query = (
+ db.session.query(Tag)
+ # .with_entities(Tag.name, Tag.id)
+ # .group_by(TaggedObject.tag_id, Tag.name)
+ # .order_by(func.count().desc())
+ .all()
+ )
+ results = [
+ {
+ "id": obj.id,
+ "type": obj.type.name,
+ "name": obj.name,
+ "changed_on": obj.changed_on,
+ "changed_by": obj.changed_by_fk,
+ "created_by": obj.created_by_fk,
+ } for obj in query
+ ]
+ return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
+
+ @has_access_api
+ @expose("/tags/suggestions/", methods=["GET"])
+ def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
+ query = (
+ db.session.query(TaggedObject)
+ .join(Tag)
+ .with_entities(TaggedObject.tag_id, Tag.name)
+ .group_by(TaggedObject.tag_id, Tag.name)
+ .order_by(func.count().desc())
+ .all()
+ )
+ tags = [{"id": id, "name": name} for id, name in query]
+ return json_success(json.dumps(tags))
From abd2499a775e0fcad1bedad357568bdb7e55bf14 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 5 Oct 2022 07:21:45 -0400
Subject: [PATCH 35/76] added TODOs for add/edit/delete tags in CRUD view
---
.../src/views/CRUD/tags/TagList.tsx | 69 ++-----------------
1 file changed, 6 insertions(+), 63 deletions(-)
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 09d1751fba8d4..844ad8c51cdf1 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -99,72 +99,15 @@ import { Link } from 'react-router-dom';
}
function handleTagEdit(edits: Tag) {
- return SupersetClient.get({
- endpoint: `/api/v1/tag/${edits.id}`,
- }).then(
- ({ json = {} }) => {
- setTags(
- tags.map(tag => {
- if (tag.id === json?.result?.id) {
- const {
- changed_by_name,
- changed_by_url,
- changed_by,
- dashboard_title = '',
- slug = '',
- json_metadata = '',
- changed_on_delta_humanized,
- url = '',
- certified_by = '',
- certification_details = '',
- owners,
- tags,
- } = json.result;
- return {
- ...tag,
- changed_by_name,
- changed_by_url,
- changed_by,
- dashboard_title,
- slug,
- json_metadata,
- changed_on_delta_humanized,
- url,
- certified_by,
- certification_details,
- owners,
- tags,
- };
- }
- return tag;
- }),
- );
- },
- createErrorHandler(errMsg =>
- addDangerToast(
- t('An error occurred while fetching dashboards: %s', errMsg),
- ),
- ),
- );
+ /* TODO:
+ what permissions need to be checked here?
+ */
+ return
}
function handleBulkTagDelete(tagsToDelete: Tag[]) {
- // TODO fix bulk tag delete
- return SupersetClient.delete({
- endpoint: `/api/v1/tag/?q=${rison.encode(
- tagsToDelete.map(({ id }) => id),
- )}`,
- }).then(
- ({ json = {} }) => {
- refreshData();
- addSuccessToast(json.message);
- },
- createErrorHandler(errMsg =>
- addDangerToast(
- t('There was an issue deleting the selected tags: ', errMsg),
- ),
- ),
- );
+ // TODO what permissions need to be checked here?
+ return
}
const columns = useMemo(
From fc156488ee5cb60d6007c147b7b9e0d81bc104ba Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 1 Dec 2022 13:52:56 -0500
Subject: [PATCH 36/76] fixed imports from tags models refactoring
---
superset/models/dashboard.py | 3 +--
superset/models/slice.py | 3 +--
superset/models/sql_lab.py | 3 +--
superset/tags/api.py | 2 +-
superset/tags/commands/create.py | 2 +-
superset/tags/dao.py | 2 +-
superset/views/all_entities.py | 2 +-
superset/views/base_api.py | 2 +-
8 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 4c41e1430c274..393a3df12cce4 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -53,7 +53,6 @@
from superset.models.filter_set import FilterSet
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
from superset.models.slice import Slice
-from superset.models.tags import DashboardUpdater, Tag
from superset.models.user_attributes import UserAttribute
from superset.tasks.thumbnails import cache_dashboard_thumbnail
from superset.utils import core as utils
@@ -150,7 +149,7 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
)
owners = relationship(security_manager.user_model, secondary=dashboard_user)
tags = relationship(
- Tag,
+ "Tag",
secondary="tagged_object",
primaryjoin="and_(Dashboard.id == TaggedObject.object_id)",
secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
diff --git a/superset/models/slice.py b/superset/models/slice.py
index 901583961db11..8d65b563d155b 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -42,7 +42,6 @@
from superset import db, is_feature_enabled, security_manager
from superset.legacy import update_time_range
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
-from superset.models.tags import ChartUpdater, Tag
from superset.tasks.thumbnails import cache_chart_thumbnail
from superset.utils import core as utils
from superset.utils.hashing import md5_sha_from_str
@@ -99,7 +98,7 @@ class Slice( # pylint: disable=too-many-public-methods
)
owners = relationship(security_manager.user_model, secondary=slice_user)
tags = relationship(
- Tag,
+ "Tag",
secondary="tagged_object",
primaryjoin="and_(Slice.id == TaggedObject.object_id)",
secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index cdd88c34c6def..1d7dab2677f1f 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -49,7 +49,6 @@
ExtraJSONMixin,
ImportExportMixin,
)
-from superset.models.tags import QueryUpdater, Tag
from superset.sql_parse import CtasMethod, ParsedQuery, Table
from superset.sqllab.limiting_factor import LimitingFactor
from superset.utils.core import GenericDataType, QueryStatus, user_label
@@ -366,7 +365,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
rows = Column(Integer, nullable=True)
last_run = Column(DateTime, nullable=True)
tags = relationship(
- Tag,
+ "Tag",
secondary="tagged_object",
primaryjoin="and_(SavedQuery.id == TaggedObject.object_id)",
secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 473e0e7b16dc4..22b23518cb76d 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -24,7 +24,7 @@
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.extensions import event_logger
-from superset.models.tags import ObjectTypes, Tag
+from superset.tags.models import ObjectTypes, Tag
from superset.tags.commands.create import CreateTagCommand
from superset.tags.commands.exceptions import (
TagCreateFailedError,
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index ecee9de7502db..c02a248d6a575 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -21,7 +21,7 @@
from superset.commands.base import BaseCommand, CreateMixin
from superset.dao.exceptions import DAOCreateFailedError
-from superset.models.tags import ObjectTypes
+from superset.tags.models import ObjectTypes
from superset.tags.commands.exceptions import (
TagCreateFailedError,
TagInvalidError,
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index 7e2e0985a40dc..e043ccc910a8e 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -20,7 +20,7 @@
from superset import security_manager
from superset.dao.base import BaseDAO
from superset.extensions import db
-from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
logger = logging.getLogger(__name__)
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index 739db5db1f546..17f292a9bd449 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -33,7 +33,7 @@
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
-from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
from superset.superset_typing import FlaskResponse
from superset.views.base import SupersetModelView
diff --git a/superset/views/base_api.py b/superset/views/base_api.py
index c9249ff847cdf..ad4299762e0c4 100644
--- a/superset/views/base_api.py
+++ b/superset/views/base_api.py
@@ -35,7 +35,7 @@
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
-from superset.models.tags import Tag
+from superset.tags.models import Tag
from superset.schemas import error_payload_content
from superset.sql_lab import Query as SqllabQuery
from superset.stats_logger import BaseStatsLogger
From 7a2144bd48f180c823a9b26e03885448366e793b Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 1 Dec 2022 15:31:28 -0500
Subject: [PATCH 37/76] fixed initialization and tag list view
---
.../src/components/ObjectTags/index.tsx | 4 +-
.../components/PropertiesModal/index.tsx | 4 +-
.../components/PropertiesModal/index.tsx | 4 +-
superset-frontend/src/tags.ts | 40 +-
.../src/views/CRUD/tags/TagCard.tsx | 248 +++---
.../src/views/CRUD/tags/TagList.tsx | 712 ++++++++----------
superset/initialization/__init__.py | 19 -
superset/views/tags.py | 22 +-
8 files changed, 488 insertions(+), 565 deletions(-)
diff --git a/superset-frontend/src/components/ObjectTags/index.tsx b/superset-frontend/src/components/ObjectTags/index.tsx
index 88dfe69ba58e7..3e219a405236b 100644
--- a/superset-frontend/src/components/ObjectTags/index.tsx
+++ b/superset-frontend/src/components/ObjectTags/index.tsx
@@ -29,7 +29,7 @@ import {
ClientErrorObject,
getClientErrorObject,
} from 'src/utils/getClientErrorObject';
-import { deleteTag, fetchTags } from 'src/tags';
+import { deleteTaggedObjects, fetchTags } from 'src/tags';
export interface ObjectTagsProps {
objectType: string;
@@ -137,7 +137,7 @@ export const ObjectTags = ({
}, [objectType, objectId, includeTypes]);
const onDelete = (tagIndex: number) => {
- deleteTag(
+ deleteTaggedObjects(
{ objectType, objectId },
tags[tagIndex],
() => setTags(tags.filter((_, i) => i !== tagIndex)),
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 7ed82bccce2ab..3f4856fa24770 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -43,7 +43,7 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { loadTags } from 'src/components/ObjectTags';
import TagType from 'src/types/TagType';
-import { addTag, deleteTag, fetchTags, OBJECT_TYPES } from 'src/tags';
+import { addTag, deleteTaggedObjects, fetchTags, OBJECT_TYPES } from 'src/tags';
import { TagsList } from 'src/components/Tags';
const StyledFormItem = styled(FormItem)`
@@ -606,7 +606,7 @@ const PropertiesModal = ({
// delete tags that are in old tags, but not in new tags
oldTags.map((tag: TagType) => {
if (!newTags.some(t => t.name === tag.name)) {
- deleteTag(
+ deleteTaggedObjects(
{
objectType: OBJECT_TYPES.DASHBOARD,
objectId: dashboardId,
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index a122889d686be..31e24d3db521a 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -34,7 +34,7 @@ import Chart, { Slice } from 'src/types/Chart';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import withToasts from 'src/components/MessageToasts/withToasts';
import { loadTags } from 'src/components/ObjectTags';
-import { addTag, deleteTag, fetchTags, OBJECT_TYPES } from 'src/tags';
+import { addTag, deleteTaggedObjects, fetchTags, OBJECT_TYPES } from 'src/tags';
import TagType from 'src/types/TagType';
import { TagsList } from 'src/components/Tags';
@@ -264,7 +264,7 @@ function PropertiesModal({
// delete tags that are in old tags, but not in new tags
oldTags.map((tag: TagType) => {
if (!newTags.some(t => t.name === tag.name)) {
- deleteTag(
+ deleteTaggedObjects(
{
objectType: OBJECT_TYPES.CHART,
objectId: slice.slice_id,
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index 4d0a1580c1125..cf5a3432623ad 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -29,13 +29,12 @@ export function fetchAllTags(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- let url = `/tagview/tags/`;
+ const url = `/tagview/tags/`;
SupersetClient.get({ endpoint: url })
.then(({ json }) => callback(json))
.catch(response => error(response));
}
-
export function fetchTags(
{
objectType,
@@ -48,7 +47,9 @@ export function fetchTags(
if (objectType === undefined || objectId === undefined) {
throw new Error('Need to specify objectType and objectId');
}
- SupersetClient.get({ endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/` })
+ SupersetClient.get({
+ endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
+ })
.then(({ json }) =>
callback(
json.filter((tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes),
@@ -71,11 +72,11 @@ export function fetchSuggestions(
.catch(response => error(response));
}
-export function deleteTag(
+export function deleteTaggedObjects(
{ objectType, objectId }: { objectType: string; objectId: number },
tag: Tag,
callback: (text: string) => void,
- error: (response: Response) => void,
+ error: (response: string) => void,
) {
if (objectType === undefined || objectId === undefined) {
throw new Error('Need to specify objectType and objectId');
@@ -85,8 +86,33 @@ export function deleteTag(
body: JSON.stringify([tag.name]),
parseMethod: 'text',
})
- .then(({ text }) => callback(text))
- .catch(response => error(response));
+ .then(({ text }) =>
+ text ? callback(text) : callback('Successfully Deleted Tagged Objects'),
+ )
+ .catch(response => {
+ const err_str = response.message;
+ return err_str ? error(err_str) : error('Error Deleting Tagged Objects');
+ });
+}
+
+export function deleteTags(
+ tags: Tag[],
+ callback: (text: string) => void,
+ error: (response: string) => void,
+) {
+ const tags_str = JSON.stringify(tags.map(tag => tag.name) as string[]);
+ SupersetClient.delete({
+ endpoint: `/tagview/tags`,
+ body: tags_str,
+ parseMethod: 'text',
+ })
+ .then(({ text }) =>
+ text ? callback(text) : callback('Successfully Deleted Tag'),
+ )
+ .catch(response => {
+ const err_str = response.message;
+ return err_str ? error(err_str) : error('Error Deleting Tag');
+ });
}
export function addTag(
diff --git a/superset-frontend/src/views/CRUD/tags/TagCard.tsx b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
index ed4d82eba7150..617559a298070 100644
--- a/superset-frontend/src/views/CRUD/tags/TagCard.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
@@ -16,137 +16,117 @@
* specific language governing permissions and limitations
* under the License.
*/
- import React from 'react';
- import { Link, useHistory } from 'react-router-dom';
- import { t, useTheme } from '@superset-ui/core';
- import { handleDashboardDelete, CardStyles } from 'src/views/CRUD/utils';
- import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
- import { AntdDropdown } from 'src/components';
- import { Menu } from 'src/components/Menu';
- import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
- import ListViewCard from 'src/components/ListViewCard';
- import Icons from 'src/components/Icons';
- import Label from 'src/components/Label';
- import FacePile from 'src/components/FacePile';
- import FaveStar from 'src/components/FaveStar';
- import { Tag } from 'src/views/CRUD/types';
-
- interface TagCardProps {
- tag: Tag;
- hasPerm: (name: string) => boolean;
- bulkSelectEnabled: boolean;
- refreshData: () => void;
- loading: boolean;
- addDangerToast: (msg: string) => void;
- addSuccessToast: (msg: string) => void;
- openTagEditModal?: (t: Tag) => void;
- tagFilter?: string;
- userId?: string | number;
- showThumbnails?: boolean;
- }
-
- function TagCard({
- tag,
- hasPerm,
- bulkSelectEnabled,
- tagFilter,
- refreshData,
- userId,
- addDangerToast,
- addSuccessToast,
- openTagEditModal,
- showThumbnails,
- }: TagCardProps) {
- const history = useHistory();
- const canEdit = hasPerm('can_write');
- const canDelete = hasPerm('can_write');
- const canExport = hasPerm('can_export');
-
- const theme = useTheme();
- const menu = (
-
- );
- return (
-
- >
- ) : null
- }
- url={undefined}
- linkComponent={Link}
- imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
- description={t('Modified %s', tag.changed_on)}
- actions={
- {
- e.stopPropagation();
- e.preventDefault();
- }}
- >
-
-
-
-
- }
- />
-
- );
- }
-
- export default TagCard;
-
\ No newline at end of file
+import React from 'react';
+import { Link, useHistory } from 'react-router-dom';
+import { t, useTheme } from '@superset-ui/core';
+import { handleDashboardDelete, CardStyles } from 'src/views/CRUD/utils';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
+import { AntdDropdown } from 'src/components';
+import { Menu } from 'src/components/Menu';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import ListViewCard from 'src/components/ListViewCard';
+import Icons from 'src/components/Icons';
+import Label from 'src/components/Label';
+import FacePile from 'src/components/FacePile';
+import FaveStar from 'src/components/FaveStar';
+import { Tag } from 'src/views/CRUD/types';
+
+interface TagCardProps {
+ tag: Tag;
+ hasPerm: (name: string) => boolean;
+ bulkSelectEnabled: boolean;
+ refreshData: () => void;
+ loading: boolean;
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ tagFilter?: string;
+ userId?: string | number;
+ showThumbnails?: boolean;
+}
+
+function TagCard({
+ tag,
+ hasPerm,
+ bulkSelectEnabled,
+ tagFilter,
+ refreshData,
+ userId,
+ addDangerToast,
+ addSuccessToast,
+ showThumbnails,
+}: TagCardProps) {
+ const history = useHistory();
+ const canDelete = hasPerm('can_write');
+ const canExport = hasPerm('can_export');
+
+ const theme = useTheme();
+ const menu = (
+
+ );
+ return (
+
+ >
+ ) : null
+ }
+ url={undefined}
+ linkComponent={Link}
+ imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
+ description={t('Modified %s', tag.changed_on)}
+ actions={
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ >
+
+
+
+
+ }
+ />
+
+ );
+}
+
+export default TagCard;
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 844ad8c51cdf1..3c6ca081a1843 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -16,410 +16,326 @@
* specific language governing permissions and limitations
* under the License.
*/
- import { SupersetClient, t } from '@superset-ui/core';
- import React, { useState, useMemo, useCallback } from 'react';
- import rison from 'rison';
- import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
- import {
- createFetchRelated,
- createErrorHandler,
- handleDashboardDelete,
- Actions,
- } from 'src/views/CRUD/utils';
- import { useListViewResource } from 'src/views/CRUD/hooks';
- import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
- import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
- import ListView, {
- ListViewProps,
- Filters,
- FilterOperator,
- } from 'src/components/ListView';
- import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
- import withToasts from 'src/components/MessageToasts/withToasts';
- import Icons from 'src/components/Icons';
- import PropertiesModal from 'src/dashboard/components/PropertiesModal';
- import { Tooltip } from 'src/components/Tooltip';
- import TagCard from './TagCard';
-import { Tag } from '../types';
+import { t } from '@superset-ui/core';
+import React, { useMemo, useCallback } from 'react';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
+import {
+ createFetchRelated,
+ createErrorHandler,
+ Actions,
+} from 'src/views/CRUD/utils';
+import { useListViewResource } from 'src/views/CRUD/hooks';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
+import ListView, {
+ ListViewProps,
+ Filters,
+ FilterOperator,
+} from 'src/components/ListView';
+import { dangerouslyGetItemDoNotUse } from 'src/utils/localStorageHelpers';
+import withToasts from 'src/components/MessageToasts/withToasts';
+import Icons from 'src/components/Icons';
+import { Tooltip } from 'src/components/Tooltip';
import FacePile from 'src/components/FacePile';
import { Link } from 'react-router-dom';
-
- const PAGE_SIZE = 25;
-
- interface TagListProps {
- addDangerToast: (msg: string) => void;
- addSuccessToast: (msg: string) => void;
- user: {
- userId: string | number;
- firstName: string;
- lastName: string;
- };
- }
-
- function TagList(props: TagListProps) {
- const {
- addDangerToast,
- addSuccessToast,
- user: { userId },
- } = props;
-
- const {
- state: {
- loading,
- resourceCount: tagCount,
- resourceCollection: tags,
- bulkSelectEnabled,
- },
- setResourceCollection: setTags,
- hasPerm,
- fetchData,
- toggleBulkSelect,
- refreshData,
- } = useListViewResource(
- 'tag',
- t('tag'),
- addDangerToast,
- );
-
- const [tagToEdit, setTagToEdit] = useState(
- null,
- );
-
- // TODO: Fix usage of localStorage keying on the user id
- const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
-
- const canCreate = hasPerm('can_write');
- const canEdit = hasPerm('can_write');
- const canDelete = hasPerm('can_write');
+import { deleteTags } from 'src/tags';
+import { Tag } from '../types';
+import TagCard from './TagCard';
+
+const PAGE_SIZE = 25;
+
+interface TagListProps {
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ user: {
+ userId: string | number;
+ firstName: string;
+ lastName: string;
+ };
+}
+
+function TagList(props: TagListProps) {
+ const {
+ addDangerToast,
+ addSuccessToast,
+ user: { userId },
+ } = props;
+
+ const {
+ state: {
+ loading,
+ resourceCount: tagCount,
+ resourceCollection: tags,
+ bulkSelectEnabled,
+ },
+ setResourceCollection: setTags,
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource('tag', t('tag'), addDangerToast);
+
+ // TODO: Fix usage of localStorage keying on the user id
+ const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
+
+ const canDelete = hasPerm('can_write');
+
+ const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
- const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-
- function openTagEditModal(tag: Tag) {
- setTagToEdit(tag);
- }
-
- function handleTagEdit(edits: Tag) {
- /* TODO:
- what permissions need to be checked here?
- */
- return
- }
-
- function handleBulkTagDelete(tagsToDelete: Tag[]) {
- // TODO what permissions need to be checked here?
- return
- }
-
- const columns = useMemo(
- () => [
- {
- Cell: ({
- row: {
- original: {
- name: tagName,
- },
- },
- }: any) => (
-
- {tagName}
-
+ function handleTagsDelete(
+ tags: Tag[],
+ callback: (text: string) => void,
+ error: (text: string) => void,
+ ) {
+ // TODO what permissions need to be checked here?
+ deleteTags(tags, callback, error);
+ refreshData();
+ }
+
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { name: tagName },
+ },
+ }: any) => (
+ {tagName}
),
- Header: t('Name'),
- accessor: 'name',
- },
- {
- Cell: ({
- row: {
- original: {
- changed_by_name: changedByName,
- changed_by_url: changedByUrl,
- },
- },
- }: any) => {changedByName},
- Header: t('Modified by'),
- accessor: 'changed_by.first_name',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => {changedOn},
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { created_by: createdBy },
- },
- }: any) =>
- createdBy ? : '',
- Header: t('Created by'),
- accessor: 'created_by',
- disableSortBy: true,
- size: 'xl',
- },
- // {
- // Cell: ({
- // row: {
- // original: { num_tagged_objects: numTaggedObjects },
- // },
- // }: any) => numTaggedObjects,
- // Header: t('Number of Tagged Entities'),
- // accessor: 'num_tagged_objects',
- // disableSortBy: true,
- // },
- {
- Cell: ({ row: { original } }: any) => {
- const handleDelete = () =>
- handleDashboardDelete(
- original,
- refreshData,
- addSuccessToast,
- addDangerToast,
- );
- const handleEdit = () => openTagEditModal(original);
-
- return (
-
- {canDelete && (
-
- {t('Are you sure you want to delete')}{' '}
- {original.dashboard_title}?
- >
- }
- onConfirm={handleDelete}
- >
- {confirmDelete => (
-
-
+ Header: t('Name'),
+ accessor: 'name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by_name: changedByName,
+ changed_by_url: changedByUrl,
+ },
+ },
+ }: any) => {changedByName},
+ Header: t('Modified by'),
+ accessor: 'changed_by.first_name',
+ size: 'xl',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on_delta_humanized: changedOn },
+ },
+ }: any) => {changedOn},
+ Header: t('Modified'),
+ accessor: 'changed_on_delta_humanized',
+ size: 'xl',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { created_by: createdBy },
+ },
+ }: any) => (createdBy ? : ''),
+ Header: t('Created by'),
+ accessor: 'created_by',
+ disableSortBy: true,
+ size: 'xl',
+ },
+ {
+ Cell: ({ row: { original } }: any) => {
+ const handleDelete = () =>
+ handleTagsDelete([original], addSuccessToast, addDangerToast);
+ return (
+
+ {canDelete && (
+
+ {t('Are you sure you want to delete')}{' '}
+ {original.dashboard_title}?
+ >
+ }
+ onConfirm={handleDelete}
+ >
+ {confirmDelete => (
+
+
{/* fix icon name */}
-
-
-
- )}
-
- )}
- {canEdit && (
-
-
-
-
-
- )}
-
- );
- },
- Header: t('Actions'),
- id: 'actions',
- hidden: !canEdit && !canDelete,
- disableSortBy: true,
- },
- ],
- [
- userId,
- canEdit,
- canDelete,
- refreshData,
- addSuccessToast,
- addDangerToast,
- ],
- );
-
- const filters: Filters = useMemo(() => {
- const filters_list = [
- {
- Header: t('Created by'),
- id: 'created_by',
- input: 'select',
- operator: FilterOperator.relationOneMany,
- unfilteredLabel: t('All'),
- fetchSelects: createFetchRelated(
- 'tag',
- 'created_by',
- createErrorHandler(errMsg =>
- addDangerToast(
- t(
- 'An error occurred while fetching tag created by values: %s',
- errMsg,
- ),
- ),
- ),
- props.user,
- ),
- paginate: true,
- },
- {
+
+
+
+ )}
+
+ )}
+
+ );
+ },
+ Header: t('Actions'),
+ id: 'actions',
+ hidden: !canDelete,
+ disableSortBy: true,
+ },
+ ],
+ [userId, canDelete, refreshData, addSuccessToast, addDangerToast],
+ );
+
+ const filters: Filters = useMemo(() => {
+ const filters_list = [
+ {
+ Header: t('Created by'),
+ id: 'created_by',
+ input: 'select',
+ operator: FilterOperator.relationOneMany,
+ unfilteredLabel: t('All'),
+ fetchSelects: createFetchRelated(
+ 'tag',
+ 'created_by',
+ createErrorHandler(errMsg =>
+ addDangerToast(
+ t(
+ 'An error occurred while fetching tag created by values: %s',
+ errMsg,
+ ),
+ ),
+ ),
+ props.user,
+ ),
+ paginate: true,
+ },
+ {
Header: t('Search'),
id: 'name',
input: 'search',
operator: FilterOperator.titleOrSlug,
- }
- ] as Filters;
- return filters_list;
- }, [addDangerToast, props.user]);
-
- const sortTypes = [
- {
- desc: false,
- id: 'name',
- label: t('Alphabetical'),
- value: 'alphabetical',
- },
- {
- desc: true,
- id: 'changed_on_delta_humanized',
- label: t('Recently modified'),
- value: 'recently_modified',
- },
- {
- desc: false,
- id: 'changed_on_delta_humanized',
- label: t('Least recently modified'),
- value: 'least_recently_modified',
- },
- ];
-
- const renderCard = useCallback(
- (tag: Tag) => (
-
- ),
- [
- addDangerToast,
- addSuccessToast,
- bulkSelectEnabled,
- hasPerm,
- loading,
- userId,
- refreshData,
- userKey,
- ],
- );
-
- const subMenuButtons: SubMenuProps['buttons'] = [];
- if (canDelete) {
- subMenuButtons.push({
- name: t('Bulk select'),
- buttonStyle: 'secondary',
- 'data-test': 'bulk-select',
- onClick: toggleBulkSelect,
- });
- }
- if (canCreate) {
- subMenuButtons.push({
- name: (
- <>
- {t('Tag')}
- >
- ),
- buttonStyle: 'primary',
- onClick: () => {
- // TODO Add Tags??
- },
- });
- }
- return (
- <>
-
-
- {confirmDelete => {
- const bulkActions: ListViewProps['bulkActions'] = [];
- if (canDelete) {
- bulkActions.push({
- key: 'delete',
- name: t('Delete'),
- type: 'danger',
- onSelect: confirmDelete,
- });
- }
- return (
- <>
- {tagToEdit && (
- setTagToEdit(null)}
- onSubmit={handleTagEdit}
- />
- )}
-
- bulkActions={bulkActions}
- bulkSelectEnabled={bulkSelectEnabled}
- cardSortSelectOptions={sortTypes}
- className="dashboard-list-view"
- columns={columns}
- count={tagCount}
- data={tags}
- disableBulkSelect={toggleBulkSelect}
- fetchData={fetchData}
- filters={filters}
- initialSort={initialSort}
- loading={loading}
- pageSize={PAGE_SIZE}
- showThumbnails={
- userKey
- ? userKey.thumbnails
- : isFeatureEnabled(FeatureFlag.THUMBNAILS)
- }
- renderCard={renderCard}
- defaultViewMode={
- isFeatureEnabled(FeatureFlag.LISTVIEWS_DEFAULT_CARD_VIEW)
- ? 'card'
- : 'table'
- }
- />
- >
- );
- }}
-
- >
- );
- }
-
- export default withToasts(TagList);
-
\ No newline at end of file
+ },
+ ] as Filters;
+ return filters_list;
+ }, [addDangerToast, props.user]);
+
+ const sortTypes = [
+ {
+ desc: false,
+ id: 'name',
+ label: t('Alphabetical'),
+ value: 'alphabetical',
+ },
+ {
+ desc: true,
+ id: 'changed_on_delta_humanized',
+ label: t('Recently modified'),
+ value: 'recently_modified',
+ },
+ {
+ desc: false,
+ id: 'changed_on_delta_humanized',
+ label: t('Least recently modified'),
+ value: 'least_recently_modified',
+ },
+ ];
+
+ const renderCard = useCallback(
+ (tag: Tag) => (
+
+ ),
+ [
+ addDangerToast,
+ addSuccessToast,
+ bulkSelectEnabled,
+ hasPerm,
+ loading,
+ userId,
+ refreshData,
+ userKey,
+ ],
+ );
+
+ const subMenuButtons: SubMenuProps['buttons'] = [];
+ if (canDelete) {
+ subMenuButtons.push({
+ name: t('Bulk select'),
+ buttonStyle: 'secondary',
+ 'data-test': 'bulk-select',
+ onClick: toggleBulkSelect,
+ });
+ }
+
+ const handleBulkDelete = (tagsToDelete: Tag[]) =>
+ handleTagsDelete(tagsToDelete, addSuccessToast, addDangerToast);
+
+ return (
+ <>
+
+
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ type: 'danger',
+ onSelect: confirmDelete,
+ });
+ }
+ return (
+ <>
+
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={sortTypes}
+ className="dashboard-list-view"
+ columns={columns}
+ count={tagCount}
+ data={tags.filter(tag => !tag.name.includes(':'))}
+ disableBulkSelect={toggleBulkSelect}
+ fetchData={fetchData}
+ filters={filters}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ showThumbnails={
+ userKey
+ ? userKey.thumbnails
+ : isFeatureEnabled(FeatureFlag.THUMBNAILS)
+ }
+ renderCard={renderCard}
+ defaultViewMode={
+ isFeatureEnabled(FeatureFlag.LISTVIEWS_DEFAULT_CARD_VIEW)
+ ? 'card'
+ : 'table'
+ }
+ />
+ >
+ );
+ }}
+
+ >
+ );
+}
+
+export default withToasts(TagList);
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 6d0c8d379b022..d5e40c9d0abcb 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -363,25 +363,6 @@ def init_views(self) -> None:
category="SQL Lab",
category_label=__("SQL Lab"),
)
- appbuilder.add_view(
- DatabaseView,
- "Databases",
- label=__("Databases"),
- icon="fa-database",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-database",
- )
- appbuilder.add_link(
- "Datasets",
- label=__("Datasets"),
- href="/tablemodelview/list/",
- icon="fa-table",
- category="Data",
- category_label=__("Data"),
- category_icon="fa-table",
- )
- appbuilder.add_separator("Data")
appbuilder.add_view(
TaggedObjectsModelView,
"All Entities",
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 300835ebcf18b..4008857d40382 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -159,7 +159,7 @@ def post( # pylint: disable=no-self-use
@has_access_api
@expose("/tags///", methods=["DELETE"])
- def delete( # pylint: disable=no-self-use
+ def delete_tagged_objects( # pylint: disable=no-self-use
self, object_type: ObjectTypes, object_id: int
) -> FlaskResponse:
"""Remove tags from an object."""
@@ -178,6 +178,26 @@ def delete( # pylint: disable=no-self-use
return Response(status=204) # 204 NO CONTENT
+ @has_access_api
+ @expose("/tags", methods=["DELETE"])
+ def delete_tags( # pylint: disable=no-self-use
+ self
+ ) -> FlaskResponse:
+ """Remove tags, and all tagged objects with that tag """
+ tag_names = request.get_json(force=True)
+ if not tag_names:
+ return Response(status=403)
+ db.session()
+ db.session.query(TaggedObject).filter(
+ TaggedObject.tag.has(Tag.name.in_(tag_names)),
+ ).delete(synchronize_session=False)
+ db.session.query(Tag).filter(
+ Tag.name.in_(tag_names),
+ ).delete(synchronize_session=False)
+ db.session.commit()
+
+ return Response(status=204) # 204 NO CONTENT
+
@has_access_api
@expose("/tagged_objects/", methods=["GET", "POST"])
def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
From 7a5d8f7870eafbff5a14e2837964c1a7d2494b1e Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Fri, 2 Dec 2022 11:44:31 -0500
Subject: [PATCH 38/76] lint fixes
---
.../src/components/ObjectTags/index.tsx | 8 +--
.../src/components/ObjectTags/styles.ts | 58 +++++++++----------
superset-frontend/src/components/Tags/Tag.tsx | 6 +-
.../components/PropertiesModal/index.tsx | 34 +++++------
.../components/ExploreChartHeader/index.jsx | 1 -
.../components/PropertiesModal/index.tsx | 35 ++++++-----
.../views/CRUD/allentities/AllEntities.tsx | 1 -
.../CRUD/allentities/AllEntitiesTable.tsx | 5 +-
.../src/views/CRUD/chart/ChartList.tsx | 2 +-
.../views/CRUD/dashboard/DashboardList.tsx | 4 +-
superset-frontend/src/views/routes.tsx | 5 +-
11 files changed, 80 insertions(+), 79 deletions(-)
diff --git a/superset-frontend/src/components/ObjectTags/index.tsx b/superset-frontend/src/components/ObjectTags/index.tsx
index 3e219a405236b..1f613a4e7c8be 100644
--- a/superset-frontend/src/components/ObjectTags/index.tsx
+++ b/superset-frontend/src/components/ObjectTags/index.tsx
@@ -21,7 +21,6 @@ import React, { useEffect, useState } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
import Tag from 'src/types/TagType';
-import { objectTagsStyles } from './styles';
import { TagsList } from 'src/components/Tags';
import rison from 'rison';
import { cacheWrapper } from 'src/utils/cacheWrapper';
@@ -30,6 +29,7 @@ import {
getClientErrorObject,
} from 'src/utils/getClientErrorObject';
import { deleteTaggedObjects, fetchTags } from 'src/tags';
+import { objectTagsStyles } from './styles';
export interface ObjectTagsProps {
objectType: string;
@@ -141,7 +141,7 @@ export const ObjectTags = ({
{ objectType, objectId },
tags[tagIndex],
() => setTags(tags.filter((_, i) => i !== tagIndex)),
- (error) => {
+ error => {
throw new Error(t('An Error occured when deleting the tag'));
},
);
@@ -150,9 +150,7 @@ export const ObjectTags = ({
return (
-
+
css`
+export const objectTagsStyles = (theme: SupersetTheme) => css`
.ant-tag {
color: ${theme.colors.grayscale.dark2};
}
-
+
.react-tags {
position: relative;
display: inline-block;
@@ -31,19 +32,19 @@
margin: 0 ${theme.gridUnit * 2.5}px;
border: 0px solid #f5f5f5;
border-radius: 1px;
-
+
/* shared font styles */
font-size: ${theme.gridUnit * 3}px;
line-height: 1.2;
-
+
/* clicking anywhere will focus the input */
cursor: text;
}
-
+
.react-tags__selected {
display: inline;
}
-
+
.react-tags__selected-tag {
display: inline-block;
box-sizing: border-box;
@@ -52,57 +53,57 @@
border: 0px solid #f5f5f5;
border-radius: ${theme.borderRadius}px;
background: #f1f1f1;
-
+
/* match the font styles */
font-size: inherit;
line-height: inherit;
}
-
+
.react-tags__search {
display: inline-block;
-
+
/* new tag border layout */
border: 1px dashed #d9d9d9;
-
+
/* match tag layout */
line-height: ${theme.gridUnit * 5}px;
margin-bottom: 0;
padding: 0 ${theme.gridUnit * 1.75}px;
-
+
/* prevent autoresize overflowing the container */
max-width: 100%;
}
-
+
.react-tags__search:focus-within {
border: 1px solid ${theme.colors.grayscale.dark2};
}
-
+
@media screen and (min-width: ${theme.gridUnit * 7.5}em) {
.react-tags__search {
/* this will become the offsetParent for suggestions */
position: relative;
}
}
-
+
.react-tags__search input {
max-width: 150%;
-
+
/* remove styles and layout from this element */
margin: 0;
margin-left: 0;
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%;
@@ -110,13 +111,13 @@
width: 100%;
z-index: ${theme.zIndex.max};
}
-
+
@media screen and (min-width: ${theme.gridUnit * 7.5}em) {
.react-tags__suggestions {
width: ${theme.gridUnit * 60}px;
}
}
-
+
.react-tags__suggestions ul {
margin: 4px -1px;
padding: 0;
@@ -126,30 +127,29 @@
border-radius: 2px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
-
+
.react-tags__suggestions li {
border-bottom: 1px solid #ddd;
- padding: ${ theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
+ padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
}
-
+
.react-tags__suggestions li mark {
text-decoration: underline;
background: none;
font-weight: ${theme.typography.weights.bold};
}
-
+
.react-tags__suggestions li:hover {
cursor: pointer;
background: #eee;
}
-
+
.react-tags__suggestions li.is-active {
background: #b7cfe0;
}
-
+
.react-tags__suggestions li.is-disabled {
opacity: calc(${theme.opacity.mediumHeavy});
cursor: auto;
}
- `;
-
\ No newline at end of file
+`;
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
index f75b19f0db020..db072feba5804 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -55,7 +55,11 @@ const Tag = ({
) : (
{id ? (
-
+
{isLongTag ? `${name.slice(0, 20)}...` : name}
) : isLongTag ? (
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 3f4856fa24770..448d99b472f1d 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -109,15 +109,12 @@ const PropertiesModal = ({
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const tagsAsSelectValues = useMemo(() => {
- const selectTags = tags.map((tag) => {
- return {
- value:tag.name,
- label:tag.name
- }
- });
+ const selectTags = tags.map(tag => ({
+ value: tag.name,
+ label: tag.name,
+ }));
return selectTags;
- }, [tags.length])
-
+ }, [tags.length]);
const handleErrorResponse = async (response: Response) => {
const { error, statusText, message } = await getClientErrorObject(response);
@@ -376,7 +373,7 @@ const PropertiesModal = ({
includeTypes: false,
},
(currentTags: TagType[]) => updateTags(currentTags, tags),
- (error) => {
+ error => {
handleErrorResponse(error);
},
);
@@ -577,18 +574,19 @@ const PropertiesModal = ({
includeTypes: false,
},
(tags: TagType[]) => setTags(tags),
- () => {
+ (error: Response) => {
handleErrorResponse(error);
},
);
} catch (error: any) {
- console.log(error);
+ handleErrorResponse(error);
}
}, [dashboardId]);
const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
// update the tags for this object
// add tags that are in new tags, but not in old tags
+ // eslint-disable-next-line array-callback-return
newTags.map((tag: TagType) => {
if (!oldTags.some(t => t.name === tag.name)) {
addTag(
@@ -604,6 +602,7 @@ const PropertiesModal = ({
}
});
// delete tags that are in old tags, but not in new tags
+ // eslint-disable-next-line array-callback-return
oldTags.map((tag: TagType) => {
if (!newTags.some(t => t.name === tag.name)) {
deleteTaggedObjects(
@@ -614,19 +613,18 @@ const PropertiesModal = ({
tag,
() => {},
() => {},
- )
+ );
}
});
- }
+ };
const handleChangeTags = (values: { label: string; value: number }[]) => {
// triggered whenever a new tag is selected or a tag was deselected
// on new tag selected, add the tag
-
- const uniqueTags = [...new Set(values.map((v => v.label)))];
- setTags([...uniqueTags.map(t => ({name: t}))])
- return;
- }
+
+ const uniqueTags = [...new Set(values.map(v => v.label))];
+ setTags([...uniqueTags.map(t => ({ name: t }))]);
+ };
return (
([]);
const tagsAsSelectValues = useMemo(() => {
- const selectTags = tags.map((tag) => {
- return {
- value:tag.name,
- label:tag.name
- }
- });
+ const selectTags = tags.map(tag => ({
+ value: tag.name,
+ label: tag.name,
+ }));
return selectTags;
- }, [tags.length])
+ }, [tags.length]);
function showError({ error, statusText, message }: any) {
let errorText = error || statusText || t('An error has occurred');
@@ -180,8 +178,8 @@ function PropertiesModal({
includeTypes: false,
},
(currentTags: TagType[]) => updateTags(currentTags, tags),
- (error) => {
- showError(error)
+ error => {
+ showError(error);
},
);
} catch (error: any) {
@@ -235,7 +233,7 @@ function PropertiesModal({
includeTypes: false,
},
(tags: TagType[]) => setTags(tags),
- (error) => {
+ error => {
showError(error);
},
);
@@ -247,6 +245,7 @@ function PropertiesModal({
const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
// update the tags for this object
// add tags that are in new tags, but not in old tags
+ // eslint-disable-next-line array-callback-return
newTags.map((tag: TagType) => {
if (!oldTags.some(t => t.name === tag.name)) {
addTag(
@@ -262,6 +261,7 @@ function PropertiesModal({
}
});
// delete tags that are in old tags, but not in new tags
+ // eslint-disable-next-line array-callback-return
oldTags.map((tag: TagType) => {
if (!newTags.some(t => t.name === tag.name)) {
deleteTaggedObjects(
@@ -272,23 +272,22 @@ function PropertiesModal({
tag,
() => {},
() => {},
- )
+ );
}
});
- }
+ };
const handleChangeTags = (values: { label: string; value: number }[]) => {
// triggered whenever a new tag is selected or a tag was deselected
// on new tag selected, add the tag
-
- const uniqueTags = [...new Set(values.map((v => v.label)))];
- setTags([...uniqueTags.map(t => ({name: t}))])
- return;
- }
+
+ const uniqueTags = [...new Set(values.map(v => v.label))];
+ setTags([...uniqueTags.map(t => ({ name: t }))]);
+ };
const handleClearTags = () => {
setTags([]);
- }
+ };
return (
();
const [tagsQuery, setTagsQuery] = useQueryParam('tags', StringParam);
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
index d5fb262e2e7ee..6a204abee940e 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
@@ -55,7 +55,9 @@ interface AllEntitiesTableProps {
search?: string;
}
-export default function AllEntitiesTable({ search = '' }: AllEntitiesTableProps) {
+export default function AllEntitiesTable({
+ search = '',
+}: AllEntitiesTableProps) {
const [objects, setObjects] = useState({
dashboard: [],
chart: [],
@@ -76,7 +78,6 @@ export default function AllEntitiesTable({ search = '' }: AllEntitiesTableProps)
console.log(error.json());
},
);
-
}, [search]);
const renderTable = (type: any) => {
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index e46a3b22dfe1e..572306a1a0877 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -70,9 +70,9 @@ import CertifiedBadge from 'src/components/CertifiedBadge';
import { GenericLink } from 'src/components/GenericLink/GenericLink';
import { bootstrapData } from 'src/preamble';
import Owner from 'src/types/Owner';
-import ChartCard from './ChartCard';
import { OBJECT_TYPES } from 'src/tags';
import { loadTags } from 'src/components/ObjectTags';
+import ChartCard from './ChartCard';
const FlexRowContainer = styled.div`
align-items: center;
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index 3ec7e88d62af2..31f76c52d77e0 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -53,9 +53,9 @@ import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { bootstrapData } from 'src/preamble';
+import { loadTags } from 'src/components/ObjectTags';
import DashboardCard from './DashboardCard';
import { DashboardStatus } from './types';
-import { loadTags } from 'src/components/ObjectTags';
const PAGE_SIZE = 25;
const PASSWORDS_NEEDED_MESSAGE = t(
@@ -566,7 +566,7 @@ function DashboardList(props: DashboardListProps) {
input: 'select',
operator: FilterOperator.chartTags,
unfilteredLabel: t('All'),
- fetchSelects: loadTags
+ fetchSelects: loadTags,
});
}
filters_list.push({
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index 5097b370ccb51..2821fad71b288 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -112,7 +112,10 @@ const SavedQueryList = lazy(
),
);
const AllEntitiesPage = lazy(
- () => import(/* webpackChunkName: "AllEntities" */ 'src/views/CRUD/allentities/AllEntities'),
+ () =>
+ import(
+ /* webpackChunkName: "AllEntities" */ 'src/views/CRUD/allentities/AllEntities'
+ ),
);
const TagsPage = lazy(
() => import(/* webpackChunkName: "TagList" */ 'src/views/CRUD/tags/TagList'),
From 06820db3bbedf319e9ce433cc96d60d80d0920fb Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Fri, 2 Dec 2022 11:51:46 -0500
Subject: [PATCH 39/76] undid acidental changes
---
.../components/ExploreChartHeader/index.jsx | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index c36c2a0577e8d..958aa16a31994 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -206,7 +206,6 @@ export const ExploreChartHeader = ({
}, [metadata, slice?.description]);
const oldSliceName = slice?.slice_name;
-
return (
<>
- ) : null
+
+ {sliceFormData ? (
+
+ ) : null}
+ {metadataBar}
+
}
rightPanelAdditionalItems={
Date: Fri, 2 Dec 2022 14:31:43 -0500
Subject: [PATCH 40/76] pylint errors
---
superset/charts/schemas.py | 47 ++++++++++++++++++++----------
superset/connectors/sqla/models.py | 1 -
superset/dashboards/schemas.py | 18 ++++++++----
superset/tags/api.py | 3 +-
superset/tags/commands/create.py | 3 +-
superset/tags/dao.py | 13 ++++++---
superset/tags/models.py | 8 +++--
superset/views/tags.py | 13 ++++++---
8 files changed, 69 insertions(+), 37 deletions(-)
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 12c62052c1243..1195c61e82861 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -149,7 +149,7 @@
}
-class TagSchema(Schema):
+class TagSchema(Schema):
id = fields.Int()
name = fields.String()
type = fields.String()
@@ -171,7 +171,8 @@ class ChartEntityResponseSchema(Schema):
form_data = fields.Dict(description=form_data_description)
slice_url = fields.String(description=slice_url_description)
certified_by = fields.String(description=certified_by_description)
- certification_details = fields.String(description=certification_details_description)
+ certification_details = fields.String(
+ description=certification_details_description)
class ChartPostSchema(Schema):
@@ -182,7 +183,8 @@ class ChartPostSchema(Schema):
slice_name = fields.String(
description=slice_name_description, required=True, validate=Length(1, 250)
)
- description = fields.String(description=description_description, allow_none=True)
+ description = fields.String(
+ description=description_description, allow_none=True)
viz_type = fields.String(
description=viz_type_description,
validate=Length(0, 250),
@@ -203,7 +205,8 @@ class ChartPostSchema(Schema):
cache_timeout = fields.Integer(
description=cache_timeout_description, allow_none=True
)
- datasource_id = fields.Integer(description=datasource_id_description, required=True)
+ datasource_id = fields.Integer(
+ description=datasource_id_description, required=True)
datasource_type = fields.String(
description=datasource_type_description,
validate=validate.OneOf(choices=[ds.value for ds in DatasourceType]),
@@ -212,8 +215,10 @@ class ChartPostSchema(Schema):
datasource_name = fields.String(
description=datasource_name_description, allow_none=True
)
- dashboards = fields.List(fields.Integer(description=dashboards_description))
- certified_by = fields.String(description=certified_by_description, allow_none=True)
+ dashboards = fields.List(fields.Integer(
+ description=dashboards_description))
+ certified_by = fields.String(
+ description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -229,7 +234,8 @@ class ChartPutSchema(Schema):
slice_name = fields.String(
description=slice_name_description, allow_none=True, validate=Length(0, 250)
)
- description = fields.String(description=description_description, allow_none=True)
+ description = fields.String(
+ description=description_description, allow_none=True)
viz_type = fields.String(
description=viz_type_description,
allow_none=True,
@@ -255,8 +261,10 @@ class ChartPutSchema(Schema):
validate=validate.OneOf(choices=[ds.value for ds in DatasourceType]),
allow_none=True,
)
- dashboards = fields.List(fields.Integer(description=dashboards_description))
- certified_by = fields.String(description=certified_by_description, allow_none=True)
+ dashboards = fields.List(fields.Integer(
+ description=dashboards_description))
+ certified_by = fields.String(
+ description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -918,14 +926,16 @@ class AnnotationLayerSchema(Schema):
keys=fields.String(
desciption="Name of property to be overridden",
validate=validate.OneOf(
- choices=("granularity", "time_grain_sqla", "time_range", "time_shift"),
+ choices=("granularity", "time_grain_sqla",
+ "time_range", "time_shift"),
),
),
values=fields.Raw(allow_none=True),
description="which properties should be overridable",
allow_none=True,
)
- show = fields.Boolean(description="Should the layer be shown", required=True)
+ show = fields.Boolean(
+ description="Should the layer be shown", required=True)
showLabel = fields.Boolean(
description="Should the label always be shown",
allow_none=True,
@@ -998,7 +1008,8 @@ class Meta: # pylint: disable=too-few-public-methods
unknown = EXCLUDE
datasource = fields.Nested(ChartDataDatasourceSchema, allow_none=True)
- result_type = EnumField(ChartDataResultType, by_value=True, allow_none=True)
+ result_type = EnumField(ChartDataResultType,
+ by_value=True, allow_none=True)
annotation_layers = fields.List(
fields.Nested(AnnotationLayerSchema),
@@ -1015,7 +1026,8 @@ class Meta: # pylint: disable=too-few-public-methods
"if defined in datasource",
allow_none=True,
)
- filters = fields.List(fields.Nested(ChartDataFilterSchema), allow_none=True)
+ filters = fields.List(fields.Nested(
+ ChartDataFilterSchema), allow_none=True)
granularity = fields.String(
description="Name of temporal column used for time filtering. For legacy Druid "
"datasources this defines the time grain.",
@@ -1141,7 +1153,8 @@ class Meta: # pylint: disable=too-few-public-methods
(
fields.Raw(
validate=[
- Length(min=1, error=_("orderby column must be populated"))
+ Length(min=1, error=_(
+ "orderby column must be populated"))
],
allow_none=False,
),
@@ -1312,7 +1325,8 @@ class ChartDataResponseResult(Schema):
allow_none=False,
)
data = fields.List(fields.Dict(), description="A list with results")
- colnames = fields.List(fields.String(), description="A list of column names")
+ colnames = fields.List(
+ fields.String(), description="A list of column names")
coltypes = fields.List(
fields.Integer(), description="A list of generic data types of each column"
)
@@ -1376,7 +1390,8 @@ class ImportV1ChartSchema(Schema):
slice_name = fields.String(required=True)
viz_type = fields.String(required=True)
params = fields.Dict()
- query_context = fields.String(allow_none=True, validate=utils.validate_json)
+ query_context = fields.String(
+ allow_none=True, validate=utils.validate_json)
cache_timeout = fields.Integer(allow_none=True)
uuid = fields.UUID(required=True)
version = fields.String(required=True)
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 7447be0c0096a..95d1b2f5837b4 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -40,7 +40,6 @@
import dateutil.parser
import numpy as np
import pandas as pd
-from pyrsistent import v
import sqlalchemy as sa
import sqlparse
from flask import escape, Markup
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 2892b4929b530..7bb6c50f068ff 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -147,7 +147,8 @@ class RolesSchema(Schema):
id = fields.Int()
name = fields.String()
-class TagSchema(Schema):
+
+class TagSchema(Schema):
id = fields.Int()
name = fields.String()
type = fields.String()
@@ -164,7 +165,8 @@ class DashboardGetResponseSchema(Schema):
json_metadata = fields.String(description=json_metadata_description)
position_json = fields.String(description=position_json_description)
certified_by = fields.String(description=certified_by_description)
- certification_details = fields.String(description=certification_details_description)
+ certification_details = fields.String(
+ description=certification_details_description)
changed_by_name = fields.String()
changed_by_url = fields.String()
changed_by = fields.Nested(UserSchema)
@@ -254,7 +256,8 @@ class DashboardPostSchema(BaseDashboardSchema):
validate=validate_json_metadata,
)
published = fields.Boolean(description=published_description)
- certified_by = fields.String(description=certified_by_description, allow_none=True)
+ certified_by = fields.String(
+ description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -274,7 +277,8 @@ class DashboardPutSchema(BaseDashboardSchema):
owners = fields.List(
fields.Integer(description=owners_description, allow_none=True)
)
- roles = fields.List(fields.Integer(description=roles_description, allow_none=True))
+ roles = fields.List(fields.Integer(
+ description=roles_description, allow_none=True))
position_json = fields.String(
description=position_json_description, allow_none=True, validate=validate_json
)
@@ -284,8 +288,10 @@ class DashboardPutSchema(BaseDashboardSchema):
allow_none=True,
validate=validate_json_metadata,
)
- published = fields.Boolean(description=published_description, allow_none=True)
- certified_by = fields.String(description=certified_by_description, allow_none=True)
+ published = fields.Boolean(
+ description=published_description, allow_none=True)
+ certified_by = fields.String(
+ description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 22b23518cb76d..03599236bd932 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -14,7 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-# pylint: disable=too-many-lines
import logging
from flask import request, Response
@@ -105,7 +104,7 @@ def __repr__(self) -> str:
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
log_to_statsd=False,
)
- def post(self, object_type: ObjectTypes, object_id: int) -> Response:
+ def add_new_tags(self, object_type: ObjectTypes, object_id: int) -> Response:
"""Adds new tags to an object.
---
post:
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index c02a248d6a575..be4ad99c04b4b 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -40,7 +40,8 @@ def __init__(self, object_type: ObjectTypes, object_id: int, data: Dict[str, Any
def run(self) -> Model:
self.validate()
try:
- tag = TagDAO.create_tagged_objects(self._object_type, self._object_id, self._properties)
+ tag = TagDAO.create_tagged_objects(
+ self._object_type, self._object_id, self._properties)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
raise TagCreateFailedError() from ex
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index e043ccc910a8e..aff05551c7bf6 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -17,7 +17,6 @@
import logging
from typing import Any, Dict
-from superset import security_manager
from superset.dao.base import BaseDAO
from superset.extensions import db
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
@@ -30,7 +29,11 @@ class TagDAO(BaseDAO):
# base_filter = TagAccessFilter
@staticmethod
- def create_tagged_objects(object_type: ObjectTypes, object_id: int, properties: Dict[str, Any]) -> None:
+ def create_tagged_objects(
+ object_type: ObjectTypes,
+ object_id: int,
+ properties: Dict[str, Any]
+ ) -> None:
tag_names = properties["tags"]
tagged_objects = []
@@ -43,7 +46,8 @@ def create_tagged_objects(object_type: ObjectTypes, object_id: int, properties:
tag = TagDAO.get_by_name(name, type_)
tagged_objects.append(
- TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+ TaggedObject(object_id=object_id,
+ object_type=object_type, tag=tag)
)
db.session.add_all(tagged_objects)
@@ -53,7 +57,8 @@ def create_tagged_objects(object_type: ObjectTypes, object_id: int, properties:
@staticmethod
def get_by_name(name: str, type_: str) -> Tag:
- tag = db.session.query(Tag).filter(Tag.name == name, Tag.type == type_).first()
+ tag = db.session.query(Tag).filter(
+ Tag.name == name, Tag.type == type_).first()
if not tag:
tag = Tag(name=name, type=type_)
# security_manager.raise_for_tag_access(tag)
diff --git a/superset/tags/models.py b/superset/tags/models.py
index da1b895d8bb32..283cdfede2616 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -92,7 +92,8 @@ class TaggedObject(Model, AuditMixinNullable):
__tablename__ = "tagged_object"
id = Column(Integer, primary_key=True)
tag_id = Column(Integer, ForeignKey("tag.id"))
- object_id = Column(Integer, ForeignKey("dashboards.id"), ForeignKey("slices.id"), ForeignKey("saved_query.id"))
+ object_id = Column(Integer, ForeignKey("dashboards.id"),
+ ForeignKey("slices.id"), ForeignKey("saved_query.id"))
object_type = Column(Enum(ObjectTypes))
tag = relationship("Tag", backref="objects")
@@ -157,7 +158,8 @@ def after_insert(
cls._add_owners(session, target)
# add `type:` tags
- tag = get_tag("type:{0}".format(cls.object_type), session, TagTypes.type)
+ tag = get_tag("type:{0}".format(cls.object_type),
+ session, TagTypes.type)
tagged_object = TaggedObject(
tag_id=tag.id, object_id=target.id, object_type=cls.object_type
)
@@ -270,7 +272,7 @@ def after_delete(
cls, _mapper: Mapper, connection: Connection, target: FavStar
) -> None:
session = Session(bind=connection)
- name = "favorited_by:{0}".format(target.user_id)
+ name = f'favorited_by:{target.user_id}'
query = (
session.query(TaggedObject.id)
.join(Tag)
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 4008857d40382..fd4535f275b7d 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -50,6 +50,7 @@ def process_template(content: str) -> str:
}
return template.render(context)
+
class TagModelView(SupersetModelView):
route_base = "/superset/tags"
datamodel = SQLAInterface(Tag)
@@ -63,6 +64,7 @@ def list(self) -> FlaskResponse:
return super().render_app_template()
+
class TagView(BaseSupersetView):
@staticmethod
def is_enabled() -> bool:
@@ -108,7 +110,7 @@ def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
)
tags = [{"id": id, "name": name} for id, name in query]
return json_success(json.dumps(tags))
-
+
@has_access_api
@expose("/tags///", methods=["GET"])
def get( # pylint: disable=no-self-use
@@ -144,12 +146,14 @@ def post( # pylint: disable=no-self-use
else:
type_ = TagTypes.custom
- tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
+ tag = db.session.query(Tag).filter_by(
+ name=name, type=type_).first()
if not tag:
tag = Tag(name=name, type=type_)
tagged_objects.append(
- TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+ TaggedObject(object_id=object_id,
+ object_type=object_type, tag=tag)
)
db.session.add_all(tagged_objects)
@@ -210,7 +214,8 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
return json_success(json.dumps([]))
# filter types
- types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
+ types = [type_ for type_ in request.args.get(
+ "types", "").split(",") if type_]
results: List[Dict[str, Any]] = []
From 8970fba1adafd1856d40a52c5bfb7aeb39b9b381 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Mon, 5 Dec 2022 09:23:46 -0500
Subject: [PATCH 41/76] suggestion fixes and removing unused object tags
---
.../src/components/ObjectTags/index.tsx | 165 ------------------
.../src/components/ObjectTags/styles.ts | 155 ----------------
.../src/components/Tags/TagsList.tsx | 5 +-
.../components/PropertiesModal/index.tsx | 1 -
4 files changed, 2 insertions(+), 324 deletions(-)
delete mode 100644 superset-frontend/src/components/ObjectTags/index.tsx
delete mode 100644 superset-frontend/src/components/ObjectTags/styles.ts
diff --git a/superset-frontend/src/components/ObjectTags/index.tsx b/superset-frontend/src/components/ObjectTags/index.tsx
deleted file mode 100644
index 1f613a4e7c8be..0000000000000
--- a/superset-frontend/src/components/ObjectTags/index.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * 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, { useEffect, useState } from 'react';
-import { styled, SupersetClient, t } from '@superset-ui/core';
-import Tag from 'src/types/TagType';
-
-import { TagsList } from 'src/components/Tags';
-import rison from 'rison';
-import { cacheWrapper } from 'src/utils/cacheWrapper';
-import {
- ClientErrorObject,
- getClientErrorObject,
-} from 'src/utils/getClientErrorObject';
-import { deleteTaggedObjects, fetchTags } from 'src/tags';
-import { objectTagsStyles } from './styles';
-
-export interface ObjectTagsProps {
- objectType: string;
- objectId: number;
- includeTypes: boolean;
- editMode: boolean;
- maxTags: number | undefined;
- onChange?: (tags: Tag[]) => void;
-}
-
-const StyledTagsDiv = styled.div`
- margin-left: ${({ theme }) => theme.gridUnit * 2}px;
- max-width: 100%;
- display: -webkit-flex;
- display: flex;
- -webkit-flex-direction: row;
- -webkit-flex-wrap: wrap;
-`;
-
-const localCache = new Map();
-
-const cachedSupersetGet = cacheWrapper(
- SupersetClient.get,
- localCache,
- ({ endpoint }) => endpoint || '',
-);
-
-type SelectTagsValue = {
- value: string | number | undefined;
- label: string;
-};
-
-export const tagToSelectOption = (
- item: Tag & { table_name: string },
-): SelectTagsValue => ({
- value: item.id,
- label: 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);
- });
-};
-
-export const ObjectTags = ({
- objectType,
- objectId,
- includeTypes,
- editMode = false,
- maxTags = undefined,
- onChange,
-}: ObjectTagsProps) => {
- const [tags, setTags] = useState([]);
-
- useEffect(() => {
- try {
- fetchTags(
- { objectType, objectId, includeTypes },
- (tags: Tag[]) => setTags(tags),
- () => {
- throw new Error(t('An Error occured when fetching tags'));
- },
- );
- } catch (error: any) {
- throw new Error(t('An Error occured when fetching tags'));
- }
- }, [objectType, objectId, includeTypes]);
-
- const onDelete = (tagIndex: number) => {
- deleteTaggedObjects(
- { objectType, objectId },
- tags[tagIndex],
- () => setTags(tags.filter((_, i) => i !== tagIndex)),
- error => {
- throw new Error(t('An Error occured when deleting the tag'));
- },
- );
- onChange?.(tags);
- };
-
- return (
-
-
-
-
-
- );
-};
-
-export default ObjectTags;
diff --git a/superset-frontend/src/components/ObjectTags/styles.ts b/superset-frontend/src/components/ObjectTags/styles.ts
deleted file mode 100644
index 4ff3cfcd9a3e4..0000000000000
--- a/superset-frontend/src/components/ObjectTags/styles.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-/* eslint-disable theme-colors/no-literal-colors */
-/**
- * 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 { css, SupersetTheme } from '@superset-ui/core';
-
-export const objectTagsStyles = (theme: SupersetTheme) => css`
- .ant-tag {
- color: ${theme.colors.grayscale.dark2};
- }
-
- .react-tags {
- position: relative;
- display: inline-block;
- padding: 1px 0 0 1px;
- margin: 0 ${theme.gridUnit * 2.5}px;
- border: 0px solid #f5f5f5;
- border-radius: 1px;
-
- /* shared font styles */
- font-size: ${theme.gridUnit * 3}px;
- line-height: 1.2;
-
- /* clicking anywhere will focus the input */
- cursor: text;
- }
-
- .react-tags__selected {
- display: inline;
- }
-
- .react-tags__selected-tag {
- display: inline-block;
- box-sizing: border-box;
- margin: 0;
- padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
- border: 0px solid #f5f5f5;
- border-radius: ${theme.borderRadius}px;
- background: #f1f1f1;
-
- /* match the font styles */
- font-size: inherit;
- line-height: inherit;
- }
-
- .react-tags__search {
- display: inline-block;
-
- /* new tag border layout */
- border: 1px dashed #d9d9d9;
-
- /* match tag layout */
- line-height: ${theme.gridUnit * 5}px;
- margin-bottom: 0;
- padding: 0 ${theme.gridUnit * 1.75}px;
-
- /* prevent autoresize overflowing the container */
- max-width: 100%;
- }
-
- .react-tags__search:focus-within {
- border: 1px solid ${theme.colors.grayscale.dark2};
- }
-
- @media screen and (min-width: ${theme.gridUnit * 7.5}em) {
- .react-tags__search {
- /* this will become the offsetParent for suggestions */
- position: relative;
- }
- }
-
- .react-tags__search input {
- max-width: 150%;
-
- /* remove styles and layout from this element */
- margin: 0;
- margin-left: 0;
- 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: ${theme.zIndex.max};
- }
-
- @media screen and (min-width: ${theme.gridUnit * 7.5}em) {
- .react-tags__suggestions {
- width: ${theme.gridUnit * 60}px;
- }
- }
-
- .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: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 2}px;
- }
-
- .react-tags__suggestions li mark {
- text-decoration: underline;
- background: none;
- font-weight: ${theme.typography.weights.bold};
- }
-
- .react-tags__suggestions li:hover {
- cursor: pointer;
- background: #eee;
- }
-
- .react-tags__suggestions li.is-active {
- background: #b7cfe0;
- }
-
- .react-tags__suggestions li.is-disabled {
- opacity: calc(${theme.opacity.mediumHeavy});
- cursor: auto;
- }
-`;
diff --git a/superset-frontend/src/components/Tags/TagsList.tsx b/superset-frontend/src/components/Tags/TagsList.tsx
index 4f763299da9d1..028ae5c5a832f 100644
--- a/superset-frontend/src/components/Tags/TagsList.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.tsx
@@ -36,10 +36,9 @@ export type TagsListProps = {
const TagsDiv = styled.div`
max-width: 100%;
- display: -webkit-flex;
display: flex;
- -webkit-flex-direction: row;
- -webkit-flex-wrap: wrap;
+ flex-direction: row;
+ flex-wrap: wrap;
`;
const TagsList = ({
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index ed96a6f53574f..dc22c10a80af1 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -36,7 +36,6 @@ import withToasts from 'src/components/MessageToasts/withToasts';
import { loadTags } from 'src/components/ObjectTags';
import { addTag, deleteTaggedObjects, fetchTags, OBJECT_TYPES } from 'src/tags';
import TagType from 'src/types/TagType';
-import { TagsList } from 'src/components/Tags';
export type PropertiesModalProps = {
slice: Slice;
From 74733e5941524c9b5a994bca24d4d935b5b240f4 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Mon, 5 Dec 2022 10:24:22 -0500
Subject: [PATCH 42/76] added TagList stories file
---
.../src/components/Tags/TagsList.stories.tsx | 58 +++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 superset-frontend/src/components/Tags/TagsList.stories.tsx
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,
+ },
+ },
+};
From 4ca90da1aa26e859af5f056d8012a9ca2ae25c7b Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 6 Dec 2022 10:12:29 -0500
Subject: [PATCH 43/76] fixed package-lock
---
superset-frontend/package-lock.json | 1753 +++++++++++++++++----------
1 file changed, 1102 insertions(+), 651 deletions(-)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index c31184b040afe..34dc1c17845ae 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -88,7 +88,7 @@
"match-sorter": "^6.1.0",
"memoize-one": "^5.1.1",
"moment": "^2.26.0",
- "moment-timezone": "^0.5.33",
+ "moment-timezone": "^0.5.37",
"mousetrap": "^1.6.1",
"mustache": "^2.2.1",
"polished": "^3.7.2",
@@ -273,7 +273,7 @@
"webpack": "^5.52.1",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.8.0",
- "webpack-dev-server": "^4.2.0",
+ "webpack-dev-server": "^4.10.1",
"webpack-manifest-plugin": "^4.0.2",
"webpack-sources": "^3.2.0"
},
@@ -6600,6 +6600,12 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
+ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
+ "devOptional": true
+ },
"node_modules/@lerna/add": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz",
@@ -8829,15 +8835,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
- "node_modules/@lerna/publish/node_modules/negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "dev": true,
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/@lerna/publish/node_modules/node-gyp": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz",
@@ -10837,16 +10834,6 @@
"devOptional": true,
"peer": true
},
- "node_modules/@npmcli/run-script/node_modules/negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "devOptional": true,
- "peer": true,
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/@npmcli/run-script/node_modules/node-gyp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -17866,6 +17853,25 @@
"@babel/types": "^7.3.0"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.10",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz",
+ "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/cheerio": {
"version": "0.22.21",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz",
@@ -17892,6 +17898,25 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
+ "node_modules/@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz",
+ "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/d3": {
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.38.tgz",
@@ -18020,6 +18045,29 @@
"devOptional": true,
"peer": true
},
+ "node_modules/@types/express": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
+ "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.18",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.30",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz",
+ "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
"node_modules/@types/fetch-mock": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@types/fetch-mock/-/fetch-mock-7.3.5.tgz",
@@ -18082,9 +18130,9 @@
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
},
"node_modules/@types/http-proxy": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
- "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==",
+ "version": "1.17.9",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
+ "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==",
"devOptional": true,
"dependencies": {
"@types/node": "*"
@@ -18192,6 +18240,12 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/mime": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
+ "devOptional": true
+ },
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -18287,6 +18341,12 @@
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
},
+ "node_modules/@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+ "devOptional": true
+ },
"node_modules/@types/react": {
"version": "16.9.43",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.43.tgz",
@@ -18497,6 +18557,25 @@
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.30.tgz",
"integrity": "sha512-AnxLHewubLVzoF/A4qdxBGHCKifw8cY32iro3DQX9TPcetE95zBeVt3jnsvtvAUf1vwzMfwzp4t/L2yqPlnjkQ=="
},
+ "node_modules/@types/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/shortid": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
@@ -18524,6 +18603,15 @@
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
+ "node_modules/@types/sockjs": {
+ "version": "0.3.33",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
+ "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -18654,6 +18742,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
+ "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
@@ -19806,12 +19903,12 @@
"integrity": "sha512-9jN7+BijYKWO8fxfcG7QZh7js6V+g3OjkxMRHfKWNjjs85048VY4cd27Uoe6yk55P66L/z7Dflu5+YEApgMzkA=="
},
"node_modules/accepts": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
- "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dependencies": {
- "mime-types": "~2.1.18",
- "negotiator": "0.6.1"
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
@@ -20057,7 +20154,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -20074,7 +20171,7 @@
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz",
"integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -20090,7 +20187,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
+ "devOptional": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
@@ -22176,18 +22273,16 @@
"node": ">= 0.6"
}
},
- "node_modules/bonjour": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
- "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+ "node_modules/bonjour-service": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz",
+ "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==",
"devOptional": true,
"dependencies": {
- "array-flatten": "^2.1.0",
- "deep-equal": "^1.0.1",
+ "array-flatten": "^2.1.2",
"dns-equal": "^1.0.0",
- "dns-txt": "^2.0.2",
- "multicast-dns": "^6.0.1",
- "multicast-dns-service-types": "^1.1.0"
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
}
},
"node_modules/boolbase": {
@@ -22558,12 +22653,6 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
- "node_modules/buffer-indexof": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
- "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
- "devOptional": true
- },
"node_modules/buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -23155,9 +23244,15 @@
}
},
"node_modules/chokidar": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
- "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -24109,9 +24204,9 @@
"dev": true
},
"node_modules/connect-history-api-fallback": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
- "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
"devOptional": true,
"engines": {
"node": ">=0.8"
@@ -26872,23 +26967,6 @@
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
- "node_modules/deep-equal": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
- "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
- "devOptional": true,
- "dependencies": {
- "is-arguments": "^1.0.4",
- "is-date-object": "^1.0.1",
- "is-regex": "^1.0.4",
- "object-is": "^1.0.1",
- "object-keys": "^1.1.1",
- "regexp.prototype.flags": "^1.2.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/deep-equal-ident": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz",
@@ -27385,26 +27463,19 @@
"node_modules/dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
- "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+ "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==",
"devOptional": true
},
"node_modules/dns-packet": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
- "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
- "devOptional": true,
- "dependencies": {
- "ip": "^1.1.0",
- "safe-buffer": "^5.0.1"
- }
- },
- "node_modules/dns-txt": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
- "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz",
+ "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==",
"devOptional": true,
"dependencies": {
- "buffer-indexof": "^1.0.0"
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
}
},
"node_modules/doctrine": {
@@ -29741,39 +29812,11 @@
"node": ">= 0.10.0"
}
},
- "node_modules/express/node_modules/accepts": {
- "version": "1.3.7",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
- "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
- "dependencies": {
- "mime-types": "~2.1.24",
- "negotiator": "0.6.2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/express/node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
- "node_modules/express/node_modules/negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express/node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/express/node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -30474,14 +30517,6 @@
"node": ">= 0.8"
}
},
- "node_modules/finalhandler/node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/finalhandler/node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -33018,9 +33053,9 @@
}
},
"node_modules/http-parser-js": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz",
- "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==",
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==",
"devOptional": true
},
"node_modules/http-proxy": {
@@ -33075,12 +33110,12 @@
"devOptional": true
},
"node_modules/http-proxy-middleware": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz",
- "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+ "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"devOptional": true,
"dependencies": {
- "@types/http-proxy": "^1.17.5",
+ "@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
@@ -33088,6 +33123,14 @@
},
"engines": {
"node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
}
},
"node_modules/http-proxy-middleware/node_modules/braces": {
@@ -33136,13 +33179,13 @@
}
},
"node_modules/http-proxy-middleware/node_modules/micromatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
- "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"devOptional": true,
"dependencies": {
- "braces": "^3.0.1",
- "picomatch": "^2.2.3"
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
@@ -33701,33 +33744,6 @@
"resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ="
},
- "node_modules/internal-ip": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz",
- "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==",
- "devOptional": true,
- "dependencies": {
- "default-gateway": "^6.0.0",
- "ipaddr.js": "^1.9.1",
- "is-ip": "^3.1.0",
- "p-event": "^4.2.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/internal-ip?sponsor=1"
- }
- },
- "node_modules/internal-ip/node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "devOptional": true,
- "engines": {
- "node": ">= 0.10"
- }
- },
"node_modules/internal-slot": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
@@ -34086,27 +34102,6 @@
"node": ">=8"
}
},
- "node_modules/is-ip": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz",
- "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==",
- "devOptional": true,
- "dependencies": {
- "ip-regex": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-ip/node_modules/ip-regex": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
- "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
- "devOptional": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-lambda": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
@@ -34197,20 +34192,12 @@
"node": ">=4"
}
},
- "node_modules/is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "devOptional": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "devOptional": true,
+ "dev": true,
+ "peer": true,
"engines": {
"node": ">=8"
}
@@ -39757,11 +39744,11 @@
}
},
"node_modules/memfs": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.4.tgz",
- "integrity": "sha512-2mDCPhuduRPOxlfgsXF9V+uqC6Jgz8zt/bNe4d4W7d5f6pCzHrWkxLNr17jKGXd4+j2kQNsAG2HARPnt74sqVQ==",
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz",
+ "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==",
"dependencies": {
- "fs-monkey": "1.0.3"
+ "fs-monkey": "^1.0.3"
},
"engines": {
"node": ">= 4.0.0"
@@ -40465,19 +40452,19 @@
}
},
"node_modules/mime-db": {
- "version": "1.49.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
- "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==",
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
- "version": "2.1.32",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
- "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
- "mime-db": "1.49.0"
+ "mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
@@ -40923,9 +40910,9 @@
}
},
"node_modules/moment-timezone": {
- "version": "0.5.33",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz",
- "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==",
+ "version": "0.5.37",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
+ "integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"dependencies": {
"moment": ">= 2.9.0"
},
@@ -41006,24 +40993,18 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/multicast-dns": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
- "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
"devOptional": true,
"dependencies": {
- "dns-packet": "^1.3.1",
+ "dns-packet": "^5.2.2",
"thunky": "^1.0.2"
},
"bin": {
"multicast-dns": "cli.js"
}
},
- "node_modules/multicast-dns-service-types": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
- "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
- "devOptional": true
- },
"node_modules/multimatch": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
@@ -41177,9 +41158,9 @@
}
},
"node_modules/negotiator": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
- "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"engines": {
"node": ">= 0.6"
}
@@ -41335,12 +41316,12 @@
}
},
"node_modules/node-forge": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
- "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"devOptional": true,
"engines": {
- "node": ">= 6.0.0"
+ "node": ">= 6.13.0"
}
},
"node_modules/node-gyp": {
@@ -41997,15 +41978,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"devOptional": true
},
- "node_modules/npm-registry-fetch/node_modules/negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "devOptional": true,
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/npm-registry-fetch/node_modules/socks-proxy-agent": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
@@ -43268,9 +43240,9 @@
}
},
"node_modules/parseurl": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
- "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
@@ -43399,9 +43371,9 @@
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
},
"node_modules/picomatch": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
- "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
@@ -43566,45 +43538,6 @@
"node": ">=10"
}
},
- "node_modules/portfinder": {
- "version": "1.0.28",
- "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
- "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
- "devOptional": true,
- "dependencies": {
- "async": "^2.6.2",
- "debug": "^3.1.1",
- "mkdirp": "^0.5.5"
- },
- "engines": {
- "node": ">= 0.12.0"
- }
- },
- "node_modules/portfinder/node_modules/async": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
- "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
- "devOptional": true,
- "dependencies": {
- "lodash": "^4.17.14"
- }
- },
- "node_modules/portfinder/node_modules/debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)",
- "devOptional": true,
- "dependencies": {
- "ms": "^2.1.1"
- }
- },
- "node_modules/portfinder/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "devOptional": true
- },
"node_modules/posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -47931,7 +47864,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true,
+ "devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -48282,12 +48215,15 @@
"devOptional": true
},
"node_modules/selfsigned": {
- "version": "1.10.11",
- "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
- "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz",
+ "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==",
"devOptional": true,
"dependencies": {
- "node-forge": "^0.10.0"
+ "node-forge": "^1"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/semver": {
@@ -48432,14 +48368,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/serve-static/node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -48852,16 +48780,25 @@
}
},
"node_modules/sockjs": {
- "version": "0.3.21",
- "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
- "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==",
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
"devOptional": true,
"dependencies": {
"faye-websocket": "^0.11.3",
- "uuid": "^3.4.0",
+ "uuid": "^8.3.2",
"websocket-driver": "^0.7.4"
}
},
+ "node_modules/sockjs/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "devOptional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/socks": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz",
@@ -53051,36 +52988,40 @@
}
},
"node_modules/webpack-dev-server": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.2.0.tgz",
- "integrity": "sha512-iBaDkHBLfW3cEITeJWNkjZBrm+b5A3YLg8XVdNOdjUNABdXJwcsJv4dzKSnVf1q4Ch489+6epWVW6OcOyVfG7w==",
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.1.tgz",
+ "integrity": "sha512-FIzMq3jbBarz3ld9l7rbM7m6Rj1lOsgq/DyLGMX/fPEB1UBUPtf5iL/4eNfhx8YYJTRlzfv107UfWSWcBK5Odw==",
"devOptional": true,
"dependencies": {
+ "@types/bonjour": "^3.5.9",
+ "@types/connect-history-api-fallback": "^1.3.5",
+ "@types/express": "^4.17.13",
+ "@types/serve-index": "^1.9.1",
+ "@types/serve-static": "^1.13.10",
+ "@types/sockjs": "^0.3.33",
+ "@types/ws": "^8.5.1",
"ansi-html-community": "^0.0.8",
- "bonjour": "^3.5.0",
- "chokidar": "^3.5.1",
- "colorette": "^1.2.2",
+ "bonjour-service": "^1.0.11",
+ "chokidar": "^3.5.3",
+ "colorette": "^2.0.10",
"compression": "^1.7.4",
- "connect-history-api-fallback": "^1.6.0",
- "del": "^6.0.0",
- "express": "^4.17.1",
+ "connect-history-api-fallback": "^2.0.0",
+ "default-gateway": "^6.0.3",
+ "express": "^4.17.3",
"graceful-fs": "^4.2.6",
"html-entities": "^2.3.2",
- "http-proxy-middleware": "^2.0.0",
- "internal-ip": "^6.2.0",
+ "http-proxy-middleware": "^2.0.3",
"ipaddr.js": "^2.0.1",
"open": "^8.0.9",
"p-retry": "^4.5.0",
- "portfinder": "^1.0.28",
- "schema-utils": "^3.1.0",
- "selfsigned": "^1.10.11",
+ "rimraf": "^3.0.2",
+ "schema-utils": "^4.0.0",
+ "selfsigned": "^2.0.1",
"serve-index": "^1.9.1",
- "sockjs": "^0.3.21",
+ "sockjs": "^0.3.24",
"spdy": "^4.0.2",
- "strip-ansi": "^7.0.0",
- "url": "^0.11.0",
- "webpack-dev-middleware": "^5.1.0",
- "ws": "^8.1.0"
+ "webpack-dev-middleware": "^5.3.1",
+ "ws": "^8.4.2"
},
"bin": {
"webpack-dev-server": "bin/webpack-dev-server.js"
@@ -53088,6 +53029,10 @@
"engines": {
"node": ">= 12.13.0"
},
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
"peerDependencies": {
"webpack": "^4.37.0 || ^5.0.0"
},
@@ -53097,40 +53042,201 @@
}
}
},
- "node_modules/webpack-dev-server/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "node_modules/webpack-dev-server/node_modules/ajv": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+ "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"devOptional": true,
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
},
"funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/webpack-dev-server/node_modules/del": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
- "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
+ "node_modules/webpack-dev-server/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"devOptional": true,
"dependencies": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/body-parser": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
+ "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
+ "devOptional": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.10.3",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/colorette": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
+ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "devOptional": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 0.8"
}
},
+ "node_modules/webpack-dev-server/node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/express": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
+ "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
+ "devOptional": true,
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.0",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.10.3",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "devOptional": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "devOptional": true,
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "devOptional": true
+ },
"node_modules/webpack-dev-server/node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -53143,6 +53249,30 @@
"node": ">=8"
}
},
+ "node_modules/webpack-dev-server/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "devOptional": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/webpack-dev-server/node_modules/open": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz",
@@ -53160,41 +53290,155 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/webpack-dev-server/node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "node_modules/webpack-dev-server/node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/qs": {
+ "version": "6.10.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
+ "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"devOptional": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/webpack-dev-server/node_modules/strip-ansi": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
- "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
+ "node_modules/webpack-dev-server/node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"devOptional": true,
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
},
"engines": {
- "node": ">=12"
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/webpack-dev-server/node_modules/schema-utils": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+ "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.8.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
},
"funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "devOptional": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "devOptional": true,
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "devOptional": true
+ },
+ "node_modules/webpack-dev-server/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "devOptional": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "devOptional": true,
+ "engines": {
+ "node": ">=0.6"
}
},
"node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.1.0.tgz",
- "integrity": "sha512-oT660AR1gOnU/NTdUQi3EiGR0iXG7CFxmKsj3ylWCBA2khJ8LFHK+sKv3BZEsC11gl1eChsltRhzUq7nWj7XIQ==",
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
+ "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
"devOptional": true,
"dependencies": {
- "colorette": "^1.2.2",
- "memfs": "^3.2.2",
+ "colorette": "^2.0.10",
+ "memfs": "^3.4.3",
"mime-types": "^2.1.31",
"range-parser": "^1.2.1",
- "schema-utils": "^3.1.0"
+ "schema-utils": "^4.0.0"
},
"engines": {
"node": ">= 12.13.0"
@@ -57000,6 +57244,7 @@
"@ant-design/icons": "^4.2.2",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
+ "lodash": "^4.17.11",
"prop-types": "*",
"react": "^16.13.1",
"react-dom": "^16.13.1"
@@ -57021,9 +57266,13 @@
"regenerator-runtime": "^0.13.7",
"xss": "^1.0.10"
},
+ "devDependencies": {
+ "@testing-library/react": "^11.2.0"
+ },
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
+ "@types/classnames": "*",
"@types/react": "*",
"react": "^16.13.1",
"react-dom": "^16.13.1"
@@ -57051,6 +57300,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
+ "@types/lodash": "*",
"@types/react": "*",
"react": "^16.13.1"
}
@@ -62017,6 +62267,12 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "@leichtgewicht/ip-codec": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
+ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
+ "devOptional": true
+ },
"@lerna/add": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz",
@@ -63757,12 +64013,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "dev": true
- },
"node-gyp": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz",
@@ -65379,13 +65629,6 @@
"devOptional": true,
"peer": true
},
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "devOptional": true,
- "peer": true
- },
"node-gyp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -70957,6 +71200,7 @@
"version": "file:plugins/plugin-chart-table",
"requires": {
"@react-icons/all-files": "^4.1.0",
+ "@testing-library/react": "^11.2.0",
"@types/d3-array": "^2.9.0",
"@types/enzyme": "^3.10.5",
"@types/react-table": "^7.0.29",
@@ -71458,6 +71702,25 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "devOptional": true,
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/bonjour": {
+ "version": "3.5.10",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz",
+ "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==",
+ "devOptional": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/cheerio": {
"version": "0.22.21",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz",
@@ -71484,6 +71747,25 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
+ "@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "devOptional": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/connect-history-api-fallback": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz",
+ "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==",
+ "devOptional": true,
+ "requires": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
"@types/d3": {
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.38.tgz",
@@ -71612,6 +71894,29 @@
"devOptional": true,
"peer": true
},
+ "@types/express": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
+ "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
+ "devOptional": true,
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.18",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.17.30",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz",
+ "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==",
+ "devOptional": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
"@types/fetch-mock": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/@types/fetch-mock/-/fetch-mock-7.3.5.tgz",
@@ -71674,9 +71979,9 @@
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
},
"@types/http-proxy": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
- "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==",
+ "version": "1.17.9",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz",
+ "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==",
"devOptional": true,
"requires": {
"@types/node": "*"
@@ -71784,6 +72089,12 @@
"@types/unist": "*"
}
},
+ "@types/mime": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
+ "devOptional": true
+ },
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -71878,6 +72189,12 @@
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
},
+ "@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+ "devOptional": true
+ },
"@types/react": {
"version": "16.9.43",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.43.tgz",
@@ -72090,6 +72407,25 @@
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.30.tgz",
"integrity": "sha512-AnxLHewubLVzoF/A4qdxBGHCKifw8cY32iro3DQX9TPcetE95zBeVt3jnsvtvAUf1vwzMfwzp4t/L2yqPlnjkQ=="
},
+ "@types/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==",
+ "devOptional": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+ "devOptional": true,
+ "requires": {
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
"@types/shortid": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
@@ -72117,6 +72453,15 @@
"integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
"dev": true
},
+ "@types/sockjs": {
+ "version": "0.3.33",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz",
+ "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==",
+ "devOptional": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -72238,6 +72583,15 @@
}
}
},
+ "@types/ws": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
+ "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
+ "devOptional": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/yargs": {
"version": "15.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
@@ -73121,12 +73475,12 @@
"integrity": "sha512-9jN7+BijYKWO8fxfcG7QZh7js6V+g3OjkxMRHfKWNjjs85048VY4cd27Uoe6yk55P66L/z7Dflu5+YEApgMzkA=="
},
"accepts": {
- "version": "1.3.5",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
- "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
- "mime-types": "~2.1.18",
- "negotiator": "0.6.1"
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
}
},
"ace-builds": {
@@ -73321,7 +73675,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "dev": true,
+ "devOptional": true,
"requires": {
"ajv": "^8.0.0"
},
@@ -73330,7 +73684,7 @@
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz",
"integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==",
- "dev": true,
+ "devOptional": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -73342,7 +73696,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
+ "devOptional": true
}
}
},
@@ -74947,18 +75301,16 @@
}
}
},
- "bonjour": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz",
- "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=",
+ "bonjour-service": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz",
+ "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==",
"devOptional": true,
"requires": {
- "array-flatten": "^2.1.0",
- "deep-equal": "^1.0.1",
+ "array-flatten": "^2.1.2",
"dns-equal": "^1.0.0",
- "dns-txt": "^2.0.2",
- "multicast-dns": "^6.0.1",
- "multicast-dns-service-types": "^1.1.0"
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
}
},
"boolbase": {
@@ -75249,12 +75601,6 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
- "buffer-indexof": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz",
- "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==",
- "devOptional": true
- },
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -75704,9 +76050,9 @@
}
},
"chokidar": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
- "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -76462,9 +76808,9 @@
"dev": true
},
"connect-history-api-fallback": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz",
- "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
"devOptional": true
},
"console-browserify": {
@@ -78596,20 +78942,6 @@
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
- "deep-equal": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
- "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
- "devOptional": true,
- "requires": {
- "is-arguments": "^1.0.4",
- "is-date-object": "^1.0.1",
- "is-regex": "^1.0.4",
- "object-is": "^1.0.1",
- "object-keys": "^1.1.1",
- "regexp.prototype.flags": "^1.2.0"
- }
- },
"deep-equal-ident": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz",
@@ -79001,26 +79333,16 @@
"dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
- "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=",
+ "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==",
"devOptional": true
},
"dns-packet": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
- "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
- "devOptional": true,
- "requires": {
- "ip": "^1.1.0",
- "safe-buffer": "^5.0.1"
- }
- },
- "dns-txt": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz",
- "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=",
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz",
+ "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==",
"devOptional": true,
"requires": {
- "buffer-indexof": "^1.0.0"
+ "@leichtgewicht/ip-codec": "^2.0.1"
}
},
"doctrine": {
@@ -80866,30 +81188,11 @@
"vary": "~1.1.2"
},
"dependencies": {
- "accepts": {
- "version": "1.3.7",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
- "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
- "requires": {
- "mime-types": "~2.1.24",
- "negotiator": "0.6.2"
- }
- },
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
- },
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
- },
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -81448,11 +81751,6 @@
"unpipe": "~1.0.0"
},
"dependencies": {
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
- },
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@@ -83365,9 +83663,9 @@
}
},
"http-parser-js": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz",
- "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==",
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==",
"devOptional": true
},
"http-proxy": {
@@ -83410,12 +83708,12 @@
}
},
"http-proxy-middleware": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz",
- "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
+ "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"devOptional": true,
"requires": {
- "@types/http-proxy": "^1.17.5",
+ "@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
@@ -83453,13 +83751,13 @@
"devOptional": true
},
"micromatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
- "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"devOptional": true,
"requires": {
- "braces": "^3.0.1",
- "picomatch": "^2.2.3"
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
}
},
"to-regex-range": {
@@ -83877,26 +84175,6 @@
"resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ="
},
- "internal-ip": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz",
- "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==",
- "devOptional": true,
- "requires": {
- "default-gateway": "^6.0.0",
- "ipaddr.js": "^1.9.1",
- "is-ip": "^3.1.0",
- "p-event": "^4.2.0"
- },
- "dependencies": {
- "ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "devOptional": true
- }
- }
- },
"internal-slot": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
@@ -84159,23 +84437,6 @@
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"devOptional": true
},
- "is-ip": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz",
- "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==",
- "devOptional": true,
- "requires": {
- "ip-regex": "^4.0.0"
- },
- "dependencies": {
- "ip-regex": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
- "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
- "devOptional": true
- }
- }
- },
"is-lambda": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
@@ -84238,17 +84499,12 @@
"symbol-observable": "^1.1.0"
}
},
- "is-path-cwd": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
- "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
- "devOptional": true
- },
"is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "devOptional": true
+ "dev": true,
+ "peer": true
},
"is-plain-obj": {
"version": "1.1.0",
@@ -88579,11 +88835,11 @@
}
},
"memfs": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.4.tgz",
- "integrity": "sha512-2mDCPhuduRPOxlfgsXF9V+uqC6Jgz8zt/bNe4d4W7d5f6pCzHrWkxLNr17jKGXd4+j2kQNsAG2HARPnt74sqVQ==",
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz",
+ "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==",
"requires": {
- "fs-monkey": "1.0.3"
+ "fs-monkey": "^1.0.3"
}
},
"memoize-one": {
@@ -89017,16 +89273,16 @@
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
- "version": "1.49.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
- "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA=="
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
- "version": "2.1.32",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz",
- "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==",
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
- "mime-db": "1.49.0"
+ "mime-db": "1.52.0"
}
},
"mimic-fn": {
@@ -89372,9 +89628,9 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
- "version": "0.5.33",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz",
- "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==",
+ "version": "0.5.37",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
+ "integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"requires": {
"moment": ">= 2.9.0"
}
@@ -89444,21 +89700,15 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multicast-dns": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz",
- "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==",
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
"devOptional": true,
"requires": {
- "dns-packet": "^1.3.1",
+ "dns-packet": "^5.2.2",
"thunky": "^1.0.2"
}
},
- "multicast-dns-service-types": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
- "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
- "devOptional": true
- },
"multimatch": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
@@ -89583,9 +89833,9 @@
}
},
"negotiator": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
- "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"neo-async": {
"version": "2.6.2",
@@ -89729,9 +89979,9 @@
}
},
"node-forge": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
- "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"devOptional": true
},
"node-gyp": {
@@ -90255,12 +90505,6 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"devOptional": true
},
- "negotiator": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "devOptional": true
- },
"socks-proxy-agent": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
@@ -91240,9 +91484,9 @@
}
},
"parseurl": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
- "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascal-case": {
"version": "3.1.2",
@@ -91349,9 +91593,9 @@
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
},
"picomatch": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
- "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"pify": {
"version": "3.0.0",
@@ -91467,43 +91711,6 @@
"@babel/runtime": "^7.12.5"
}
},
- "portfinder": {
- "version": "1.0.28",
- "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
- "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
- "devOptional": true,
- "requires": {
- "async": "^2.6.2",
- "debug": "^3.1.1",
- "mkdirp": "^0.5.5"
- },
- "dependencies": {
- "async": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
- "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
- "devOptional": true,
- "requires": {
- "lodash": "^4.17.14"
- }
- },
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "devOptional": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "devOptional": true
- }
- }
- },
"posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -94804,7 +95011,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true
+ "devOptional": true
},
"require-main-filename": {
"version": "2.0.0",
@@ -95087,12 +95294,12 @@
"devOptional": true
},
"selfsigned": {
- "version": "1.10.11",
- "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz",
- "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz",
+ "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==",
"devOptional": true,
"requires": {
- "node-forge": "^0.10.0"
+ "node-forge": "^1"
}
},
"semver": {
@@ -95216,13 +95423,6 @@
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
- },
- "dependencies": {
- "parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
- }
}
},
"set-blocking": {
@@ -95552,14 +95752,22 @@
}
},
"sockjs": {
- "version": "0.3.21",
- "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz",
- "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==",
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
"devOptional": true,
"requires": {
"faye-websocket": "^0.11.3",
- "uuid": "^3.4.0",
+ "uuid": "^8.3.2",
"websocket-driver": "^0.7.4"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "devOptional": true
+ }
}
},
"socks": {
@@ -98920,60 +99128,201 @@
}
},
"webpack-dev-server": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.2.0.tgz",
- "integrity": "sha512-iBaDkHBLfW3cEITeJWNkjZBrm+b5A3YLg8XVdNOdjUNABdXJwcsJv4dzKSnVf1q4Ch489+6epWVW6OcOyVfG7w==",
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.10.1.tgz",
+ "integrity": "sha512-FIzMq3jbBarz3ld9l7rbM7m6Rj1lOsgq/DyLGMX/fPEB1UBUPtf5iL/4eNfhx8YYJTRlzfv107UfWSWcBK5Odw==",
"devOptional": true,
"requires": {
+ "@types/bonjour": "^3.5.9",
+ "@types/connect-history-api-fallback": "^1.3.5",
+ "@types/express": "^4.17.13",
+ "@types/serve-index": "^1.9.1",
+ "@types/serve-static": "^1.13.10",
+ "@types/sockjs": "^0.3.33",
+ "@types/ws": "^8.5.1",
"ansi-html-community": "^0.0.8",
- "bonjour": "^3.5.0",
- "chokidar": "^3.5.1",
- "colorette": "^1.2.2",
+ "bonjour-service": "^1.0.11",
+ "chokidar": "^3.5.3",
+ "colorette": "^2.0.10",
"compression": "^1.7.4",
- "connect-history-api-fallback": "^1.6.0",
- "del": "^6.0.0",
- "express": "^4.17.1",
+ "connect-history-api-fallback": "^2.0.0",
+ "default-gateway": "^6.0.3",
+ "express": "^4.17.3",
"graceful-fs": "^4.2.6",
"html-entities": "^2.3.2",
- "http-proxy-middleware": "^2.0.0",
- "internal-ip": "^6.2.0",
+ "http-proxy-middleware": "^2.0.3",
"ipaddr.js": "^2.0.1",
"open": "^8.0.9",
"p-retry": "^4.5.0",
- "portfinder": "^1.0.28",
- "schema-utils": "^3.1.0",
- "selfsigned": "^1.10.11",
+ "rimraf": "^3.0.2",
+ "schema-utils": "^4.0.0",
+ "selfsigned": "^2.0.1",
"serve-index": "^1.9.1",
- "sockjs": "^0.3.21",
+ "sockjs": "^0.3.24",
"spdy": "^4.0.2",
- "strip-ansi": "^7.0.0",
- "url": "^0.11.0",
- "webpack-dev-middleware": "^5.1.0",
- "ws": "^8.1.0"
+ "webpack-dev-middleware": "^5.3.1",
+ "ws": "^8.4.2"
},
"dependencies": {
- "ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "ajv": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
+ "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
+ "devOptional": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "devOptional": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.3"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"devOptional": true
},
- "del": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
- "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
+ "body-parser": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
+ "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
"devOptional": true,
"requires": {
- "globby": "^11.0.1",
- "graceful-fs": "^4.2.4",
- "is-glob": "^4.0.1",
- "is-path-cwd": "^2.2.0",
- "is-path-inside": "^3.0.2",
- "p-map": "^4.0.0",
- "rimraf": "^3.0.2",
- "slash": "^3.0.0"
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.10.3",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ }
+ },
+ "bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "devOptional": true
+ },
+ "colorette": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
+ "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
+ "devOptional": true
+ },
+ "content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "devOptional": true,
+ "requires": {
+ "safe-buffer": "5.2.1"
+ }
+ },
+ "cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "devOptional": true
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "devOptional": true
+ },
+ "destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "devOptional": true
+ },
+ "express": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
+ "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
+ "devOptional": true,
+ "requires": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.0",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.10.3",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "devOptional": true,
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
}
},
+ "http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "devOptional": true,
+ "requires": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "devOptional": true
+ },
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -98983,6 +99332,27 @@
"is-docker": "^2.0.0"
}
},
+ "json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "devOptional": true
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "devOptional": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "devOptional": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
"open": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/open/-/open-8.2.1.tgz",
@@ -98994,32 +99364,113 @@
"is-wsl": "^2.2.0"
}
},
- "slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"devOptional": true
},
- "strip-ansi": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
- "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
+ "qs": {
+ "version": "6.10.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
+ "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
+ "devOptional": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"devOptional": true,
"requires": {
- "ansi-regex": "^6.0.1"
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
}
},
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "devOptional": true
+ },
+ "schema-utils": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
+ "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+ "devOptional": true,
+ "requires": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.8.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.0.0"
+ }
+ },
+ "send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "devOptional": true,
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ }
+ },
+ "serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "devOptional": true,
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "devOptional": true
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "devOptional": true
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "devOptional": true
+ },
"webpack-dev-middleware": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.1.0.tgz",
- "integrity": "sha512-oT660AR1gOnU/NTdUQi3EiGR0iXG7CFxmKsj3ylWCBA2khJ8LFHK+sKv3BZEsC11gl1eChsltRhzUq7nWj7XIQ==",
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
+ "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
"devOptional": true,
"requires": {
- "colorette": "^1.2.2",
- "memfs": "^3.2.2",
+ "colorette": "^2.0.10",
+ "memfs": "^3.4.3",
"mime-types": "^2.1.31",
"range-parser": "^1.2.1",
- "schema-utils": "^3.1.0"
+ "schema-utils": "^4.0.0"
}
},
"ws": {
From 2cab6ee234784875e8cf53e8f4e8079d8e6b8f18 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 6 Dec 2022 11:30:13 -0500
Subject: [PATCH 44/76] fixed import errors and changed tag crud so they
display as antdtags
---
.../src/components/Tags/utils.tsx | 91 +++++++++++++++++++
.../components/PropertiesModal/index.tsx | 3 +-
.../components/PropertiesModal/index.tsx | 2 +-
.../src/views/CRUD/chart/ChartList.tsx | 2 +-
.../views/CRUD/dashboard/DashboardList.tsx | 2 +-
.../src/views/CRUD/tags/TagList.tsx | 20 ++--
6 files changed, 101 insertions(+), 19 deletions(-)
create mode 100644 superset-frontend/src/components/Tags/utils.tsx
diff --git a/superset-frontend/src/components/Tags/utils.tsx b/superset-frontend/src/components/Tags/utils.tsx
new file mode 100644
index 0000000000000..af2853c5c0b7b
--- /dev/null
+++ b/superset-frontend/src/components/Tags/utils.tsx
@@ -0,0 +1,91 @@
+/**
+ * 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;
+};
+
+export const tagToSelectOption = (
+ item: Tag & { table_name: string },
+): SelectTagsValue => ({
+ value: item.id,
+ label: 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/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 448d99b472f1d..119140068f92c 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -41,10 +41,9 @@ import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeMo
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import withToasts from 'src/components/MessageToasts/withToasts';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
-import { loadTags } from 'src/components/ObjectTags';
import TagType from 'src/types/TagType';
import { addTag, deleteTaggedObjects, fetchTags, OBJECT_TYPES } from 'src/tags';
-import { TagsList } from 'src/components/Tags';
+import { loadTags } from 'src/components/Tags/utils';
const StyledFormItem = styled(FormItem)`
margin-bottom: 0;
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index dc22c10a80af1..9d3f802f1b6c0 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -33,7 +33,7 @@ import {
import Chart, { Slice } from 'src/types/Chart';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import withToasts from 'src/components/MessageToasts/withToasts';
-import { loadTags } from 'src/components/ObjectTags';
+import { loadTags } from 'src/components/Tags/utils';
import { addTag, deleteTaggedObjects, fetchTags, OBJECT_TYPES } from 'src/tags';
import TagType from 'src/types/TagType';
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 572306a1a0877..8cf8efc1b9cf5 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -71,7 +71,7 @@ import { GenericLink } from 'src/components/GenericLink/GenericLink';
import { bootstrapData } from 'src/preamble';
import Owner from 'src/types/Owner';
import { OBJECT_TYPES } from 'src/tags';
-import { loadTags } from 'src/components/ObjectTags';
+import { loadTags } from 'src/components/Tags/utils';
import ChartCard from './ChartCard';
const FlexRowContainer = styled.div`
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index 31f76c52d77e0..6c7f4e6558034 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -53,7 +53,7 @@ import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { bootstrapData } from 'src/preamble';
-import { loadTags } from 'src/components/ObjectTags';
+import { loadTags } from 'src/components/Tags/utils';
import DashboardCard from './DashboardCard';
import { DashboardStatus } from './types';
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 3c6ca081a1843..728c483355d99 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -39,6 +39,7 @@ import { Tooltip } from 'src/components/Tooltip';
import FacePile from 'src/components/FacePile';
import { Link } from 'react-router-dom';
import { deleteTags } from 'src/tags';
+import { Tag as AntdTag } from 'antd';
import { Tag } from '../types';
import TagCard from './TagCard';
@@ -100,24 +101,15 @@ function TagList(props: TagListProps) {
original: { name: tagName },
},
}: any) => (
- {tagName}
+
+
+ {tagName}
+
+
),
Header: t('Name'),
accessor: 'name',
},
- {
- Cell: ({
- row: {
- original: {
- changed_by_name: changedByName,
- changed_by_url: changedByUrl,
- },
- },
- }: any) => {changedByName},
- Header: t('Modified by'),
- accessor: 'changed_by.first_name',
- size: 'xl',
- },
{
Cell: ({
row: {
From c4b4fa2751aba4d5389f220b788f3e879ced6983 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 6 Dec 2022 12:02:38 -0500
Subject: [PATCH 45/76] fixed type errors
---
superset-frontend/src/components/Tags/Tag.tsx | 2 +-
.../src/views/CRUD/chart/ChartList.tsx | 8 ++---
.../views/CRUD/dashboard/DashboardList.tsx | 2 +-
.../CRUD/data/savedquery/SavedQueryList.tsx | 1 +
.../src/views/CRUD/tags/TagCard.tsx | 29 +++++++------------
.../src/views/CRUD/tags/TagList.tsx | 1 -
superset-frontend/src/views/CRUD/types.ts | 10 +++++--
7 files changed, 23 insertions(+), 30 deletions(-)
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
index db072feba5804..4070f9d1c2676 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { styled, SupersetTheme } from '@superset-ui/core';
+import { styled } from '@superset-ui/core';
import TagType from 'src/types/TagType';
import AntdTag from 'antd/lib/tag';
import React, { useMemo } from 'react';
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 8cf8efc1b9cf5..8a24bc6efd344 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -30,7 +30,6 @@ import { uniqBy } from 'lodash';
import moment from 'moment';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
- Actions,
createErrorHandler,
createFetchRelated,
handleChartDelete,
@@ -70,7 +69,6 @@ import CertifiedBadge from 'src/components/CertifiedBadge';
import { GenericLink } from 'src/components/GenericLink/GenericLink';
import { bootstrapData } from 'src/preamble';
import Owner from 'src/types/Owner';
-import { OBJECT_TYPES } from 'src/tags';
import { loadTags } from 'src/components/Tags/utils';
import ChartCard from './ChartCard';
@@ -158,7 +156,7 @@ type ChartLinkedDashboard = {
dashboard_title: string;
};
-const Actions = styled.div`
+const StyledActions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
@@ -493,7 +491,7 @@ function ChartList(props: ChartListProps) {
}
return (
-
+
{canDelete && (
)}
-
+
);
},
Header: t('Actions'),
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index 6c7f4e6558034..28c1001995791 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { styled, SupersetClient, t } from '@superset-ui/core';
+import { SupersetClient, t } from '@superset-ui/core';
import React, { useState, useMemo, useCallback } from 'react';
import { Link } from 'react-router-dom';
import rison from 'rison';
diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
index 15e4ef7361e02..b88d816943ae2 100644
--- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
+++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
@@ -480,6 +480,7 @@ function SavedQueryList({
{
Header: t('Tags'),
id: 'tags',
+ key: 'tags',
input: 'search',
operator: FilterOperator.savedQueryTags,
},
diff --git a/superset-frontend/src/views/CRUD/tags/TagCard.tsx b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
index 617559a298070..84e600262e642 100644
--- a/superset-frontend/src/views/CRUD/tags/TagCard.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagCard.tsx
@@ -17,19 +17,17 @@
* under the License.
*/
import React from 'react';
-import { Link, useHistory } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { t, useTheme } from '@superset-ui/core';
-import { handleDashboardDelete, CardStyles } from 'src/views/CRUD/utils';
+import { CardStyles } from 'src/views/CRUD/utils';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { AntdDropdown } from 'src/components';
import { Menu } from 'src/components/Menu';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ListViewCard from 'src/components/ListViewCard';
import Icons from 'src/components/Icons';
-import Label from 'src/components/Label';
-import FacePile from 'src/components/FacePile';
-import FaveStar from 'src/components/FaveStar';
import { Tag } from 'src/views/CRUD/types';
+import { deleteTags } from 'src/tags';
interface TagCardProps {
tag: Tag;
@@ -55,9 +53,12 @@ function TagCard({
addSuccessToast,
showThumbnails,
}: TagCardProps) {
- const history = useHistory();
const canDelete = hasPerm('can_write');
- const canExport = hasPerm('can_export');
+
+ const handleTagDelete = (tag: Tag) => {
+ deleteTags([tag], addSuccessToast, addDangerToast);
+ refreshData();
+ };
const theme = useTheme();
const menu = (
@@ -71,16 +72,7 @@ function TagCard({
{t('Are you sure you want to delete')} {tag.name}?
>
}
- onConfirm={() =>
- handleTagDelete(
- tag,
- refreshData,
- addSuccessToast,
- addDangerToast,
- tagFilter,
- userId,
- )
- }
+ onConfirm={() => handleTagDelete(tag)}
>
{confirmDelete => (
{
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 728c483355d99..8001f3822431c 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -69,7 +69,6 @@ function TagList(props: TagListProps) {
resourceCollection: tags,
bulkSelectEnabled,
},
- setResourceCollection: setTags,
hasPerm,
fetchData,
toggleBulkSelect,
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 3161e255ae49a..c3ef45133c061 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -133,11 +133,15 @@ export enum QueryObjectColumns {
tracking_url = 'tracking_url',
}
+export type ImportResourceName =
+ | 'chart'
+ | 'dashboard'
+ | 'database'
+ | 'dataset'
+ | 'saved_query';
+
export interface Tag {
- changed_by_name: string;
- changed_by_url: string;
changed_on_delta_humanized: string;
- changed_by: string;
name: string;
id: number;
created_by: object;
From a060998785aa7ec263c929182eefc9aa081fa5bc Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 6 Dec 2022 12:56:23 -0500
Subject: [PATCH 46/76] fixed api test error
---
tests/integration_tests/tags/api_tests.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index d3ccdc3302519..ec580595aaafc 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -30,7 +30,7 @@
from superset.common.db_query_status import QueryStatus
from superset.models.core import Database
from superset.utils.database import get_example_database, get_main_database
-from superset.models.sql_lab import Tag
+from superset.tags.models import Tag
from tests.integration_tests.base_tests import SupersetTestCase
From adcde8100c2d1f0b1522af96cb9bb21657f8f5a0 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 6 Dec 2022 15:07:18 -0500
Subject: [PATCH 47/76] fixed frontend tests
---
.../src/components/Tags/TagsList.test.tsx | 15 +++++++++------
.../src/components/Tags/TagsList.tsx | 2 ++
.../PropertiesModal/PropertiesModal.test.tsx | 15 ++++++++-------
.../components/PropertiesModal/index.tsx | 2 +-
4 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx b/superset-frontend/src/components/Tags/TagsList.test.tsx
index 4e2ffd2567d5d..988f30f43b5bf 100644
--- a/superset-frontend/src/components/Tags/TagsList.test.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.test.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
-import { render, screen } from 'spec/helpers/testing-library';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
import TagsList, { TagsListProps } from './TagsList';
const testTags = [
@@ -49,16 +49,19 @@ const mockedProps: TagsListProps = {
maxTags: 5,
};
-const findAllTags = () => screen.getAllByRole('link')! as HTMLElement[];
+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', () => {
+test('should render 5 elements', async () => {
render();
- const tagsListItems = findAllTags();
+ const tagsListItems = await findAllTags();
expect(tagsListItems).toHaveLength(5);
expect(tagsListItems[0]).toHaveTextContent(testTags[0].name);
expect(tagsListItems[1]).toHaveTextContent(testTags[1].name);
@@ -67,9 +70,9 @@ test('should render 5 elements', () => {
expect(tagsListItems[4]).toHaveTextContent(testTags[4].name);
});
-test('should render 3 elements when maxTags is set to 3', () => {
+test('should render 3 elements when maxTags is set to 3', async () => {
render();
- const tagsListItems = findAllTags();
+ 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
index 028ae5c5a832f..c5c669ee00d61 100644
--- a/superset-frontend/src/components/Tags/TagsList.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.tsx
@@ -75,6 +75,7 @@ const TagsList = ({
{tags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
(
{
expect(
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
- expect(screen.getAllByRole('heading')).toHaveLength(5);
+ expect(screen.getAllByRole('heading')).toHaveLength(6);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
@@ -217,7 +217,8 @@ test('should render - FeatureFlag enabled', async () => {
expect(
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
- expect(screen.getAllByRole('heading')).toHaveLength(4);
+ expect(screen.getByRole('heading', { name: 'Tags' })).toBeInTheDocument();
+ expect(screen.getAllByRole('heading')).toHaveLength(5);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
@@ -226,7 +227,7 @@ test('should render - FeatureFlag enabled', async () => {
expect(screen.getAllByRole('button')).toHaveLength(4);
expect(screen.getAllByRole('textbox')).toHaveLength(4);
- expect(screen.getAllByRole('combobox')).toHaveLength(2);
+ expect(screen.getAllByRole('combobox')).toHaveLength(3);
expect(spyColorSchemeControlWrapper).toBeCalledWith(
expect.objectContaining({ colorScheme: 'supersetColors' }),
@@ -245,10 +246,10 @@ test('should open advance', async () => {
).toBeInTheDocument();
expect(screen.getAllByRole('textbox')).toHaveLength(4);
- expect(screen.getAllByRole('combobox')).toHaveLength(2);
+ expect(screen.getAllByRole('combobox')).toHaveLength(3);
userEvent.click(screen.getByRole('button', { name: 'Advanced' }));
expect(screen.getAllByRole('textbox')).toHaveLength(5);
- expect(screen.getAllByRole('combobox')).toHaveLength(2);
+ expect(screen.getAllByRole('combobox')).toHaveLength(3);
});
test('should close modal', async () => {
@@ -382,7 +383,7 @@ test('should show all roles', async () => {
useRedux: true,
});
- expect(screen.getAllByRole('combobox')).toHaveLength(2);
+ expect(screen.getAllByRole('combobox')).toHaveLength(3);
expect(
screen.getByRole('combobox', { name: SupersetCore.t('Roles') }),
).toBeInTheDocument();
@@ -415,7 +416,7 @@ test('should show active owners with dashboard rbac', async () => {
useRedux: true,
});
- expect(screen.getAllByRole('combobox')).toHaveLength(2);
+ expect(screen.getAllByRole('combobox')).toHaveLength(3);
expect(
screen.getByRole('combobox', { name: SupersetCore.t('Owners') }),
).toBeInTheDocument();
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 119140068f92c..97916d444ac21 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -574,7 +574,7 @@ const PropertiesModal = ({
},
(tags: TagType[]) => setTags(tags),
(error: Response) => {
- handleErrorResponse(error);
+ addDangerToast(`Error fetching tags: ${error.text}`);
},
);
} catch (error: any) {
From 14a24efbf8e9622bad68dc76a5cc2a29aa066eb9 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 08:24:24 -0500
Subject: [PATCH 48/76] fixed unused import
---
superset-frontend/src/components/Tags/TagsList.test.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx b/superset-frontend/src/components/Tags/TagsList.test.tsx
index 988f30f43b5bf..f67dbce294663 100644
--- a/superset-frontend/src/components/Tags/TagsList.test.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.test.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import { render, waitFor } from 'spec/helpers/testing-library';
import TagsList, { TagsListProps } from './TagsList';
const testTags = [
From 8e00386cbaf314294dd0d903b0adf15b6556bce5 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 09:37:52 -0500
Subject: [PATCH 49/76] fixed feature flag set
---
superset/config.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/superset/config.py b/superset/config.py
index ec8f6dd539307..f163997c6ee4b 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -417,7 +417,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]:
"DASHBOARD_CACHE": False,
"REMOVE_SLICE_LEVEL_LABEL_COLORS": False,
"SHARE_QUERIES_VIA_KV_STORE": False,
- "TAGGING_SYSTEM": True,
+ "TAGGING_SYSTEM": False,
"SQLLAB_BACKEND_PERSISTENCE": True,
"LISTVIEWS_DEFAULT_CARD_VIEW": False,
# When True, this flag allows display of HTML tags in Markdown components
From b07343ce99d2392398e95270cdd829fb99b2cc17 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 15:52:24 -0500
Subject: [PATCH 50/76] feature flag fixes
---
superset/charts/api.py | 17 +++++++++--------
superset/connectors/sqla/models.py | 1 -
superset/dashboards/api.py | 12 +++++++-----
superset/initialization/__init__.py | 4 ----
superset/models/dashboard.py | 15 ++++++++-------
superset/models/slice.py | 15 ++++++++-------
superset/models/sql_lab.py | 17 +++++++++--------
superset/queries/saved_queries/api.py | 13 ++++++++-----
superset/tags/models.py | 1 -
tests/integration_tests/fixtures/tags.py | 1 +
10 files changed, 50 insertions(+), 46 deletions(-)
diff --git a/superset/charts/api.py b/superset/charts/api.py
index ed31a9b80c824..20eb5cda502f4 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -130,9 +130,6 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"owners.id",
"owners.last_name",
"owners.username",
- "tags.id",
- "tags.name",
- "tags.type",
"dashboards.id",
"dashboards.dashboard_title",
"params",
@@ -143,6 +140,9 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"query_context",
"is_managed_externally",
]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ show_columns += ["tags.id","tags.name","tags.type"]
+
show_select_columns = show_columns + ["table.id"]
list_columns = [
"is_managed_externally",
@@ -181,13 +181,12 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"slice_name",
"table.default_endpoint",
"table.table_name",
- "tags.id",
- "tags.name",
- "tags.type",
"thumbnail_url",
"url",
"viz_type",
]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ list_columns += ["tags.id", "tags.name", "tags.type"]
list_select_columns = list_columns + ["changed_by_fk", "changed_on"]
order_columns = [
"changed_by.first_name",
@@ -216,17 +215,19 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"owners",
"dashboards",
"slice_name",
- "tags",
"viz_type",
]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_columns += ['tags']
base_order = ("changed_on", "desc")
base_filters = [["id", ChartFilter, lambda: []]]
search_filters = {
"id": [ChartFavoriteFilter, ChartCertifiedFilter],
"slice_name": [ChartAllTextFilter],
- "tags": [ChartTagFilter],
"created_by": [ChartHasCreatedByFilter, ChartCreatedByMeFilter],
}
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_filters['tags'] = [ChartTagFilter]
# Will just affect _info endpoint
edit_columns = ["slice_name"]
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 12ddce7ecb617..fd5942c51a5a0 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -2253,7 +2253,6 @@ def write_shadow_dataset(
sa.event.listen(SqlMetric, "after_update", SqlaTable.update_column)
sa.event.listen(TableColumn, "after_update", SqlaTable.update_column)
-
RLSFilterRoles = Table(
"rls_filter_roles",
metadata,
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 0cd9e16effdbf..64df100620f44 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -181,11 +181,10 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"owners.email",
"roles.id",
"roles.name",
- "tags.id",
- "tags.name",
- "tags.type",
"is_managed_externally",
]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ list_columns += ["tags.id", "tags.name", "tags.type"]
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
order_columns = [
"changed_by.first_name",
@@ -219,14 +218,17 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"published",
"roles",
"slug",
- "tags",
)
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_columns += ("tags",)
search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter],
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
"created_by": [DashboardCreatedByMeFilter, DashboardHasCreatedByFilter],
- "tags": [DashboardTagFilter],
}
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_filters['tags'] = [DashboardTagFilter]
+
base_order = ("changed_on", "desc")
add_model_schema = DashboardPostSchema()
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index d5e40c9d0abcb..d363e5ec1a702 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -368,8 +368,6 @@ def init_views(self) -> None:
"All Entities",
label=__("All Entities"),
icon="",
- category="Tagging",
- category_label=__("Tagging"),
category_icon="",
menu_cond=lambda: feature_flag_manager.is_feature_enabled(
"TAGGING_SYSTEM"
@@ -380,8 +378,6 @@ def init_views(self) -> None:
"Tags",
label=__("Tags"),
icon="",
- category="Tagging",
- category_label=__("Tagging"),
category_icon="",
menu_cond=lambda: feature_flag_manager.is_feature_enabled(
"TAGGING_SYSTEM"
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 393a3df12cce4..299f2c51e756e 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -148,13 +148,14 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
Slice, secondary=dashboard_slices, backref="dashboards"
)
owners = relationship(security_manager.user_model, secondary=dashboard_user)
- tags = relationship(
- "Tag",
- secondary="tagged_object",
- primaryjoin="and_(Dashboard.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
- "TaggedObject.object_type == 'dashboard')",
- )
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ tags = relationship(
+ "Tag",
+ secondary="tagged_object",
+ primaryjoin="and_(Dashboard.id == TaggedObject.object_id)",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
+ "TaggedObject.object_type == 'dashboard')",
+ )
published = Column(Boolean, default=False)
is_managed_externally = Column(Boolean, nullable=False, default=False)
external_url = Column(Text, nullable=True)
diff --git a/superset/models/slice.py b/superset/models/slice.py
index 8d65b563d155b..ff89ed9b73a08 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -97,13 +97,14 @@ class Slice( # pylint: disable=too-many-public-methods
security_manager.user_model, foreign_keys=[last_saved_by_fk]
)
owners = relationship(security_manager.user_model, secondary=slice_user)
- tags = relationship(
- "Tag",
- secondary="tagged_object",
- primaryjoin="and_(Slice.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
- "TaggedObject.object_type == 'chart')",
- )
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ tags = relationship(
+ "Tag",
+ secondary="tagged_object",
+ primaryjoin="and_(Slice.id == TaggedObject.object_id)",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
+ "TaggedObject.object_type == 'chart')",
+ )
table = relationship(
"SqlaTable",
foreign_keys=[datasource_id],
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index 1d7dab2677f1f..7f05d60afe315 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -41,7 +41,7 @@
from sqlalchemy.engine.url import URL
from sqlalchemy.orm import backref, relationship
-from superset import security_manager
+from superset import is_feature_enabled, security_manager
from superset.jinja_context import BaseTemplateProcessor, get_template_processor
from superset.models.helpers import (
AuditMixinNullable,
@@ -364,13 +364,14 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
)
rows = Column(Integer, nullable=True)
last_run = Column(DateTime, nullable=True)
- tags = relationship(
- "Tag",
- secondary="tagged_object",
- primaryjoin="and_(SavedQuery.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
- "TaggedObject.object_type == 'query')",
- )
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ tags = relationship(
+ "Tag",
+ secondary="tagged_object",
+ primaryjoin="and_(SavedQuery.id == TaggedObject.object_id)",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
+ "TaggedObject.object_type == 'saved_query')",
+ )
export_parent = "database"
export_fields = [
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index f42c6c38c2d3d..343d17fba4e46 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -26,6 +26,7 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_babel import ngettext
+from superset import is_feature_enabled
from superset.commands.importers.exceptions import (
IncorrectFormatError,
NoValidFilesFoundError,
@@ -117,10 +118,9 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"schema",
"sql",
"sql_tables",
- "tags.id",
- "tags.name",
- "tags.type",
]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ list_columns += ["tags.id", "tags.name", "tags.type"]
list_select_columns = list_columns + ["changed_by_fk", "changed_on"]
add_columns = [
"db_id",
@@ -144,12 +144,15 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
"last_run_delta_humanized",
]
- search_columns = ["id", "database", "label", "schema", "created_by", "tags"]
+ search_columns = ["id", "database", "label", "schema", "created_by"]
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_columns += ['tags']
search_filters = {
"id": [SavedQueryFavoriteFilter],
"label": [SavedQueryAllTextFilter],
- "tags": [SavedQueryTagFilter]
}
+ if is_feature_enabled("TAGGING_SYSTEM"):
+ search_filters['tags'] = [SavedQueryTagFilter]
apispec_parameter_schemas = {
"get_delete_ids_schema": get_delete_ids_schema,
diff --git a/superset/tags/models.py b/superset/tags/models.py
index 283cdfede2616..1258f3b96110c 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -84,7 +84,6 @@ class Tag(Model, AuditMixinNullable):
name = Column(String(250), unique=True)
type = Column(Enum(TagTypes))
-
class TaggedObject(Model, AuditMixinNullable):
"""An association between an object and a tag."""
diff --git a/tests/integration_tests/fixtures/tags.py b/tests/integration_tests/fixtures/tags.py
index 57fd4ec7196e2..cc3969ac2bea0 100644
--- a/tests/integration_tests/fixtures/tags.py
+++ b/tests/integration_tests/fixtures/tags.py
@@ -31,3 +31,4 @@ def with_tagging_system_feature():
yield
app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = False
clear_sqla_event_listeners()
+ yield
From 6d9dca4ed88b8ec5c2726d29601bf551ec48c25c Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 16:05:49 -0500
Subject: [PATCH 51/76] fixed pylint error
---
superset/dashboards/api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 64df100620f44..b49cd5346e791 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -184,7 +184,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"is_managed_externally",
]
if is_feature_enabled("TAGGING_SYSTEM"):
- list_columns += ["tags.id", "tags.name", "tags.type"]
+ list_columns += ["tags.id", "tags.name", "tags.type"]
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
order_columns = [
"changed_by.first_name",
From 6ad36535230cae04b4714f5433053847d7e73d86 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 16:13:16 -0500
Subject: [PATCH 52/76] fixed pylint error v2
---
superset/dashboards/api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index b49cd5346e791..f7286c6c224c0 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -184,7 +184,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"is_managed_externally",
]
if is_feature_enabled("TAGGING_SYSTEM"):
- list_columns += ["tags.id", "tags.name", "tags.type"]
+ list_columns += ["tags.id", "tags.name", "tags.type"]
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
order_columns = [
"changed_by.first_name",
From b0fb771e49b1f85f8563e14b04e080d5b32ece5d Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Wed, 7 Dec 2022 16:25:07 -0500
Subject: [PATCH 53/76] removed accidental test fixture error
---
tests/integration_tests/fixtures/tags.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/integration_tests/fixtures/tags.py b/tests/integration_tests/fixtures/tags.py
index cc3969ac2bea0..57fd4ec7196e2 100644
--- a/tests/integration_tests/fixtures/tags.py
+++ b/tests/integration_tests/fixtures/tags.py
@@ -31,4 +31,3 @@ def with_tagging_system_feature():
yield
app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = False
clear_sqla_event_listeners()
- yield
From 2093ecc7c85fc297a3a15b7d52a4c46d657144a3 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 8 Dec 2022 09:21:16 -0500
Subject: [PATCH 54/76] fixing tagging tests
---
tests/integration_tests/tags/api_tests.py | 34 ++++++++++++++++-------
1 file changed, 24 insertions(+), 10 deletions(-)
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index ec580595aaafc..cc02abe62941b 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -30,12 +30,24 @@
from superset.common.db_query_status import QueryStatus
from superset.models.core import Database
from superset.utils.database import get_example_database, get_main_database
-from superset.tags.models import Tag
+from superset.tags.models import Tag, TagTypes
from tests.integration_tests.base_tests import SupersetTestCase
TAGS_FIXTURE_COUNT = 10
+TAGS_LIST_COLUMNS = [
+ 'id',
+ 'name',
+ 'type',
+ 'changed_by.first_name',
+ 'changed_by.last_name',
+ 'changed_on_delta_humanized',
+ 'created_by.first_name',
+ 'created_by.last_name'
+]
+
+
class TestTagApi(SupersetTestCase):
def insert_tag(
self,
@@ -53,6 +65,9 @@ def insert_tag(
@pytest.fixture()
def create_tags(self):
with self.create_app().app_context():
+ # clear tags table
+ tags = db.session.query(Tag).delete()
+ db.session.commit()
tags = []
for cx in range(TAGS_FIXTURE_COUNT):
tags.append(
@@ -82,13 +97,16 @@ def test_get_tag(self):
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200)
expected_result = {
+ "changed_by": None,
+ "changed_on_delta_humanized": "now",
+ "created_by": None,
"id": tag.id,
"name": "test get tag",
- "type": 'custom'
+ "type": TagTypes.custom.value
}
data = json.loads(rv.data.decode("utf-8"))
- for key, value in data["result"].items():
- self.assertEqual(value, expected_result[key])
+ for key, value in expected_result.items():
+ self.assertEqual(value, data["result"][key])
# rollback changes
db.session.delete(tag)
db.session.commit()
@@ -117,10 +135,6 @@ def test_get_list_tag(self):
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
- assert data["count"] == TAGS_FIXTURE_COUNT
+ assert data['count'] == TAGS_FIXTURE_COUNT
# check expected columns
- assert sorted(list(data["result"][0].keys())) == [
- "id",
- "name",
- "type",
- ]
+ assert data["list_columns"] == TAGS_LIST_COLUMNS
From 6311bd2bc42da0fb1cea08d3480751dd0d3d514b Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Thu, 8 Dec 2022 11:18:12 -0500
Subject: [PATCH 55/76] precommit fixes
---
superset/charts/api.py | 8 +--
superset/charts/filters.py | 4 +-
superset/charts/schemas.py | 45 ++++++-----------
superset/dashboards/api.py | 36 +++++++++-----
superset/dashboards/filters.py | 4 +-
superset/dashboards/schemas.py | 15 ++----
superset/initialization/__init__.py | 13 ++---
superset/models/dashboard.py | 4 +-
superset/models/slice.py | 4 +-
superset/models/sql_lab.py | 4 +-
superset/queries/saved_queries/api.py | 6 +--
superset/queries/saved_queries/filters.py | 4 +-
superset/tags/api.py | 59 ++++++++++-------------
superset/tags/commands/create.py | 10 ++--
superset/tags/commands/exceptions.py | 6 +--
superset/tags/dao.py | 16 ++----
superset/tags/models.py | 15 +++---
superset/tags/schemas.py | 3 +-
superset/views/all_entities.py | 4 +-
superset/views/base_api.py | 8 ++-
superset/views/tags.py | 20 +++-----
tests/integration_tests/tags/api_tests.py | 26 +++++-----
22 files changed, 138 insertions(+), 176 deletions(-)
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 20eb5cda502f4..e0c01c18749ca 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -52,9 +52,9 @@
ChartCertifiedFilter,
ChartCreatedByMeFilter,
ChartFavoriteFilter,
- ChartTagFilter,
ChartFilter,
ChartHasCreatedByFilter,
+ ChartTagFilter,
)
from superset.charts.schemas import (
CHART_SCHEMAS,
@@ -141,7 +141,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"is_managed_externally",
]
if is_feature_enabled("TAGGING_SYSTEM"):
- show_columns += ["tags.id","tags.name","tags.type"]
+ show_columns += ["tags.id", "tags.name", "tags.type"]
show_select_columns = show_columns + ["table.id"]
list_columns = [
@@ -218,7 +218,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"viz_type",
]
if is_feature_enabled("TAGGING_SYSTEM"):
- search_columns += ['tags']
+ search_columns += ["tags"]
base_order = ("changed_on", "desc")
base_filters = [["id", ChartFilter, lambda: []]]
search_filters = {
@@ -227,7 +227,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"created_by": [ChartHasCreatedByFilter, ChartCreatedByMeFilter],
}
if is_feature_enabled("TAGGING_SYSTEM"):
- search_filters['tags'] = [ChartTagFilter]
+ search_filters["tags"] = [ChartTagFilter]
# Will just affect _info endpoint
edit_columns = ["slice_name"]
diff --git a/superset/charts/filters.py b/superset/charts/filters.py
index 5efd1e9969a29..ddd1f54b592ce 100644
--- a/superset/charts/filters.py
+++ b/superset/charts/filters.py
@@ -57,9 +57,7 @@ class ChartFavoriteFilter(BaseFavoriteFilter): # pylint: disable=too-few-public
model = Slice
-class ChartTagFilter( # pylint: disable=too-few-public-methods
- BaseTagFilter
-):
+class ChartTagFilter(BaseTagFilter): # pylint: disable=too-few-public-methods
"""
Custom filter for the GET list that filters all dashboards that a user has favored
"""
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 1195c61e82861..55ee256a76ce9 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -171,8 +171,7 @@ class ChartEntityResponseSchema(Schema):
form_data = fields.Dict(description=form_data_description)
slice_url = fields.String(description=slice_url_description)
certified_by = fields.String(description=certified_by_description)
- certification_details = fields.String(
- description=certification_details_description)
+ certification_details = fields.String(description=certification_details_description)
class ChartPostSchema(Schema):
@@ -183,8 +182,7 @@ class ChartPostSchema(Schema):
slice_name = fields.String(
description=slice_name_description, required=True, validate=Length(1, 250)
)
- description = fields.String(
- description=description_description, allow_none=True)
+ description = fields.String(description=description_description, allow_none=True)
viz_type = fields.String(
description=viz_type_description,
validate=Length(0, 250),
@@ -205,8 +203,7 @@ class ChartPostSchema(Schema):
cache_timeout = fields.Integer(
description=cache_timeout_description, allow_none=True
)
- datasource_id = fields.Integer(
- description=datasource_id_description, required=True)
+ datasource_id = fields.Integer(description=datasource_id_description, required=True)
datasource_type = fields.String(
description=datasource_type_description,
validate=validate.OneOf(choices=[ds.value for ds in DatasourceType]),
@@ -215,10 +212,8 @@ class ChartPostSchema(Schema):
datasource_name = fields.String(
description=datasource_name_description, allow_none=True
)
- dashboards = fields.List(fields.Integer(
- description=dashboards_description))
- certified_by = fields.String(
- description=certified_by_description, allow_none=True)
+ dashboards = fields.List(fields.Integer(description=dashboards_description))
+ certified_by = fields.String(description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -234,8 +229,7 @@ class ChartPutSchema(Schema):
slice_name = fields.String(
description=slice_name_description, allow_none=True, validate=Length(0, 250)
)
- description = fields.String(
- description=description_description, allow_none=True)
+ description = fields.String(description=description_description, allow_none=True)
viz_type = fields.String(
description=viz_type_description,
allow_none=True,
@@ -261,10 +255,8 @@ class ChartPutSchema(Schema):
validate=validate.OneOf(choices=[ds.value for ds in DatasourceType]),
allow_none=True,
)
- dashboards = fields.List(fields.Integer(
- description=dashboards_description))
- certified_by = fields.String(
- description=certified_by_description, allow_none=True)
+ dashboards = fields.List(fields.Integer(description=dashboards_description))
+ certified_by = fields.String(description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -926,16 +918,14 @@ class AnnotationLayerSchema(Schema):
keys=fields.String(
desciption="Name of property to be overridden",
validate=validate.OneOf(
- choices=("granularity", "time_grain_sqla",
- "time_range", "time_shift"),
+ choices=("granularity", "time_grain_sqla", "time_range", "time_shift"),
),
),
values=fields.Raw(allow_none=True),
description="which properties should be overridable",
allow_none=True,
)
- show = fields.Boolean(
- description="Should the layer be shown", required=True)
+ show = fields.Boolean(description="Should the layer be shown", required=True)
showLabel = fields.Boolean(
description="Should the label always be shown",
allow_none=True,
@@ -1008,8 +998,7 @@ class Meta: # pylint: disable=too-few-public-methods
unknown = EXCLUDE
datasource = fields.Nested(ChartDataDatasourceSchema, allow_none=True)
- result_type = EnumField(ChartDataResultType,
- by_value=True, allow_none=True)
+ result_type = EnumField(ChartDataResultType, by_value=True, allow_none=True)
annotation_layers = fields.List(
fields.Nested(AnnotationLayerSchema),
@@ -1026,8 +1015,7 @@ class Meta: # pylint: disable=too-few-public-methods
"if defined in datasource",
allow_none=True,
)
- filters = fields.List(fields.Nested(
- ChartDataFilterSchema), allow_none=True)
+ filters = fields.List(fields.Nested(ChartDataFilterSchema), allow_none=True)
granularity = fields.String(
description="Name of temporal column used for time filtering. For legacy Druid "
"datasources this defines the time grain.",
@@ -1153,8 +1141,7 @@ class Meta: # pylint: disable=too-few-public-methods
(
fields.Raw(
validate=[
- Length(min=1, error=_(
- "orderby column must be populated"))
+ Length(min=1, error=_("orderby column must be populated"))
],
allow_none=False,
),
@@ -1325,8 +1312,7 @@ class ChartDataResponseResult(Schema):
allow_none=False,
)
data = fields.List(fields.Dict(), description="A list with results")
- colnames = fields.List(
- fields.String(), description="A list of column names")
+ colnames = fields.List(fields.String(), description="A list of column names")
coltypes = fields.List(
fields.Integer(), description="A list of generic data types of each column"
)
@@ -1390,8 +1376,7 @@ class ImportV1ChartSchema(Schema):
slice_name = fields.String(required=True)
viz_type = fields.String(required=True)
params = fields.Dict()
- query_context = fields.String(
- allow_none=True, validate=utils.validate_json)
+ query_context = fields.String(allow_none=True, validate=utils.validate_json)
cache_timeout = fields.Integer(allow_none=True)
uuid = fields.UUID(required=True)
version = fields.String(required=True)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index f7286c6c224c0..bd28cd69770b9 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -61,8 +61,8 @@
DashboardCreatedByMeFilter,
DashboardFavoriteFilter,
DashboardHasCreatedByFilter,
- DashboardTitleOrSlugFilter,
DashboardTagFilter,
+ DashboardTitleOrSlugFilter,
FilterRelatedRoles,
)
from superset.dashboards.schemas import (
@@ -210,24 +210,36 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
edit_columns = add_columns
search_columns = (
- "created_by",
- "changed_by",
- "dashboard_title",
- "id",
- "owners",
- "published",
- "roles",
- "slug",
+ (
+ "created_by",
+ "changed_by",
+ "dashboard_title",
+ "id",
+ "owners",
+ "published",
+ "roles",
+ "slug",
+ )
+ if not is_feature_enabled("TAGGING_SYSTEM")
+ else (
+ "created_by",
+ "changed_by",
+ "dashboard_title",
+ "id",
+ "owners",
+ "published",
+ "roles",
+ "slug",
+ "tags",
+ )
)
- if is_feature_enabled("TAGGING_SYSTEM"):
- search_columns += ("tags",)
search_filters = {
"dashboard_title": [DashboardTitleOrSlugFilter],
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
"created_by": [DashboardCreatedByMeFilter, DashboardHasCreatedByFilter],
}
if is_feature_enabled("TAGGING_SYSTEM"):
- search_filters['tags'] = [DashboardTagFilter]
+ search_filters["tags"] = [DashboardTagFilter]
base_order = ("changed_on", "desc")
diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py
index 26c465d78feb6..d458671311386 100644
--- a/superset/dashboards/filters.py
+++ b/superset/dashboards/filters.py
@@ -77,9 +77,7 @@ class DashboardFavoriteFilter( # pylint: disable=too-few-public-methods
model = Dashboard
-class DashboardTagFilter( # pylint: disable=too-few-public-methods
- BaseTagFilter
-):
+class DashboardTagFilter(BaseTagFilter): # pylint: disable=too-few-public-methods
"""
Custom filter for the GET list that filters all dashboards that a user has favored
"""
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index 7bb6c50f068ff..fa2e9d6f87b8e 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -165,8 +165,7 @@ class DashboardGetResponseSchema(Schema):
json_metadata = fields.String(description=json_metadata_description)
position_json = fields.String(description=position_json_description)
certified_by = fields.String(description=certified_by_description)
- certification_details = fields.String(
- description=certification_details_description)
+ certification_details = fields.String(description=certification_details_description)
changed_by_name = fields.String()
changed_by_url = fields.String()
changed_by = fields.Nested(UserSchema)
@@ -256,8 +255,7 @@ class DashboardPostSchema(BaseDashboardSchema):
validate=validate_json_metadata,
)
published = fields.Boolean(description=published_description)
- certified_by = fields.String(
- description=certified_by_description, allow_none=True)
+ certified_by = fields.String(description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
@@ -277,8 +275,7 @@ class DashboardPutSchema(BaseDashboardSchema):
owners = fields.List(
fields.Integer(description=owners_description, allow_none=True)
)
- roles = fields.List(fields.Integer(
- description=roles_description, allow_none=True))
+ roles = fields.List(fields.Integer(description=roles_description, allow_none=True))
position_json = fields.String(
description=position_json_description, allow_none=True, validate=validate_json
)
@@ -288,10 +285,8 @@ class DashboardPutSchema(BaseDashboardSchema):
allow_none=True,
validate=validate_json_metadata,
)
- published = fields.Boolean(
- description=published_description, allow_none=True)
- certified_by = fields.String(
- description=certified_by_description, allow_none=True)
+ published = fields.Boolean(description=published_description, allow_none=True)
+ certified_by = fields.String(description=certified_by_description, allow_none=True)
certification_details = fields.String(
description=certification_details_description, allow_none=True
)
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index d363e5ec1a702..140edaff3c61e 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -150,6 +150,7 @@ def init_views(self) -> None:
from superset.tags.api import TagRestApi
from superset.views.access_requests import AccessRequestsModelView
from superset.views.alerts import AlertView, ReportView
+ from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
from superset.views.annotations import AnnotationLayerView
from superset.views.api import Api
from superset.views.chart.views import SliceAsync, SliceModelView
@@ -183,10 +184,8 @@ def init_views(self) -> None:
TableSchemaView,
TabStateView,
)
- from superset.views.users.api import CurrentUserRestApi
- from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
from superset.views.tags import TagModelView, TagView
-
+ from superset.views.users.api import CurrentUserRestApi
#
# Setup API views
@@ -369,9 +368,7 @@ def init_views(self) -> None:
label=__("All Entities"),
icon="",
category_icon="",
- menu_cond=lambda: feature_flag_manager.is_feature_enabled(
- "TAGGING_SYSTEM"
- ),
+ menu_cond=lambda: feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"),
)
appbuilder.add_view(
TagModelView,
@@ -379,9 +376,7 @@ def init_views(self) -> None:
label=__("Tags"),
icon="",
category_icon="",
- menu_cond=lambda: feature_flag_manager.is_feature_enabled(
- "TAGGING_SYSTEM"
- ),
+ menu_cond=lambda: feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"),
)
appbuilder.add_api(LogRestApi)
appbuilder.add_view(
diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py
index 299f2c51e756e..203be3ec9157f 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -153,8 +153,8 @@ class Dashboard(Model, AuditMixinNullable, ImportExportMixin):
"Tag",
secondary="tagged_object",
primaryjoin="and_(Dashboard.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
- "TaggedObject.object_type == 'dashboard')",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
+ "TaggedObject.object_type == 'dashboard')",
)
published = Column(Boolean, default=False)
is_managed_externally = Column(Boolean, nullable=False, default=False)
diff --git a/superset/models/slice.py b/superset/models/slice.py
index ff89ed9b73a08..853c6bc676ef5 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -102,8 +102,8 @@ class Slice( # pylint: disable=too-many-public-methods
"Tag",
secondary="tagged_object",
primaryjoin="and_(Slice.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
- "TaggedObject.object_type == 'chart')",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
+ "TaggedObject.object_type == 'chart')",
)
table = relationship(
"SqlaTable",
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index 7f05d60afe315..52f6548533484 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -369,8 +369,8 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
"Tag",
secondary="tagged_object",
primaryjoin="and_(SavedQuery.id == TaggedObject.object_id)",
- secondaryjoin="and_(TaggedObject.tag_id == Tag.id, " \
- "TaggedObject.object_type == 'saved_query')",
+ secondaryjoin="and_(TaggedObject.tag_id == Tag.id, "
+ "TaggedObject.object_type == 'saved_query')",
)
export_parent = "database"
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index 343d17fba4e46..a30464bf08cbb 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -50,8 +50,8 @@
from superset.queries.saved_queries.filters import (
SavedQueryAllTextFilter,
SavedQueryFavoriteFilter,
- SavedQueryTagFilter,
SavedQueryFilter,
+ SavedQueryTagFilter,
)
from superset.queries.saved_queries.schemas import (
get_delete_ids_schema,
@@ -146,13 +146,13 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
search_columns = ["id", "database", "label", "schema", "created_by"]
if is_feature_enabled("TAGGING_SYSTEM"):
- search_columns += ['tags']
+ search_columns += ["tags"]
search_filters = {
"id": [SavedQueryFavoriteFilter],
"label": [SavedQueryAllTextFilter],
}
if is_feature_enabled("TAGGING_SYSTEM"):
- search_filters['tags'] = [SavedQueryTagFilter]
+ search_filters["tags"] = [SavedQueryTagFilter]
apispec_parameter_schemas = {
"get_delete_ids_schema": get_delete_ids_schema,
diff --git a/superset/queries/saved_queries/filters.py b/superset/queries/saved_queries/filters.py
index 119b8276bdcab..a9e7006b63f4f 100644
--- a/superset/queries/saved_queries/filters.py
+++ b/superset/queries/saved_queries/filters.py
@@ -58,9 +58,7 @@ class SavedQueryFavoriteFilter(
model = SavedQuery
-class SavedQueryTagFilter( # pylint: disable=too-few-public-methods
- BaseTagFilter
-):
+class SavedQueryTagFilter(BaseTagFilter): # pylint: disable=too-few-public-methods
"""
Custom filter for the GET list that filters all dashboards that a user has favored
"""
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 03599236bd932..d89a24da3a680 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -23,21 +23,15 @@
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.extensions import event_logger
-from superset.tags.models import ObjectTypes, Tag
from superset.tags.commands.create import CreateTagCommand
-from superset.tags.commands.exceptions import (
- TagCreateFailedError,
- TagInvalidError,
-)
+from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
+from superset.tags.models import ObjectTypes, Tag
from superset.tags.schemas import (
+ openapi_spec_methods_override,
TagGetResponseSchema,
TagPostSchema,
- openapi_spec_methods_override,
-)
-from superset.views.base_api import (
- BaseSupersetModelRestApi,
- statsd_metrics,
)
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
logger = logging.getLogger(__name__)
@@ -54,28 +48,28 @@ class TagRestApi(BaseSupersetModelRestApi):
method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
list_columns = [
- "id",
- "name",
- "type",
- "changed_by.first_name",
- "changed_by.last_name",
- "changed_on_delta_humanized",
- "created_by.first_name",
- "created_by.last_name",
+ "id",
+ "name",
+ "type",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_on_delta_humanized",
+ "created_by.first_name",
+ "created_by.last_name",
]
list_select_columns = list_columns
show_columns = [
- "id",
- "name",
- "type",
- "changed_by.first_name",
- "changed_by.last_name",
- "changed_on_delta_humanized",
- "created_by.first_name",
- "created_by.last_name",
- "created_by"
+ "id",
+ "name",
+ "type",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_on_delta_humanized",
+ "created_by.first_name",
+ "created_by.last_name",
+ "created_by",
]
add_model_schema = TagPostSchema()
@@ -83,18 +77,17 @@ class TagRestApi(BaseSupersetModelRestApi):
openapi_spec_tag = "Tags"
""" Override the name set for this collection of endpoints """
- openapi_spec_component_schemas = (
- TagGetResponseSchema,
- )
- apispec_parameter_schemas = {}
+ openapi_spec_component_schemas = (TagGetResponseSchema,)
openapi_spec_methods = openapi_spec_methods_override
""" Overrides GET methods OpenApi descriptions """
def __repr__(self) -> str:
"""Deterministic string representation of the API instance for etag_cache."""
- return 'Superset.tags.api.TagRestApi@v' \
- f'{self.appbuilder.app.config["VERSION_STRING"]}' \
+ return (
+ "Superset.tags.api.TagRestApi@v"
+ f'{self.appbuilder.app.config["VERSION_STRING"]}'
f'{self.appbuilder.app.config["VERSION_SHA"]}'
+ )
@expose("///", methods=["POST"])
@protect()
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index be4ad99c04b4b..66b31876ed68b 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -21,12 +21,9 @@
from superset.commands.base import BaseCommand, CreateMixin
from superset.dao.exceptions import DAOCreateFailedError
-from superset.tags.models import ObjectTypes
-from superset.tags.commands.exceptions import (
- TagCreateFailedError,
- TagInvalidError,
-)
+from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
from superset.tags.dao import TagDAO
+from superset.tags.models import ObjectTypes
logger = logging.getLogger(__name__)
@@ -41,7 +38,8 @@ def run(self) -> Model:
self.validate()
try:
tag = TagDAO.create_tagged_objects(
- self._object_type, self._object_id, self._properties)
+ self._object_type, self._object_id, self._properties
+ )
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
raise TagCreateFailedError() from ex
diff --git a/superset/tags/commands/exceptions.py b/superset/tags/commands/exceptions.py
index ddc241df3f592..5e218fed73f1d 100644
--- a/superset/tags/commands/exceptions.py
+++ b/superset/tags/commands/exceptions.py
@@ -16,14 +16,12 @@
# under the License.
from flask_babel import lazy_gettext as _
-from superset.commands.exceptions import (
- CommandInvalidError,
- CreateFailedError,
-)
+from superset.commands.exceptions import CommandInvalidError, CreateFailedError
class TagInvalidError(CommandInvalidError):
message = _("Tag parameters are invalid.")
+
class TagCreateFailedError(CreateFailedError):
message = _("Tag could not be created.")
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index aff05551c7bf6..959cc6c07a634 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -30,10 +30,8 @@ class TagDAO(BaseDAO):
@staticmethod
def create_tagged_objects(
- object_type: ObjectTypes,
- object_id: int,
- properties: Dict[str, Any]
- ) -> None:
+ object_type: ObjectTypes, object_id: int, properties: Dict[str, Any]
+ ) -> None:
tag_names = properties["tags"]
tagged_objects = []
@@ -46,19 +44,15 @@ def create_tagged_objects(
tag = TagDAO.get_by_name(name, type_)
tagged_objects.append(
- TaggedObject(object_id=object_id,
- object_type=object_type, tag=tag)
+ TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
)
db.session.add_all(tagged_objects)
db.session.commit()
- return tag
-
@staticmethod
- def get_by_name(name: str, type_: str) -> Tag:
- tag = db.session.query(Tag).filter(
- Tag.name == name, Tag.type == type_).first()
+ def get_by_name(name: str, type_: TagTypes) -> Tag:
+ tag = db.session.query(Tag).filter(Tag.name == name, Tag.type == type_).first()
if not tag:
tag = Tag(name=name, type=type_)
# security_manager.raise_for_tag_access(tag)
diff --git a/superset/tags/models.py b/superset/tags/models.py
index 1258f3b96110c..f454507a29fa9 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -39,7 +39,6 @@
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query
- from superset.connectors.sqla.models import SqlaTable
Session = sessionmaker(autoflush=False)
@@ -84,6 +83,7 @@ class Tag(Model, AuditMixinNullable):
name = Column(String(250), unique=True)
type = Column(Enum(TagTypes))
+
class TaggedObject(Model, AuditMixinNullable):
"""An association between an object and a tag."""
@@ -91,8 +91,12 @@ class TaggedObject(Model, AuditMixinNullable):
__tablename__ = "tagged_object"
id = Column(Integer, primary_key=True)
tag_id = Column(Integer, ForeignKey("tag.id"))
- object_id = Column(Integer, ForeignKey("dashboards.id"),
- ForeignKey("slices.id"), ForeignKey("saved_query.id"))
+ object_id = Column(
+ Integer,
+ ForeignKey("dashboards.id"),
+ ForeignKey("slices.id"),
+ ForeignKey("saved_query.id"),
+ )
object_type = Column(Enum(ObjectTypes))
tag = relationship("Tag", backref="objects")
@@ -157,8 +161,7 @@ def after_insert(
cls._add_owners(session, target)
# add `type:` tags
- tag = get_tag("type:{0}".format(cls.object_type),
- session, TagTypes.type)
+ tag = get_tag("type:{0}".format(cls.object_type), session, TagTypes.type)
tagged_object = TaggedObject(
tag_id=tag.id, object_id=target.id, object_type=cls.object_type
)
@@ -271,7 +274,7 @@ def after_delete(
cls, _mapper: Mapper, connection: Connection, target: FavStar
) -> None:
session = Session(bind=connection)
- name = f'favorited_by:{target.user_id}'
+ name = f"favorited_by:{target.user_id}"
query = (
session.query(TaggedObject.id)
.join(Tag)
diff --git a/superset/tags/schemas.py b/superset/tags/schemas.py
index de44d85505fee..e823a9c2aa749 100644
--- a/superset/tags/schemas.py
+++ b/superset/tags/schemas.py
@@ -29,8 +29,7 @@
},
"info": {
"get": {
- "description": "Several metadata information about tag API "
- "endpoints.",
+ "description": "Several metadata information about tag API " "endpoints.",
}
},
}
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index 17f292a9bd449..5142039475744 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -33,8 +33,8 @@
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
-from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
from superset.superset_typing import FlaskResponse
+from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
@@ -49,6 +49,7 @@ def process_template(content: str) -> str:
}
return template.render(context)
+
class TaggedObjectsModelView(SupersetModelView):
route_base = "/superset/all_entities"
datamodel = SQLAInterface(Tag)
@@ -62,6 +63,7 @@ def list(self) -> FlaskResponse:
return super().render_app_template()
+
class TaggedObjectView(BaseSupersetView):
@staticmethod
def is_enabled() -> bool:
diff --git a/superset/views/base_api.py b/superset/views/base_api.py
index ad4299762e0c4..a161cc8622651 100644
--- a/superset/views/base_api.py
+++ b/superset/views/base_api.py
@@ -28,18 +28,18 @@
from marshmallow import fields, Schema
from sqlalchemy import and_, distinct, func
from sqlalchemy.orm.query import Query
-from superset.connectors.sqla.models import SqlaTable
+from superset.connectors.sqla.models import SqlaTable
from superset.exceptions import InvalidPayloadFormatError
from superset.extensions import db, event_logger, security_manager
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
-from superset.tags.models import Tag
from superset.schemas import error_payload_content
from superset.sql_lab import Query as SqllabQuery
from superset.stats_logger import BaseStatsLogger
from superset.superset_typing import FlaskResponse
+from superset.tags.models import Tag
from superset.utils.core import get_user_id, time_function
from superset.views.base import handle_api_exception
@@ -175,9 +175,7 @@ def apply(self, query: Query, value: Any) -> Query:
tags_query = (
db.session.query(self.model.id)
.join(self.model.tags)
- .filter(
- Tag.name.ilike(ilike_value)
- )
+ .filter(Tag.name.ilike(ilike_value))
)
return query.filter(self.model.id.in_(tags_query))
diff --git a/superset/views/tags.py b/superset/views/tags.py
index fd4535f275b7d..bd2db86e77a18 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -35,8 +35,8 @@
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.superset_typing import FlaskResponse
-from superset.views.base import SupersetModelView
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
@@ -93,7 +93,8 @@ def tags(self) -> FlaskResponse: # pylint: disable=no-self-use
"changed_on": obj.changed_on,
"changed_by": obj.changed_by_fk,
"created_by": obj.created_by_fk,
- } for obj in query
+ }
+ for obj in query
]
return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
@@ -146,14 +147,12 @@ def post( # pylint: disable=no-self-use
else:
type_ = TagTypes.custom
- tag = db.session.query(Tag).filter_by(
- name=name, type=type_).first()
+ tag = db.session.query(Tag).filter_by(name=name, type=type_).first()
if not tag:
tag = Tag(name=name, type=type_)
tagged_objects.append(
- TaggedObject(object_id=object_id,
- object_type=object_type, tag=tag)
+ TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
)
db.session.add_all(tagged_objects)
@@ -184,10 +183,8 @@ def delete_tagged_objects( # pylint: disable=no-self-use
@has_access_api
@expose("/tags", methods=["DELETE"])
- def delete_tags( # pylint: disable=no-self-use
- self
- ) -> FlaskResponse:
- """Remove tags, and all tagged objects with that tag """
+ def delete_tags(self) -> FlaskResponse: # pylint: disable=no-self-use
+ """Remove tags, and all tagged objects with that tag"""
tag_names = request.get_json(force=True)
if not tag_names:
return Response(status=403)
@@ -214,8 +211,7 @@ def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
return json_success(json.dumps([]))
# filter types
- types = [type_ for type_ in request.args.get(
- "types", "").split(",") if type_]
+ types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
results: List[Dict[str, Any]] = []
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index cc02abe62941b..a97146405efc6 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -37,14 +37,14 @@
TAGS_FIXTURE_COUNT = 10
TAGS_LIST_COLUMNS = [
- 'id',
- 'name',
- 'type',
- 'changed_by.first_name',
- 'changed_by.last_name',
- 'changed_on_delta_humanized',
- 'created_by.first_name',
- 'created_by.last_name'
+ "id",
+ "name",
+ "type",
+ "changed_by.first_name",
+ "changed_by.last_name",
+ "changed_on_delta_humanized",
+ "created_by.first_name",
+ "created_by.last_name",
]
@@ -73,7 +73,7 @@ def create_tags(self):
tags.append(
self.insert_tag(
name=f"example_tag_{cx}",
- tag_type='custom',
+ tag_type="custom",
)
)
@@ -90,7 +90,7 @@ def test_get_tag(self):
"""
tag = self.insert_tag(
name="test get tag",
- tag_type='custom',
+ tag_type="custom",
)
self.login(username="admin")
uri = f"api/v1/tag/{tag.id}"
@@ -102,7 +102,7 @@ def test_get_tag(self):
"created_by": None,
"id": tag.id,
"name": "test get tag",
- "type": TagTypes.custom.value
+ "type": TagTypes.custom.value,
}
data = json.loads(rv.data.decode("utf-8"))
for key, value in expected_result.items():
@@ -115,7 +115,7 @@ def test_get_tag_not_found(self):
"""
Query API: Test get query not found
"""
- tag = self.insert_tag(name="test tag", tag_type='custom')
+ tag = self.insert_tag(name="test tag", tag_type="custom")
max_id = db.session.query(func.max(Tag.id)).scalar()
self.login(username="admin")
uri = f"api/v1/tag/{max_id + 1}"
@@ -135,6 +135,6 @@ def test_get_list_tag(self):
rv = self.client.get(uri)
self.assertEqual(rv.status_code, 200)
data = json.loads(rv.data.decode("utf-8"))
- assert data['count'] == TAGS_FIXTURE_COUNT
+ assert data["count"] == TAGS_FIXTURE_COUNT
# check expected columns
assert data["list_columns"] == TAGS_LIST_COLUMNS
From 0b30e486bf8f9c6afa82de6d3c7d242b70cc845c Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 13 Dec 2022 09:58:22 -0500
Subject: [PATCH 56/76] feedback changes
---
.../src/components/Tags/Tag.test.tsx | 4 +-
superset-frontend/src/components/Tags/Tag.tsx | 14 ++--
.../src/components/Tags/TagsList.tsx | 2 +-
.../components/PropertiesModal/index.tsx | 76 +++++++++---------
.../components/PropertiesModal/index.tsx | 78 +++++++++----------
superset-frontend/src/types/TagType.ts | 6 +-
.../views/CRUD/allentities/AllEntities.tsx | 40 +++++-----
.../CRUD/allentities/AllEntitiesTable.tsx | 11 ++-
.../src/views/CRUD/tags/TagList.tsx | 1 -
9 files changed, 121 insertions(+), 111 deletions(-)
diff --git a/superset-frontend/src/components/Tags/Tag.test.tsx b/superset-frontend/src/components/Tags/Tag.test.tsx
index b797665b13c4f..0ff7b2e85a3f3 100644
--- a/superset-frontend/src/components/Tags/Tag.test.tsx
+++ b/superset-frontend/src/components/Tags/Tag.test.tsx
@@ -24,9 +24,9 @@ import Tag from './Tag';
const mockedProps: TagType = {
name: 'example-tag',
id: 1,
- onDelete: null,
+ onDelete: undefined,
editable: false,
- onClick: null,
+ onClick: undefined,
};
test('should render', () => {
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
index 4070f9d1c2676..6f793044d05fe 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -24,22 +24,24 @@ import React, { useMemo } from 'react';
import { Tooltip } from 'src/components/Tooltip';
const StyledTag = styled(AntdTag)`
- margin-top: ${({ theme }) => theme.gridUnit}px;
- margin-bottom: ${({ theme }) => theme.gridUnit}px;
- font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ ${({ 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 = null,
+ onDelete = undefined,
editable = false,
- onClick = null,
+ onClick = undefined,
}: TagType) => {
const isLongTag = useMemo(() => name.length > 20, [name]);
- const handleClose = () => (index ? onDelete(index) : null);
+ const handleClose = () => (index ? onDelete?.(index) : null);
const tagElem = (
<>
diff --git a/superset-frontend/src/components/Tags/TagsList.tsx b/superset-frontend/src/components/Tags/TagsList.tsx
index c5c669ee00d61..102e6d2ced31f 100644
--- a/superset-frontend/src/components/Tags/TagsList.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.tsx
@@ -70,7 +70,7 @@ const TagsList = ({
return (
- {tagsIsLong === true && typeof tempMaxTags === 'number' ? (
+ {tagsIsLong && typeof tempMaxTags === 'number' ? (
<>
{tags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
{
+ // update the tags for this object
+ // add tags that are in new tags, but not in old tags
+ // eslint-disable-next-line array-callback-return
+ newTags.map((tag: TagType) => {
+ if (!oldTags.some(t => t.name === tag.name)) {
+ addTag(
+ {
+ objectType: OBJECT_TYPES.DASHBOARD,
+ objectId: dashboardId,
+ includeTypes: false,
+ },
+ tag.name,
+ () => {},
+ () => {},
+ );
+ }
+ });
+ // delete tags that are in old tags, but not in new tags
+ // eslint-disable-next-line array-callback-return
+ oldTags.map((tag: TagType) => {
+ if (!newTags.some(t => t.name === tag.name)) {
+ deleteTaggedObjects(
+ {
+ objectType: OBJECT_TYPES.DASHBOARD,
+ objectId: dashboardId,
+ },
+ tag,
+ () => {},
+ () => {},
+ );
+ }
+ });
+ };
+
const onFinish = () => {
const { title, slug, certifiedBy, certificationDetails } =
form.getFieldsValue();
@@ -376,8 +411,8 @@ const PropertiesModal = ({
handleErrorResponse(error);
},
);
- } catch (error: any) {
- console.log(error);
+ } catch (error) {
+ handleErrorResponse(error);
}
}
@@ -577,46 +612,11 @@ const PropertiesModal = ({
addDangerToast(`Error fetching tags: ${error.text}`);
},
);
- } catch (error: any) {
+ } catch (error) {
handleErrorResponse(error);
}
}, [dashboardId]);
- const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
- // update the tags for this object
- // add tags that are in new tags, but not in old tags
- // eslint-disable-next-line array-callback-return
- newTags.map((tag: TagType) => {
- if (!oldTags.some(t => t.name === tag.name)) {
- addTag(
- {
- objectType: OBJECT_TYPES.DASHBOARD,
- objectId: dashboardId,
- includeTypes: false,
- },
- tag.name,
- () => {},
- () => {},
- );
- }
- });
- // delete tags that are in old tags, but not in new tags
- // eslint-disable-next-line array-callback-return
- oldTags.map((tag: TagType) => {
- if (!newTags.some(t => t.name === tag.name)) {
- deleteTaggedObjects(
- {
- objectType: OBJECT_TYPES.DASHBOARD,
- objectId: dashboardId,
- },
- tag,
- () => {},
- () => {},
- );
- }
- });
- };
-
const handleChangeTags = (values: { label: string; value: number }[]) => {
// triggered whenever a new tag is selected or a tag was deselected
// on new tag selected, add the tag
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index 9d3f802f1b6c0..d415afd66cbd4 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -138,6 +138,41 @@ function PropertiesModal({
[],
);
+ const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
+ // update the tags for this object
+ // add tags that are in new tags, but not in old tags
+ // eslint-disable-next-line array-callback-return
+ newTags.map((tag: TagType) => {
+ if (!oldTags.some(t => t.name === tag.name)) {
+ addTag(
+ {
+ objectType: OBJECT_TYPES.CHART,
+ objectId: slice.slice_id,
+ includeTypes: false,
+ },
+ tag.name,
+ () => {},
+ () => {},
+ );
+ }
+ });
+ // delete tags that are in old tags, but not in new tags
+ // eslint-disable-next-line array-callback-return
+ oldTags.map((tag: TagType) => {
+ if (!newTags.some(t => t.name === tag.name)) {
+ deleteTaggedObjects(
+ {
+ objectType: OBJECT_TYPES.CHART,
+ objectId: slice.slice_id,
+ },
+ tag,
+ () => {},
+ () => {},
+ );
+ }
+ });
+ };
+
const onSubmit = async (values: {
certified_by?: string;
certification_details?: string;
@@ -181,8 +216,8 @@ function PropertiesModal({
showError(error);
},
);
- } catch (error: any) {
- console.log(error);
+ } catch (error) {
+ showError(error);
}
}
@@ -236,46 +271,11 @@ function PropertiesModal({
showError(error);
},
);
- } catch (error: any) {
- console.log(error);
+ } catch (error) {
+ showError(error);
}
}, [slice.slice_id]);
- const updateTags = (oldTags: TagType[], newTags: TagType[]) => {
- // update the tags for this object
- // add tags that are in new tags, but not in old tags
- // eslint-disable-next-line array-callback-return
- newTags.map((tag: TagType) => {
- if (!oldTags.some(t => t.name === tag.name)) {
- addTag(
- {
- objectType: OBJECT_TYPES.CHART,
- objectId: slice.slice_id,
- includeTypes: false,
- },
- tag.name,
- () => {},
- () => {},
- );
- }
- });
- // delete tags that are in old tags, but not in new tags
- // eslint-disable-next-line array-callback-return
- oldTags.map((tag: TagType) => {
- if (!newTags.some(t => t.name === tag.name)) {
- deleteTaggedObjects(
- {
- objectType: OBJECT_TYPES.CHART,
- objectId: slice.slice_id,
- },
- tag,
- () => {},
- () => {},
- );
- }
- });
- };
-
const handleChangeTags = (values: { label: string; value: number }[]) => {
// triggered whenever a new tag is selected or a tag was deselected
// on new tag selected, add the tag
diff --git a/superset-frontend/src/types/TagType.ts b/superset-frontend/src/types/TagType.ts
index 2be7d96b7c6fa..4c1b85b724915 100644
--- a/superset-frontend/src/types/TagType.ts
+++ b/superset-frontend/src/types/TagType.ts
@@ -17,12 +17,14 @@
* under the License.
*/
+import { MouseEventHandler } from 'react';
+
export interface TagType {
id?: string | number;
type?: string | number;
editable?: boolean;
- onDelete?: any;
- onClick?: any;
+ onDelete?: (index: number) => void;
+ onClick?: MouseEventHandler;
name: string;
index?: number | undefined;
}
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
index abef9db46155d..2950a544eb93f 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
@@ -17,37 +17,40 @@
* under the License.
*/
import React, { useEffect, useState } from 'react';
-import { styled } from '@superset-ui/core';
+import { logging, styled, t } from '@superset-ui/core';
import Tag from 'src/types/TagType';
import { StringParam, useQueryParam } from 'use-query-params';
import withToasts from 'src/components/MessageToasts/withToasts';
import SelectControl from 'src/explore/components/controls/SelectControl';
import { fetchSuggestions } from 'src/tags';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
import AllEntitiesTable from './AllEntitiesTable';
const AllEntitiesContainer = styled.div`
- background-color: ${({ theme }) => theme.colors.grayscale.light4};
+ ${({ theme }) => `
+ background-color: ${theme.colors.grayscale.light4};
.select-control {
- margin-left: ${({ theme }) => theme.gridUnit * 4}px;
- margin-right: ${({ theme }) => theme.gridUnit * 4}px;
- margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
+ margin-left: ${theme.gridUnit * 4}px;
+ margin-right: ${theme.gridUnit * 4}px;
+ margin-bottom: ${theme.gridUnit * 2}px;
}
.select-control-label {
text-transform: uppercase;
- font-size: ${({ theme }) => theme.gridUnit * 3}px;
- color: ${({ theme }) => theme.colors.grayscale.base};
- margin-bottom: ${({ theme }) => theme.gridUnit * 1}px;
- }
+ font-size: ${theme.gridUnit * 3}px;
+ color: ${theme.colors.grayscale.base};
+ margin-bottom: ${theme.gridUnit * 1}px;
+ }`}
`;
const AllEntitiesNav = styled.div`
- height: 50px;
- background-color: ${({ theme }) => theme.colors.grayscale.light5};
- margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
+ ${({ theme }) => `
+ height: ${theme.gridUnit * 12.5}px;
+ background-color: ${theme.colors.grayscale.light5};
+ margin-bottom: ${theme.gridUnit * 4}px;
.navbar-brand {
- margin-left: ${({ theme }) => theme.gridUnit * 2}px;
- font-weight: ${({ theme }) => theme.typography.weights.bold};
- }
+ margin-left: ${theme.gridUnit * 2}px;
+ font-weight: ${theme.typography.weights.bold};
+ }`};
`;
function AllEntities() {
@@ -62,7 +65,8 @@ function AllEntities() {
setTagSuggestions(tagSuggestions);
},
(error: Response) => {
- console.log(error.json());
+ logging.log(error.json());
+ addDangerToast(`Error Fetching Suggestions`);
},
);
}, [tagsQuery]);
@@ -75,10 +79,10 @@ function AllEntities() {
return (
- All Entities
+ {t('All Entities')}
-
search by tags
+
{t('search by tags')}
({
dashboard: [],
chart: [],
@@ -75,15 +78,15 @@ export default function AllEntitiesTable({
setObjects(objects);
},
(error: Response) => {
- console.log(error.json());
+ addDangerToast('Error Fetching Tagged Objects');
+ logging.log(error.json());
},
);
}, [search]);
- const renderTable = (type: any) => {
+ const renderTable = (type: objectType) => {
const data = objects[type].map((o: TaggedObject) => ({
[type]: {o.name},
- // eslint-disable-next-line react/no-danger
modified: moment.utc(o.changed_on).fromNow(),
}));
return (
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 8001f3822431c..4561c965f4d90 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -159,7 +159,6 @@ function TagList(props: TagListProps) {
className="action-button"
onClick={confirmDelete}
>
- {/* fix icon name */}
From 66827a5b03c5a5e6960690274f1defaa6a02b7d4 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 13 Dec 2022 10:41:44 -0500
Subject: [PATCH 57/76] precommit fixes
---
superset-frontend/src/components/Tags/Tag.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/components/Tags/Tag.tsx b/superset-frontend/src/components/Tags/Tag.tsx
index 6f793044d05fe..ecd2cb135a0b6 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -25,8 +25,8 @@ import { Tooltip } from 'src/components/Tooltip';
const StyledTag = styled(AntdTag)`
${({ theme }) => `
- margin-top: ${theme.gridUnit}px;
- margin-bottom: ${theme.gridUnit}px;
+ margin-top: ${theme.gridUnit}px;
+ margin-bottom: ${theme.gridUnit}px;
font-size: ${theme.typography.sizes.s}px;
`};
`;
From 74d78a340c26cb5a51af3a9da11c8129099cd897 Mon Sep 17 00:00:00 2001
From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com>
Date: Tue, 20 Dec 2022 10:45:46 -0500
Subject: [PATCH 58/76] added tag name as key and value to avoid duplicating
tags in the select dropdown
---
superset-frontend/src/components/Tags/utils.tsx | 4 +++-
.../src/dashboard/components/PropertiesModal/index.tsx | 1 +
.../src/explore/components/PropertiesModal/index.tsx | 1 +
3 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/superset-frontend/src/components/Tags/utils.tsx b/superset-frontend/src/components/Tags/utils.tsx
index af2853c5c0b7b..690a9b44066d0 100644
--- a/superset-frontend/src/components/Tags/utils.tsx
+++ b/superset-frontend/src/components/Tags/utils.tsx
@@ -38,13 +38,15 @@ const cachedSupersetGet = cacheWrapper(
type SelectTagsValue = {
value: string | number | undefined;
label: string;
+ key: string | number | undefined;
};
export const tagToSelectOption = (
item: Tag & { table_name: string },
): SelectTagsValue => ({
- value: item.id,
+ value: item.name,
label: item.name,
+ key: item.name,
});
export const loadTags = async (
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 521ddb5199b94..98cd69a03d19e 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -111,6 +111,7 @@ const PropertiesModal = ({
const selectTags = tags.map(tag => ({
value: tag.name,
label: tag.name,
+ key: tag.name,
}));
return selectTags;
}, [tags.length]);
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index d415afd66cbd4..cc15be0b6c224 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -78,6 +78,7 @@ function PropertiesModal({
const selectTags = tags.map(tag => ({
value: tag.name,
label: tag.name,
+ key: tag.name,
}));
return selectTags;
}, [tags.length]);
From 1897e9f194dc5c7c5ddfddf1cc4f679acfa44813 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Mon, 16 Jan 2023 09:21:50 -0500
Subject: [PATCH 59/76] refactored create tag to create custom tag, changed add
tag functions to use tagDao
---
.../components/PropertiesModal/index.tsx | 12 ++--
superset/tags/api.py | 4 +-
superset/tags/commands/create.py | 5 +-
superset/tags/dao.py | 20 ++++--
superset/tags/exceptions.py | 39 +++++++++++
superset/views/all_entities.py | 56 +++++++++-------
superset/views/tags.py | 56 +++++++++-------
tests/integration_tests/tags/dao_tests.py | 66 +++++++++++++++++++
8 files changed, 196 insertions(+), 62 deletions(-)
create mode 100644 superset/tags/exceptions.py
create mode 100644 tests/integration_tests/tags/dao_tests.py
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 98cd69a03d19e..b12102d324247 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -724,11 +724,13 @@ const PropertiesModal = ({
-
-
- {t('Tags')}
-
-
+ {isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) ? (
+
+
+ {t('Tags')}
+
+
+ ) : null}
{isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) ? (
diff --git a/superset/tags/api.py b/superset/tags/api.py
index d89a24da3a680..6b2a239c15512 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -23,7 +23,7 @@
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.extensions import event_logger
-from superset.tags.commands.create import CreateTagCommand
+from superset.tags.commands.create import CreateCustomTagCommand
from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
from superset.tags.models import ObjectTypes, Tag
from superset.tags.schemas import (
@@ -148,7 +148,7 @@ def add_new_tags(self, object_type: ObjectTypes, object_id: int) -> Response:
except ValidationError as error:
return self.response_400(message=error.messages)
try:
- new_model = CreateTagCommand(object_type, object_id, item).run()
+ new_model = CreateCustomTagCommand(object_type, object_id, item).run()
return self.response(201, id=new_model.id, result=item)
except TagInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index 66b31876ed68b..06d6026eac401 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -28,7 +28,7 @@
logger = logging.getLogger(__name__)
-class CreateTagCommand(CreateMixin, BaseCommand):
+class CreateCustomTagCommand(CreateMixin, BaseCommand):
def __init__(self, object_type: ObjectTypes, object_id: int, data: Dict[str, Any]):
self._object_type = object_type
self._object_id = object_id
@@ -37,7 +37,7 @@ def __init__(self, object_type: ObjectTypes, object_id: int, data: Dict[str, Any
def run(self) -> Model:
self.validate()
try:
- tag = TagDAO.create_tagged_objects(
+ tag = TagDAO.create_custom_tagged_objects(
self._object_type, self._object_id, self._properties
)
except DAOCreateFailedError as ex:
@@ -51,7 +51,6 @@ def validate(self) -> None:
# Validate object_id
if self._object_id == 0:
exceptions.append(TagCreateFailedError())
-
if exceptions:
exception = TagInvalidError()
exception.add_list(exceptions)
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index 39ddc7dcfec93..f36fee720ab8d 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -18,7 +18,9 @@
from typing import Any, Dict
from superset.dao.base import BaseDAO
+from superset.dao.exceptions import DAOCreateFailedError
from superset.extensions import db
+from superset.tags.exceptions import InvalidTagNameError
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
logger = logging.getLogger(__name__)
@@ -27,20 +29,26 @@
class TagDAO(BaseDAO):
model_cls = Tag
# base_filter = TagAccessFilter
+ @staticmethod
+ def validate_tag_name(tag_name: str) -> bool:
+ if ":" in tag_name:
+ return False
+ return True
@staticmethod
- def create_tagged_objects(
+ def create_custom_tagged_objects(
object_type: ObjectTypes, object_id: int, properties: Dict[str, Any]
) -> None:
tag_names = properties["tags"]
tagged_objects = []
for name in tag_names:
- if ":" in name:
- type_name = name.split(":", 1)[0]
- type_ = TagTypes[type_name]
- else:
- type_ = TagTypes.custom
+ if not TagDAO.validate_tag_name(name):
+ logger.error(f"failed create tag: {name}")
+ raise DAOCreateFailedError(
+ message="Invalid Tag Name (cannot contain ':')"
+ )
+ type_ = TagTypes.custom
tag_name = name.strip()
tag = TagDAO.get_by_name(tag_name, type_)
tagged_objects.append(
diff --git a/superset/tags/exceptions.py b/superset/tags/exceptions.py
new file mode 100644
index 0000000000000..e3aaa5d235a4e
--- /dev/null
+++ b/superset/tags/exceptions.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 marshmallow.validate import ValidationError
+
+from superset.commands.exceptions import (
+ CommandException,
+ CommandInvalidError,
+ CreateFailedError,
+ DeleteFailedError,
+ ForbiddenError,
+ ImportFailedError,
+ UpdateFailedError,
+)
+
+
+class InvalidTagNameError(ValidationError):
+ """
+ Marshmallow validation error for invalid Tag name
+ """
+
+ def __init__(self) -> None:
+ super().__init__(
+ [_("Tag name is invalid (cannot contain ':')")], field_name="name"
+ )
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index f278092113f1d..b9831a82ffe60 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -16,6 +16,7 @@
# under the License.
from __future__ import absolute_import, division, print_function, unicode_literals
+import logging
from typing import Any, Dict, List
import simplejson as json
@@ -25,6 +26,7 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from jinja2.sandbox import SandboxedEnvironment
+from marshmallow import ValidationError
from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
@@ -34,11 +36,16 @@
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.superset_typing import FlaskResponse
+from superset.tags.commands.create import CreateCustomTagCommand
+from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.schemas import TagPostSchema
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
+logger = logging.getLogger(__name__)
+
def process_template(content: str) -> str:
env = SandboxedEnvironment()
@@ -111,30 +118,33 @@ def get( # pylint: disable=no-self-use
def post( # pylint: disable=no-self-use
self, object_type: ObjectTypes, object_id: int
) -> FlaskResponse:
- """Add new tags to an object."""
- if object_id == 0:
- return Response(status=404)
-
- tagged_objects = []
- for name in request.get_json(force=True):
- if ":" in name:
- type_name = name.split(":", 1)[0]
- type_ = TagTypes[type_name]
- else:
- type_ = TagTypes.custom
- tag_name = name.strip()
- tag = db.session.query(Tag).filter_by(name=tag_name, type=type_).first()
- if not tag:
- tag = Tag(name=tag_name, type=type_)
-
- tagged_objects.append(
- TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+ """
+ ---
+ post:
+ description: >-
+ Add new tags to an object..
+ requestBody:
+ array of tag names
+ """
+ data = dict()
+ data["tags"] = request.get_json(force=True)
+ try:
+ CreateCustomTagCommand(object_type, object_id, data).run()
+ return Response(status=201)
+ except TagInvalidError as ex:
+ logger.error(
+ "Invalid tag: %s",
+ str(ex),
)
-
- db.session.add_all(tagged_objects)
- db.session.commit()
-
- return Response(status=201) # 201 CREATED
+ return Response(response="tag invalid", status=422)
+ except TagCreateFailedError as ex:
+ logger.error(
+ "Error creating model %s: %s",
+ self.__class__.__name__,
+ str(ex),
+ exc_info=True,
+ )
+ return Response(response="Error creating tag", status=422)
@has_access_api
@expose("/tags///", methods=["DELETE"])
diff --git a/superset/views/tags.py b/superset/views/tags.py
index fdf7fecf86a15..7ea86391a87be 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -16,6 +16,7 @@
# under the License.
from __future__ import absolute_import, division, print_function, unicode_literals
+import logging
from typing import Any, Dict, List
import simplejson as json
@@ -25,6 +26,7 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from jinja2.sandbox import SandboxedEnvironment
+from marshmallow import ValidationError
from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
@@ -35,11 +37,16 @@
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.superset_typing import FlaskResponse
+from superset.tags.commands.create import CreateCustomTagCommand
+from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.tags.schemas import TagPostSchema
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
+logger = logging.getLogger(__name__)
+
def process_template(content: str) -> str:
env = SandboxedEnvironment()
@@ -135,30 +142,33 @@ def get( # pylint: disable=no-self-use
def post( # pylint: disable=no-self-use
self, object_type: ObjectTypes, object_id: int
) -> FlaskResponse:
- """Add new tags to an object."""
- if object_id == 0:
- return Response(status=404)
-
- tagged_objects = []
- for name in request.get_json(force=True):
- if ":" in name:
- type_name = name.split(":", 1)[0]
- type_ = TagTypes[type_name]
- else:
- type_ = TagTypes.custom
- tag_name = name.strip()
- tag = db.session.query(Tag).filter_by(name=tag_name, type=type_).first()
- if not tag:
- tag = Tag(name=tag_name, type=type_)
-
- tagged_objects.append(
- TaggedObject(object_id=object_id, object_type=object_type, tag=tag)
+ """
+ ---
+ post:
+ description: >-
+ Add new tags to an object..
+ requestBody:
+ array of tag names
+ """
+ data = dict()
+ data["tags"] = request.get_json(force=True)
+ try:
+ CreateCustomTagCommand(object_type, object_id, data).run()
+ return Response(status=201)
+ except TagInvalidError as ex:
+ logger.error(
+ "Invalid tag: %s",
+ str(ex),
)
-
- db.session.add_all(tagged_objects)
- db.session.commit()
-
- return Response(status=201) # 201 CREATED
+ return Response(response="tag invalid", status=422)
+ except TagCreateFailedError as ex:
+ logger.error(
+ "Error creating model %s: %s",
+ self.__class__.__name__,
+ str(ex),
+ exc_info=True,
+ )
+ return Response(response="Error creating model", status=422)
@has_access_api
@expose("/tags///", methods=["DELETE"])
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
new file mode 100644
index 0000000000000..2a3a13ae0d73a
--- /dev/null
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -0,0 +1,66 @@
+# 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.
+# isort:skip_file
+import copy
+import json
+import time
+from unittest.mock import patch
+import pytest
+from superset.dao.exceptions import DAOCreateFailedError
+from superset.tags.dao import TagDAO
+from superset.tags.exceptions import InvalidTagNameError
+from superset.tags.models import ObjectTypes, Tag
+
+import tests.integration_tests.test_app # pylint: disable=unused-import
+from superset import db, security_manager
+from superset.dashboards.dao import DashboardDAO
+from superset.models.dashboard import Dashboard
+from tests.integration_tests.base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.world_bank_dashboard import (
+ load_world_bank_dashboard_with_slices,
+ load_world_bank_data,
+)
+from tests.integration_tests.fixtures.tags import with_tagging_system_feature
+
+
+class TestTagsDAO(SupersetTestCase):
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ def test_create_tag(self):
+ # test that a tag cannot be added if it has ':' in it
+ try:
+ TagDAO.create_custom_tagged_objects(
+ object_type=ObjectTypes.dashboard,
+ object_id=1,
+ properties={"tags": ["invalid:example tag 1"]},
+ )
+ except Exception as e:
+ assert type(e) is DAOCreateFailedError
+
+ # test that a tag can be added if it has a valid name
+ try:
+ TagDAO.create_custom_tagged_objects(
+ object_type=ObjectTypes.dashboard,
+ object_id=1,
+ properties={"tags": ["example tag 1"]},
+ )
+ except Exception as e:
+ # should not be an exception
+ assert e is None
+
+ # check if tag exists
+ assert db.session.query(Tag).filter(Tag.name == "example tag 1").first()
From 230b3d47d799a9be3afe7fb4877d189413b9e4a8 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Mon, 16 Jan 2023 09:52:51 -0500
Subject: [PATCH 60/76] lint fixes
---
superset/tags/dao.py | 2 --
superset/tags/exceptions.py | 11 -----------
superset/views/all_entities.py | 6 ++----
superset/views/tags.py | 6 ++----
4 files changed, 4 insertions(+), 21 deletions(-)
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index f36fee720ab8d..a2f4eb6d7f018 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -20,7 +20,6 @@
from superset.dao.base import BaseDAO
from superset.dao.exceptions import DAOCreateFailedError
from superset.extensions import db
-from superset.tags.exceptions import InvalidTagNameError
from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
logger = logging.getLogger(__name__)
@@ -44,7 +43,6 @@ def create_custom_tagged_objects(
tagged_objects = []
for name in tag_names:
if not TagDAO.validate_tag_name(name):
- logger.error(f"failed create tag: {name}")
raise DAOCreateFailedError(
message="Invalid Tag Name (cannot contain ':')"
)
diff --git a/superset/tags/exceptions.py b/superset/tags/exceptions.py
index e3aaa5d235a4e..f7800ed72b2c0 100644
--- a/superset/tags/exceptions.py
+++ b/superset/tags/exceptions.py
@@ -17,17 +17,6 @@
from flask_babel import lazy_gettext as _
from marshmallow.validate import ValidationError
-from superset.commands.exceptions import (
- CommandException,
- CommandInvalidError,
- CreateFailedError,
- DeleteFailedError,
- ForbiddenError,
- ImportFailedError,
- UpdateFailedError,
-)
-
-
class InvalidTagNameError(ValidationError):
"""
Marshmallow validation error for invalid Tag name
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index b9831a82ffe60..b1e6d1f510d4f 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -26,7 +26,6 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from jinja2.sandbox import SandboxedEnvironment
-from marshmallow import ValidationError
from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
@@ -38,8 +37,7 @@
from superset.superset_typing import FlaskResponse
from superset.tags.commands.create import CreateCustomTagCommand
from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
-from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
-from superset.tags.schemas import TagPostSchema
+from superset.tags.models import ObjectTypes, Tag, TaggedObject
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
@@ -115,7 +113,7 @@ def get( # pylint: disable=no-self-use
@has_access_api
@expose("/tags///", methods=["POST"])
- def post( # pylint: disable=no-self-use
+ def post(
self, object_type: ObjectTypes, object_id: int
) -> FlaskResponse:
"""
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 7ea86391a87be..68a7cd6111237 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -26,7 +26,6 @@
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from jinja2.sandbox import SandboxedEnvironment
-from marshmallow import ValidationError
from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
@@ -39,8 +38,7 @@
from superset.superset_typing import FlaskResponse
from superset.tags.commands.create import CreateCustomTagCommand
from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
-from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
-from superset.tags.schemas import TagPostSchema
+from superset.tags.models import ObjectTypes, Tag, TaggedObject
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
@@ -139,7 +137,7 @@ def get( # pylint: disable=no-self-use
@has_access_api
@expose("/tags///", methods=["POST"])
- def post( # pylint: disable=no-self-use
+ def post(
self, object_type: ObjectTypes, object_id: int
) -> FlaskResponse:
"""
From 7bf806f8b06a429bc14f661f36e72ec9654ef273 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Thu, 19 Jan 2023 14:15:57 -0500
Subject: [PATCH 61/76] precommit fixes
---
superset/tags/exceptions.py | 1 +
superset/views/all_entities.py | 4 +---
superset/views/tags.py | 4 +---
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/superset/tags/exceptions.py b/superset/tags/exceptions.py
index f7800ed72b2c0..d1d9005bb962c 100644
--- a/superset/tags/exceptions.py
+++ b/superset/tags/exceptions.py
@@ -17,6 +17,7 @@
from flask_babel import lazy_gettext as _
from marshmallow.validate import ValidationError
+
class InvalidTagNameError(ValidationError):
"""
Marshmallow validation error for invalid Tag name
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index b1e6d1f510d4f..f8eec6d9521c2 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -113,9 +113,7 @@ def get( # pylint: disable=no-self-use
@has_access_api
@expose("/tags///", methods=["POST"])
- def post(
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
+ def post(self, object_type: ObjectTypes, object_id: int) -> FlaskResponse:
"""
---
post:
diff --git a/superset/views/tags.py b/superset/views/tags.py
index 68a7cd6111237..bbb338b0932a9 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -137,9 +137,7 @@ def get( # pylint: disable=no-self-use
@has_access_api
@expose("/tags///", methods=["POST"])
- def post(
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
+ def post(self, object_type: ObjectTypes, object_id: int) -> FlaskResponse:
"""
---
post:
From 137d4d6f8a618cdf99da0acb5e97792b2edf2a4a Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Fri, 20 Jan 2023 09:35:12 -0500
Subject: [PATCH 62/76] fixing properties modal tests to not include tags
---
.../components/PropertiesModal/PropertiesModal.test.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
index e5d0cbf1b16ff..882c9daac088a 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
@@ -178,7 +178,7 @@ test('should render - FeatureFlag disabled', async () => {
expect(
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
- expect(screen.getAllByRole('heading')).toHaveLength(6);
+ expect(screen.getAllByRole('heading')).toHaveLength(5);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument();
@@ -217,7 +217,6 @@ test('should render - FeatureFlag enabled', async () => {
expect(
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
- expect(screen.getByRole('heading', { name: 'Tags' })).toBeInTheDocument();
expect(screen.getAllByRole('heading')).toHaveLength(5);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
From e34360945d42be7cb2addcaa19c7c061afd56886 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Fri, 20 Jan 2023 10:13:06 -0500
Subject: [PATCH 63/76] added check for tags heading
---
.../components/PropertiesModal/PropertiesModal.test.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
index 882c9daac088a..2ef1fe2358956 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
@@ -217,6 +217,10 @@ test('should render - FeatureFlag enabled', async () => {
expect(
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
+ // Tags will be included since isFeatureFlag always returns true in this test
+ expect(
+ screen.getByRole('heading', { name: 'Tags' }),
+ ).toBeInTheDocument();
expect(screen.getAllByRole('heading')).toHaveLength(5);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
From 7ca0509a93e87ebdd42d7f45e402ec52c1b00d00 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Fri, 20 Jan 2023 13:09:20 -0500
Subject: [PATCH 64/76] lint fix
---
.../components/PropertiesModal/PropertiesModal.test.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
index 2ef1fe2358956..3fde5e8ca6e9c 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/PropertiesModal.test.tsx
@@ -218,9 +218,7 @@ test('should render - FeatureFlag enabled', async () => {
screen.getByRole('heading', { name: 'Certification' }),
).toBeInTheDocument();
// Tags will be included since isFeatureFlag always returns true in this test
- expect(
- screen.getByRole('heading', { name: 'Tags' }),
- ).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Tags' })).toBeInTheDocument();
expect(screen.getAllByRole('heading')).toHaveLength(5);
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
From 2ebaaccd8bb00f8fbeb875f271cb87a47feab955 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Wed, 1 Feb 2023 15:34:22 -0500
Subject: [PATCH 65/76] moved endpoints to api and added test
---
superset-frontend/src/tags.ts | 88 +++++--
.../views/CRUD/allentities/AllEntities.tsx | 47 ++--
.../CRUD/allentities/AllEntitiesTable.tsx | 7 +-
.../src/views/CRUD/tags/TagList.tsx | 2 +-
superset/constants.py | 4 +
superset/dashboards/api.py | 4 +-
superset/tags/api.py | 242 +++++++++++++++--
superset/tags/commands/create.py | 28 +-
superset/tags/commands/delete.py | 114 ++++++++
superset/tags/commands/exceptions.py | 40 ++-
superset/tags/commands/utils.py | 28 ++
superset/tags/dao.py | 206 ++++++++++++++-
superset/tags/schemas.py | 12 +
superset/views/all_entities.py | 199 +-------------
superset/views/tags.py | 247 +-----------------
tests/integration_tests/tags/api_tests.py | 215 ++++++++++++++-
.../integration_tests/tags/commands_tests.py | 158 +++++++++++
tests/integration_tests/tags/dao_tests.py | 228 ++++++++++++++--
18 files changed, 1319 insertions(+), 550 deletions(-)
create mode 100644 superset/tags/commands/delete.py
create mode 100644 superset/tags/commands/utils.py
create mode 100644 tests/integration_tests/tags/commands_tests.py
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index cf5a3432623ad..d13be2f0ffa90 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -19,17 +19,37 @@
import { JsonObject, SupersetClient } from '@superset-ui/core';
import Tag from 'src/types/TagType';
+export const OBJECT_TYPES_VALUES = Object.freeze([
+ 'dashboard',
+ 'chart',
+ 'saved_query',
+]);
+
export const OBJECT_TYPES = Object.freeze({
DASHBOARD: 'dashboard',
CHART: 'chart',
- QUERY: 'query',
+ QUERY: 'saved_query',
});
+const OBJECT_TYPE_ID_MAP = {
+ saved_query: 1,
+ chart: 2,
+ dashboard: 3,
+};
+
+const map_object_type_to_id = (objectType: string) => {
+ if (!OBJECT_TYPES_VALUES.includes(objectType)) {
+ const msg = `objectType ${objectType} is invalid`;
+ throw new Error(msg);
+ }
+ return OBJECT_TYPE_ID_MAP[objectType];
+};
+
export function fetchAllTags(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- const url = `/tagview/tags/`;
+ const url = `/api/v1/tag`;
SupersetClient.get({ endpoint: url })
.then(({ json }) => callback(json))
.catch(response => error(response));
@@ -40,19 +60,29 @@ export function fetchTags(
objectType,
objectId,
includeTypes = false,
- }: { objectType: string; objectId: number; includeTypes: boolean },
+ }: {
+ objectType: string;
+ objectId: number;
+ includeTypes: boolean;
+ },
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
if (objectType === undefined || objectId === undefined) {
throw new Error('Need to specify objectType and objectId');
}
+ if (!OBJECT_TYPES_VALUES.includes(objectType)) {
+ const msg = `objectType ${objectType} is invalid`;
+ throw new Error(msg);
+ }
SupersetClient.get({
- endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
+ endpoint: `/api/v1/${objectType}/${objectId}`,
})
.then(({ json }) =>
callback(
- json.filter((tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes),
+ json['result']['tags'].filter(
+ (tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes,
+ ),
),
)
.catch(response => error(response));
@@ -81,13 +111,20 @@ export function deleteTaggedObjects(
if (objectType === undefined || objectId === undefined) {
throw new Error('Need to specify objectType and objectId');
}
+ if (!OBJECT_TYPES_VALUES.includes(objectType)) {
+ const msg = `objectType ${objectType} is invalid`;
+ throw new Error(msg);
+ }
SupersetClient.delete({
- endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
- body: JSON.stringify([tag.name]),
- parseMethod: 'text',
+ endpoint: `/api/v1/tag/${map_object_type_to_id(objectType)}/${objectId}/`,
+ body: JSON.stringify({ tag: tag.name }),
+ parseMethod: 'json',
+ headers: { 'Content-Type': 'application/json' },
})
- .then(({ text }) =>
- text ? callback(text) : callback('Successfully Deleted Tagged Objects'),
+ .then(({ json }) =>
+ json
+ ? callback(JSON.stringify(json))
+ : callback('Successfully Deleted Tagged Objects'),
)
.catch(response => {
const err_str = response.message;
@@ -102,12 +139,15 @@ export function deleteTags(
) {
const tags_str = JSON.stringify(tags.map(tag => tag.name) as string[]);
SupersetClient.delete({
- endpoint: `/tagview/tags`,
+ endpoint: `/api/v1/tag/`,
body: tags_str,
- parseMethod: 'text',
+ parseMethod: 'json',
+ headers: { 'Content-Type': 'application/json' },
})
- .then(({ text }) =>
- text ? callback(text) : callback('Successfully Deleted Tag'),
+ .then(({ json }) =>
+ json.message
+ ? callback(json.message)
+ : callback('Successfully Deleted Tag'),
)
.catch(response => {
const err_str = response.message;
@@ -120,7 +160,11 @@ export function addTag(
objectType,
objectId,
includeTypes = false,
- }: { objectType: string; objectId: number; includeTypes: boolean },
+ }: {
+ objectType: string;
+ objectId: number;
+ includeTypes: boolean;
+ },
tag: string,
callback: (text: string) => void,
error: (response: Response) => void,
@@ -131,12 +175,14 @@ export function addTag(
if (tag.indexOf(':') !== -1 && !includeTypes) {
return;
}
+ const objectTypeId = map_object_type_to_id(objectType);
SupersetClient.post({
- endpoint: `/taggedobjectview/tags/${objectType}/${objectId}/`,
- body: JSON.stringify([tag]),
- parseMethod: 'text',
+ endpoint: `/api/v1/tag/${objectTypeId}/${objectId}/`,
+ body: JSON.stringify({ tags: [tag] }),
+ parseMethod: 'json',
+ headers: { 'Content-Type': 'application/json' },
})
- .then(({ text }) => callback(text))
+ .then(({ json }) => callback(JSON.stringify(json)))
.catch(response => error(response));
}
@@ -145,11 +191,11 @@ export function fetchObjects(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- let url = `/taggedobjectview/tagged_objects/?tags=${tags}`;
+ let url = `/api/v1/tag/get_objects/?tags=${tags}`;
if (types) {
url += `&types=${types}`;
}
SupersetClient.get({ endpoint: url })
- .then(({ json }) => callback(json))
+ .then(({ json }) => callback(json['result']))
.catch(response => error(response));
}
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
index 2950a544eb93f..6c85c676eb9dc 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
@@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useEffect, useState } from 'react';
-import { logging, styled, t } from '@superset-ui/core';
-import Tag from 'src/types/TagType';
+import React, { useMemo } from 'react';
+import { ensureIsArray, styled, t } from '@superset-ui/core';
import { StringParam, useQueryParam } from 'use-query-params';
import withToasts from 'src/components/MessageToasts/withToasts';
-import SelectControl from 'src/explore/components/controls/SelectControl';
-import { fetchSuggestions } from 'src/tags';
-import { addDangerToast } from 'src/components/MessageToasts/actions';
import AllEntitiesTable from './AllEntitiesTable';
+import { AsyncSelect } from 'src/components';
+import { SelectValue } from 'antd/lib/select';
+import { loadTags } from 'src/components/Tags/utils';
+import { getValue } from 'src/components/Select/utils';
const AllEntitiesContainer = styled.div`
${({ theme }) => `
@@ -54,28 +54,20 @@ const AllEntitiesNav = styled.div`
`;
function AllEntities() {
- const [tagSuggestions, setTagSuggestions] = useState();
const [tagsQuery, setTagsQuery] = useQueryParam('tags', StringParam);
- useEffect(() => {
- fetchSuggestions(
- { includeTypes: false },
- (suggestions: Tag[]) => {
- const tagSuggestions = [...suggestions.map(tag => tag.name)];
- setTagSuggestions(tagSuggestions);
- },
- (error: Response) => {
- logging.log(error.json());
- addDangerToast(`Error Fetching Suggestions`);
- },
- );
- }, [tagsQuery]);
-
- const onTagSearchChange = (tags: Tag[]) => {
+ const onTagSearchChange = (value: SelectValue) => {
+ const tags = ensureIsArray(value).map(tag => getValue(tag));
const tagSearch = tags.join(',');
setTagsQuery(tagSearch);
};
+ const tagsValue = useMemo(() => {
+ return tagsQuery
+ ? tagsQuery.split(',').map(tag => ({ value: tag, label: tag }))
+ : [];
+ }, [tagsQuery]);
+
return (
@@ -83,12 +75,13 @@ function AllEntities() {
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
index 391e910de7e85..648644ba1570b 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntitiesTable.tsx
@@ -72,14 +72,15 @@ export default function AllEntitiesTable({
{ tags: search, types: null },
(data: TaggedObject[]) => {
const objects = { dashboard: [], chart: [], query: [] };
- data.forEach(object => {
- objects[object.type].push(object);
+ data.forEach(function (object) {
+ const object_type = object.type;
+ objects[object_type].push(object);
});
setObjects(objects);
},
(error: Response) => {
addDangerToast('Error Fetching Tagged Objects');
- logging.log(error.json());
+ logging.log(error.text);
},
);
}, [search]);
diff --git a/superset-frontend/src/views/CRUD/tags/TagList.tsx b/superset-frontend/src/views/CRUD/tags/TagList.tsx
index 4561c965f4d90..92028168f4558 100644
--- a/superset-frontend/src/views/CRUD/tags/TagList.tsx
+++ b/superset-frontend/src/views/CRUD/tags/TagList.tsx
@@ -204,7 +204,7 @@ function TagList(props: TagListProps) {
Header: t('Search'),
id: 'name',
input: 'search',
- operator: FilterOperator.titleOrSlug,
+ operator: FilterOperator.contains,
},
] as Filters;
return filters_list;
diff --git a/superset/constants.py b/superset/constants.py
index 10de5c52f04a7..1a38bf51a6c60 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -141,6 +141,10 @@ class RouteMethod: # pylint: disable=too-few-public-methods
"samples": "read",
"delete_ssh_tunnel": "write",
"stop_query": "read",
+ "get_objects": "read",
+ "get_all_objects": "read",
+ "add_tagged_objects": "write",
+ "delete_tagged_object": "write",
}
EXTRA_FORM_DATA_APPEND_KEYS = {
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index a8b186f91beb6..f930da4290237 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -224,8 +224,9 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"published",
"roles",
"slug",
+ "tags",
)
- if not is_feature_enabled("TAGGING_SYSTEM")
+ if is_feature_enabled("TAGGING_SYSTEM")
else (
"created_by",
"changed_by",
@@ -235,7 +236,6 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"published",
"roles",
"slug",
- "tags",
)
)
search_filters = {
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 6b2a239c15512..4036fac2811e1 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -19,27 +19,47 @@
from flask import request, Response
from flask_appbuilder.api import expose, protect, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
-from marshmallow import ValidationError
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.extensions import event_logger
from superset.tags.commands.create import CreateCustomTagCommand
-from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
+from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
+from superset.tags.commands.exceptions import (
+ TagCreateFailedError,
+ TagDeleteFailedError,
+ TaggedObjectDeleteFailedError,
+ TaggedObjectNotFoundError,
+ TagInvalidError,
+ TagNotFoundError,
+)
+from superset.tags.dao import TagDAO
from superset.tags.models import ObjectTypes, Tag
from superset.tags.schemas import (
openapi_spec_methods_override,
+ TaggedObjectEntityResponseSchema,
TagGetResponseSchema,
TagPostSchema,
)
-from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
+from superset.views.base_api import (
+ BaseSupersetModelRestApi,
+ RelatedFieldFilter,
+ statsd_metrics,
+)
+from superset.views.filters import BaseFilterRelatedUsers, FilterRelatedOwners
logger = logging.getLogger(__name__)
class TagRestApi(BaseSupersetModelRestApi):
datamodel = SQLAInterface(Tag)
-
- include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET
+ include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
+ RouteMethod.RELATED,
+ "bulk_delete",
+ "get_objects",
+ "get_all_objects",
+ "add_tagged_objects",
+ "delete_tagged_object",
+ }
resource_name = "tag"
allow_browser_login = True
@@ -72,8 +92,18 @@ class TagRestApi(BaseSupersetModelRestApi):
"created_by",
]
+ base_related_field_filters = {
+ "created_by": [["id", BaseFilterRelatedUsers, lambda: []]],
+ }
+
+ related_field_filters = {
+ "created_by": RelatedFieldFilter("first_name", FilterRelatedOwners),
+ }
+ allowed_rel_fields = {"created_by"}
+
add_model_schema = TagPostSchema()
tag_get_response_schema = TagGetResponseSchema()
+ object_entity_response_schema = TaggedObjectEntityResponseSchema()
openapi_spec_tag = "Tags"
""" Override the name set for this collection of endpoints """
@@ -94,15 +124,15 @@ def __repr__(self) -> str:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
log_to_statsd=False,
)
- def add_new_tags(self, object_type: ObjectTypes, object_id: int) -> Response:
- """Adds new tags to an object.
+ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
+ """Adds tags to an object.
---
post:
description: >-
- Add new tags to an object..
+ Add tags to an object..
requestBody:
description: Tag schema
required: true
@@ -143,13 +173,195 @@ def add_new_tags(self, object_type: ObjectTypes, object_id: int) -> Response:
$ref: '#/components/responses/500'
"""
try:
- item = self.add_model_schema.load(request.json)
- # This validates custom Schema with custom validations
- except ValidationError as error:
- return self.response_400(message=error.messages)
+ tags = request.json["tags"]
+ # This validates custom Schema with custom validations
+ CreateCustomTagCommand(object_type, object_id, tags).run()
+ return self.response(201)
+ except KeyError:
+ return self.response(
+ 400, message="Missing required 'tags' field in request body"
+ )
+ except TagInvalidError:
+ return self.response(422, message="Invalid tag")
+ except TagCreateFailedError as ex:
+ logger.error(
+ "Error creating model %s: %s",
+ self.__class__.__name__,
+ str(ex),
+ exc_info=True,
+ )
+ return self.response_422(message=str(ex))
+
+ @expose("///", methods=["DELETE"])
+ @protect()
+ @safe
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
+ log_to_statsd=True,
+ )
+ def delete_tagged_object(
+ self, object_type: ObjectTypes, object_id: int
+ ) -> Response:
+ """Deletes a Tagged Object
+ ---
+ delete:
+ description: >-
+ Deletes a Tagged Object.
+ requestBody:
+ description: Tag name
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.delete'
+ parameters:
+ - in: path
+ schema:
+ type: integer
+ name: object_type
+ - in: path
+ schema:
+ type: integer
+ name: object_id
+ responses:
+ 200:
+ description: Chart delete
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ 401:
+ $ref: '#/components/responses/401'
+ 403:
+ $ref: '#/components/responses/403'
+ 404:
+ $ref: '#/components/responses/404'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ try:
+ tag = request.json["tag"]
+ DeleteTaggedObjectCommand(object_type, object_id, tag).run()
+ return self.response(200, message="OK")
+ except TagInvalidError:
+ return self.response_422()
+ except TagNotFoundError:
+ return self.response_404()
+ except TaggedObjectNotFoundError:
+ return self.response_404()
+ except TaggedObjectDeleteFailedError as ex:
+ logger.error(
+ "Error deleting tagged object %s: %s",
+ self.__class__.__name__,
+ str(ex),
+ exc_info=True,
+ )
+ return self.response_422(message=str(ex))
+
+ @expose("/", methods=["DELETE"])
+ @protect()
+ @safe
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
+ log_to_statsd=False,
+ )
+ def bulk_delete(self) -> Response:
+ """Delete Tags
+ ---
+ delete:
+ description: >-
+ Deletes multiple Tags.
+ requestBody:
+ description: List of Tag names
+ required: true
+ name: tags
+ responses:
+ 200:
+ description: Deletes multiple Tags
+ content:
+ application/json:
+ 401:
+ $ref: '#/components/responses/401'
+ 403:
+ $ref: '#/components/responses/403'
+ 404:
+ $ref: '#/components/responses/404'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ tags = request.json
+ try:
+ DeleteTagsCommand(tags).run()
+ return self.response(200, message=f"Deleted {len(tags)} tags")
+ except TagNotFoundError:
+ return self.response_404()
+ except TagInvalidError as ex:
+ return self.response(422, message=f"Invalid tag parameters: {tags}. {ex}")
+ except TagDeleteFailedError as ex:
+ return self.response_422(message=str(ex))
+
+ @expose("/get_objects/", methods=["GET"])
+ @protect()
+ @safe
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_objects",
+ log_to_statsd=False,
+ )
+ def get_objects(self) -> Response:
+ """Gets all objects associated with a Tag
+ ---
+ get:
+ description: >-
+ Gets all objects associated with a Tag.
+ parameters:
+ - in: path
+ schema:
+ type: integer
+ name: tag_id
+ responses:
+ 200:
+ description: Objects fetched
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: number
+ result:
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
+ 302:
+ description: Redirects to the current digest
+ 400:
+ $ref: '#/components/responses/400'
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ tags = [tag for tag in request.args.get("tags", "").split(",") if tag]
+ # filter types
+ types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
+
try:
- new_model = CreateCustomTagCommand(object_type, object_id, item).run()
- return self.response(201, id=new_model.id, result=item)
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(tags, types)
+ result = [
+ self.object_entity_response_schema.dump(tagged_object)
+ for tagged_object in tagged_objects
+ ]
+ return self.response(200, result=result)
except TagInvalidError as ex:
return self.response_422(message=ex.normalized_messages())
except TagCreateFailedError as ex:
diff --git a/superset/tags/commands/create.py b/superset/tags/commands/create.py
index 06d6026eac401..e9afe4a38d4a9 100644
--- a/superset/tags/commands/create.py
+++ b/superset/tags/commands/create.py
@@ -15,13 +15,12 @@
# specific language governing permissions and limitations
# under the License.
import logging
-from typing import Any, Dict
-
-from flask_appbuilder.models.sqla import Model
+from typing import List
from superset.commands.base import BaseCommand, CreateMixin
from superset.dao.exceptions import DAOCreateFailedError
from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
+from superset.tags.commands.utils import to_object_type
from superset.tags.dao import TagDAO
from superset.tags.models import ObjectTypes
@@ -29,28 +28,37 @@
class CreateCustomTagCommand(CreateMixin, BaseCommand):
- def __init__(self, object_type: ObjectTypes, object_id: int, data: Dict[str, Any]):
+ def __init__(self, object_type: ObjectTypes, object_id: int, tags: List[str]):
self._object_type = object_type
self._object_id = object_id
- self._properties = data.copy()
+ self._tags = tags
- def run(self) -> Model:
+ def run(self) -> None:
self.validate()
try:
- tag = TagDAO.create_custom_tagged_objects(
- self._object_type, self._object_id, self._properties
+ object_type = to_object_type(self._object_type)
+ if object_type is None:
+ raise TagCreateFailedError(f"invalid object type {self._object_type}")
+ TagDAO.create_custom_tagged_objects(
+ object_type=object_type,
+ object_id=self._object_id,
+ tag_names=self._tags,
)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
raise TagCreateFailedError() from ex
- return tag
def validate(self) -> None:
exceptions = []
-
# Validate object_id
if self._object_id == 0:
exceptions.append(TagCreateFailedError())
+ # Validate object type
+ object_type = to_object_type(self._object_type)
+ if not object_type:
+ exceptions.append(
+ TagCreateFailedError(f"invalid object type {self._object_type}")
+ )
if exceptions:
exception = TagInvalidError()
exception.add_list(exceptions)
diff --git a/superset/tags/commands/delete.py b/superset/tags/commands/delete.py
new file mode 100644
index 0000000000000..2113c576f6bec
--- /dev/null
+++ b/superset/tags/commands/delete.py
@@ -0,0 +1,114 @@
+# 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 logging
+from typing import List
+
+
+from superset.commands.base import BaseCommand
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.tags.commands.exceptions import (
+ TagDeleteFailedError,
+ TagInvalidError,
+ TagNotFoundError,
+ TaggedObjectDeleteFailedError,
+ TaggedObjectNotFoundError
+)
+from superset.tags.commands.utils import to_object_type
+from superset.tags.dao import TagDAO
+from superset.tags.models import ObjectTypes
+from superset.views.base import DeleteMixin
+
+logger = logging.getLogger(__name__)
+
+
+class DeleteTaggedObjectCommand(DeleteMixin, BaseCommand):
+ def __init__(self, object_type: ObjectTypes, object_id: int, tag: str):
+ self._object_type = object_type
+ self._object_id = object_id
+ self._tag = tag
+
+ def run(self) -> None:
+ self.validate()
+ try:
+ object_type = to_object_type(self._object_type)
+ if object_type is None:
+ raise TaggedObjectDeleteFailedError(
+ f'invalid object type {self._object_type}')
+ TagDAO.delete_tagged_object(
+ object_type, self._object_id, self._tag
+ )
+ except DAODeleteFailedError as ex:
+ logger.exception(ex.exception)
+ raise TaggedObjectDeleteFailedError() from ex
+
+ def validate(self) -> None:
+ exceptions = []
+ # Validate required arguments provided
+ if not (self._object_id and self._object_type):
+ exceptions.append(TaggedObjectDeleteFailedError())
+ # Validate tagged object exists
+ tag = TagDAO.find_by_name(self._tag)
+ if not tag:
+ exceptions.append(TaggedObjectDeleteFailedError(
+ f'could not find tag: {self._tag}'
+ ))
+ else:
+ # Validate object type
+ object_type = to_object_type(self._object_type)
+ if object_type is None:
+ exceptions.append(TaggedObjectDeleteFailedError(
+ f'invalid object type {self._object_type}'))
+ else:
+ tagged_object = TagDAO.find_tagged_object(
+ object_type=object_type,
+ object_id=self._object_id,
+ tag_id=tag.id
+ )
+ if tagged_object is None:
+ exceptions.append(TaggedObjectNotFoundError(
+ object_id=self._object_id,
+ object_type=object_type.name,
+ tag_name=self._tag
+ ))
+ if exceptions:
+ exception = TagInvalidError()
+ exception.add_list(exceptions)
+ raise exception
+
+
+class DeleteTagsCommand(DeleteMixin, BaseCommand):
+ def __init__(self, tags: List[str]):
+ self._tags = tags
+
+ def run(self) -> None:
+ self.validate()
+ try:
+ TagDAO.delete_tags(self._tags)
+ except DAODeleteFailedError as ex:
+ logger.exception(ex.exception)
+ raise TagDeleteFailedError() from ex
+
+ def validate(self) -> None:
+ exceptions = []
+ # Validate tag exists
+ for tag in self._tags:
+ if not TagDAO.find_by_name(tag):
+ exceptions.append(TagNotFoundError(tag))
+ if exceptions:
+ exception = TagInvalidError()
+ exception.add_list(exceptions)
+ raise exception
diff --git a/superset/tags/commands/exceptions.py b/superset/tags/commands/exceptions.py
index 5e218fed73f1d..9847c949bf7ec 100644
--- a/superset/tags/commands/exceptions.py
+++ b/superset/tags/commands/exceptions.py
@@ -14,9 +14,16 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+from typing import Optional
+
from flask_babel import lazy_gettext as _
-from superset.commands.exceptions import CommandInvalidError, CreateFailedError
+from superset.commands.exceptions import (
+ CommandException,
+ CommandInvalidError,
+ CreateFailedError,
+ DeleteFailedError,
+)
class TagInvalidError(CommandInvalidError):
@@ -25,3 +32,34 @@ class TagInvalidError(CommandInvalidError):
class TagCreateFailedError(CreateFailedError):
message = _("Tag could not be created.")
+
+
+class TagDeleteFailedError(DeleteFailedError):
+ message = _("Tag could not be deleted.")
+
+
+class TaggedObjectDeleteFailedError(DeleteFailedError):
+ message = _("Tagged Object could not be deleted.")
+
+
+class TagNotFoundError(CommandException):
+ def __init__(self, tag_name: Optional[str] = None) -> None:
+ message = "Tag not found."
+ if tag_name:
+ message = f"Tag with name {tag_name} not found."
+ super().__init__(message)
+
+
+class TaggedObjectNotFoundError(CommandException):
+ def __init__(
+ self,
+ object_id: Optional[int] = None,
+ object_type: Optional[str] = None,
+ tag_name: Optional[str] = None,
+ ) -> None:
+ message = "Tagged Object not found."
+ if object_id and object_type and tag_name:
+ message = f'Tagged object with object_id: {object_id} \
+ object_type: {object_type} \
+ and tag name: "{tag_name}" could not be found'
+ super().__init__(message)
diff --git a/superset/tags/commands/utils.py b/superset/tags/commands/utils.py
new file mode 100644
index 0000000000000..89035503a391a
--- /dev/null
+++ b/superset/tags/commands/utils.py
@@ -0,0 +1,28 @@
+# 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 typing import Optional, Union
+from superset.tags.models import ObjectTypes
+
+
+def to_object_type(object_type: Union[ObjectTypes, int, str]) -> Optional[ObjectTypes]:
+ if isinstance(object_type, ObjectTypes):
+ return object_type
+ for type_ in ObjectTypes:
+ if (object_type in [type_.value, type_.name]):
+ return type_
+ return None
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index a2f4eb6d7f018..3d68098a6a1c7 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -15,12 +15,16 @@
# specific language governing permissions and limitations
# under the License.
import logging
-from typing import Any, Dict
+from operator import and_
+from typing import Any, Dict, List, Optional
from superset.dao.base import BaseDAO
-from superset.dao.exceptions import DAOCreateFailedError
+from superset.dao.exceptions import DAOCreateFailedError, DAODeleteFailedError
from superset.extensions import db
-from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from superset.models.sql_lab import SavedQuery
+from superset.tags.models import get_tag, ObjectTypes, Tag, TaggedObject, TagTypes
logger = logging.getLogger(__name__)
@@ -28,6 +32,7 @@
class TagDAO(BaseDAO):
model_cls = Tag
# base_filter = TagAccessFilter
+
@staticmethod
def validate_tag_name(tag_name: str) -> bool:
if ":" in tag_name:
@@ -36,10 +41,8 @@ def validate_tag_name(tag_name: str) -> bool:
@staticmethod
def create_custom_tagged_objects(
- object_type: ObjectTypes, object_id: int, properties: Dict[str, Any]
+ object_type: ObjectTypes, object_id: int, tag_names: List[str]
) -> None:
- tag_names = properties["tags"]
-
tagged_objects = []
for name in tag_names:
if not TagDAO.validate_tag_name(name):
@@ -57,8 +60,193 @@ def create_custom_tagged_objects(
db.session.commit()
@staticmethod
- def get_by_name(name: str, type_: TagTypes) -> Tag:
- tag = db.session.query(Tag).filter(Tag.name == name, Tag.type == type_).first()
+ def delete_tagged_object(
+ object_type: ObjectTypes, object_id: int, tag_name: str
+ ) -> None:
+ """
+ deletes a tagged object by the object_id, object_type, and tag_name
+ """
+ tag = TagDAO.find_by_name(tag_name.strip())
+ if not tag:
+ raise DAODeleteFailedError(
+ message=f"Tag with name {tag_name} does not exist."
+ )
+
+ tagged_object = db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id == tag.id,
+ TaggedObject.object_type == object_type,
+ TaggedObject.object_id == object_id,
+ )
+ if not tagged_object:
+ raise DAODeleteFailedError(
+ message=f'Tagged object with object_id: {object_id} \
+ object_type: {object_type} \
+ and tag name: "{tag_name}" could not be found'
+ )
+ deleted = tagged_object.delete()
+ if not deleted:
+ raise DAODeleteFailedError(
+ message=f'Tagged object with object_id: {object_id} \
+ object_type: {object_type} \
+ and tag name: "{tag_name}" could not be deleted'
+ )
+ db.session.commit()
+
+ @staticmethod
+ def delete_tags(tag_names: List[str]) -> None:
+ """
+ deletes tags from a list of tag names
+ """
+ tags_to_delete = []
+ for name in tag_names:
+ tag_name = name.strip()
+ if not TagDAO.find_by_name(tag_name):
+ raise DAODeleteFailedError(
+ message=f"Tag with name {tag_name} does not exist."
+ )
+ tags_to_delete.append(tag_name)
+ db.session.query(Tag).filter(Tag.name.in_(tags_to_delete)).delete()
+ db.session.commit()
+
+ @staticmethod
+ def get_by_name(name: str, type_: TagTypes = TagTypes.custom) -> Tag:
+ """
+ returns a tag if one exists by that name, none otherwise.
+ important!: Creates a tag by that name if the tag is not found.
+ """
+ tag = (
+ db.session.query(Tag)
+ .filter(Tag.name == name, Tag.type == type_.name)
+ .first()
+ )
if not tag:
- tag = Tag(name=name, type=type_)
+ tag = get_tag(name, db.session, type_)
return tag
+
+ @staticmethod
+ def find_by_name(name: str) -> Tag:
+ """
+ returns a tag if one exists by that name, none otherwise.
+ Does NOT create a tag if the tag is not found.
+ """
+ return db.session.query(Tag).filter(Tag.name == name).first()
+
+ @staticmethod
+ def find_tagged_object(
+ object_type: ObjectTypes, object_id: int, tag_id: int
+ ) -> TaggedObject:
+ """
+ returns a tagged object if one exists by that name, none otherwise.
+ """
+ return (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tag_id,
+ TaggedObject.object_id == object_id,
+ TaggedObject.object_type == object_type,
+ )
+ .first()
+ )
+
+ @staticmethod
+ def get_tagged_objects_for_tags(
+ tags: Optional[List[str]] = None, obj_types: Optional[List[str]] = None
+ ) -> List[Dict[str, Any]]:
+ """
+ returns a list of tagged objects filtered by tag names and object types
+ if no filters applied returns all tagged objects
+ """
+ # id = fields.Int()
+ # type = fields.String()
+ # name = fields.String()
+ # url = fields.String()
+ # changed_on = fields.DateTime()
+ # created_by = fields.Nested(UserSchema)
+ # creator = fields.String(
+
+ # filter types
+
+ results: List[Dict[str, Any]] = []
+
+ # dashboards
+ if not obj_types or "dashboard" in obj_types:
+ dashboards = (
+ db.session.query(Dashboard)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(not tags or Tag.name.in_(tags))
+ )
+
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.dashboard.name,
+ "name": obj.dashboard_title,
+ "url": obj.url,
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in dashboards
+ )
+
+ # charts
+ if not obj_types or "chart" in obj_types:
+ charts = (
+ db.session.query(Slice)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Slice.id,
+ TaggedObject.object_type == ObjectTypes.chart,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(not tags or Tag.name.in_(tags))
+ )
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.chart.name,
+ "name": obj.slice_name,
+ "url": obj.url,
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in charts
+ )
+
+ # saved queries
+ if not obj_types or "query" in obj_types:
+ saved_queries = (
+ db.session.query(SavedQuery)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == SavedQuery.id,
+ TaggedObject.object_type == ObjectTypes.query,
+ ),
+ )
+ .join(Tag, TaggedObject.tag_id == Tag.id)
+ .filter(not tags or Tag.name.in_(tags))
+ )
+ results.extend(
+ {
+ "id": obj.id,
+ "type": ObjectTypes.query.name,
+ "name": obj.label,
+ "url": obj.url(),
+ "changed_on": obj.changed_on,
+ "created_by": obj.created_by_fk,
+ "creator": obj.creator(),
+ }
+ for obj in saved_queries
+ )
+ return results
diff --git a/superset/tags/schemas.py b/superset/tags/schemas.py
index e823a9c2aa749..0496eef916afb 100644
--- a/superset/tags/schemas.py
+++ b/superset/tags/schemas.py
@@ -16,6 +16,8 @@
# under the License.
from marshmallow import fields, Schema
+from superset.dashboards.schemas import UserSchema
+
object_type_description = "A title for the tag."
openapi_spec_methods_override = {
@@ -35,6 +37,16 @@
}
+class TaggedObjectEntityResponseSchema(Schema):
+ id = fields.Int()
+ type = fields.String()
+ name = fields.String()
+ url = fields.String()
+ changed_on = fields.DateTime()
+ created_by = fields.Nested(UserSchema)
+ creator = fields.String()
+
+
class TagGetResponseSchema(Schema):
id = fields.Int()
name = fields.String()
diff --git a/superset/views/all_entities.py b/superset/views/all_entities.py
index f8eec6d9521c2..4031d81d2129e 100644
--- a/superset/views/all_entities.py
+++ b/superset/views/all_entities.py
@@ -17,30 +17,21 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
-from typing import Any, Dict, List
-import simplejson as json
-from flask import request, Response
from flask_appbuilder import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
-from flask_appbuilder.security.decorators import has_access, has_access_api
+from flask_appbuilder.security.decorators import has_access
from jinja2.sandbox import SandboxedEnvironment
-from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
-from superset import db, is_feature_enabled, utils
+from superset import is_feature_enabled
from superset.jinja_context import ExtraCache
-from superset.models.dashboard import Dashboard
-from superset.models.slice import Slice
-from superset.models.sql_lab import SavedQuery
from superset.superset_typing import FlaskResponse
-from superset.tags.commands.create import CreateCustomTagCommand
-from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
-from superset.tags.models import ObjectTypes, Tag, TaggedObject
+from superset.tags.models import Tag
from superset.views.base import SupersetModelView
-from .base import BaseSupersetView, json_success
+from .base import BaseSupersetView
logger = logging.getLogger(__name__)
@@ -78,185 +69,3 @@ def is_enabled() -> bool:
def ensure_enabled(self) -> None:
if not self.is_enabled():
raise NotFound()
-
- @has_access_api
- @expose("/tags/suggestions/", methods=["GET"])
- def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
- query = (
- db.session.query(TaggedObject)
- .join(Tag)
- .with_entities(TaggedObject.tag_id, Tag.name)
- .group_by(TaggedObject.tag_id, Tag.name)
- .order_by(func.count().desc())
- .all()
- )
- tags = [{"id": id, "name": name} for id, name in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["GET"])
- def get( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """List all tags a given object has."""
- if object_id == 0:
- return json_success(json.dumps([]))
-
- query = db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- )
- )
- tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["POST"])
- def post(self, object_type: ObjectTypes, object_id: int) -> FlaskResponse:
- """
- ---
- post:
- description: >-
- Add new tags to an object..
- requestBody:
- array of tag names
- """
- data = dict()
- data["tags"] = request.get_json(force=True)
- try:
- CreateCustomTagCommand(object_type, object_id, data).run()
- return Response(status=201)
- except TagInvalidError as ex:
- logger.error(
- "Invalid tag: %s",
- str(ex),
- )
- return Response(response="tag invalid", status=422)
- except TagCreateFailedError as ex:
- logger.error(
- "Error creating model %s: %s",
- self.__class__.__name__,
- str(ex),
- exc_info=True,
- )
- return Response(response="Error creating tag", status=422)
-
- @has_access_api
- @expose("/tags///", methods=["DELETE"])
- def delete( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """Remove tags from an object."""
- tag_names = request.get_json(force=True)
- if not tag_names:
- return Response(status=403)
-
- db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- ),
- TaggedObject.tag.has(Tag.name.in_(tag_names)),
- ).delete(synchronize_session=False)
- db.session.commit()
-
- return Response(status=204) # 204 NO CONTENT
-
- @has_access_api
- @expose("/tagged_objects/", methods=["GET", "POST"])
- def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
- tags = [
- process_template(tag)
- for tag in request.args.get("tags", "").split(",")
- if tag
- ]
-
- # filter types
- types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
-
- results: List[Dict[str, Any]] = []
-
- # dashboards
- if not types or "dashboard" in types:
- dashboards = (
- db.session.query(Dashboard)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(not tags or Tag.name.in_(tags))
- )
-
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.dashboard.name,
- "name": obj.dashboard_title,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in dashboards
- )
-
- # charts
- if not types or "chart" in types:
- charts = (
- db.session.query(Slice)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(not tags or Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.chart.name,
- "name": obj.slice_name,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in charts
- )
-
- # saved queries
- if not types or "query" in types:
- saved_queries = (
- db.session.query(SavedQuery)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == SavedQuery.id,
- TaggedObject.object_type == ObjectTypes.query,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(not tags or Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.query.name,
- "name": obj.label,
- "url": obj.url(),
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in saved_queries
- )
-
- return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
diff --git a/superset/views/tags.py b/superset/views/tags.py
index bbb338b0932a9..44027823cbce6 100644
--- a/superset/views/tags.py
+++ b/superset/views/tags.py
@@ -17,28 +17,19 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
-from typing import Any, Dict, List
import simplejson as json
-from flask import request, Response
from flask_appbuilder import expose
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from jinja2.sandbox import SandboxedEnvironment
-from sqlalchemy import and_, func
from werkzeug.exceptions import NotFound
from superset import db, is_feature_enabled, utils
-from superset.connectors.sqla.models import SqlaTable
from superset.jinja_context import ExtraCache
-from superset.models.dashboard import Dashboard
-from superset.models.slice import Slice
-from superset.models.sql_lab import SavedQuery
from superset.superset_typing import FlaskResponse
-from superset.tags.commands.create import CreateCustomTagCommand
-from superset.tags.commands.exceptions import TagCreateFailedError, TagInvalidError
-from superset.tags.models import ObjectTypes, Tag, TaggedObject
+from superset.tags.models import Tag
from superset.views.base import SupersetModelView
from .base import BaseSupersetView, json_success
@@ -83,13 +74,7 @@ def ensure_enabled(self) -> None:
@has_access_api
@expose("/tags/", methods=["GET"])
def tags(self) -> FlaskResponse: # pylint: disable=no-self-use
- query = (
- db.session.query(Tag)
- # .with_entities(Tag.name, Tag.id)
- # .group_by(TaggedObject.tag_id, Tag.name)
- # .order_by(func.count().desc())
- .all()
- )
+ query = db.session.query(Tag).all()
results = [
{
"id": obj.id,
@@ -102,231 +87,3 @@ def tags(self) -> FlaskResponse: # pylint: disable=no-self-use
for obj in query
]
return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
-
- @has_access_api
- @expose("/tags/suggestions/", methods=["GET"])
- def suggestions(self) -> FlaskResponse: # pylint: disable=no-self-use
- query = (
- db.session.query(TaggedObject)
- .join(Tag)
- .with_entities(TaggedObject.tag_id, Tag.name)
- .group_by(TaggedObject.tag_id, Tag.name)
- .order_by(func.count().desc())
- .all()
- )
- tags = [{"id": id, "name": name} for id, name in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["GET"])
- def get( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """List all tags a given object has."""
- if object_id == 0:
- return json_success(json.dumps([]))
-
- query = db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- )
- )
- tags = [{"id": obj.tag.id, "name": obj.tag.name} for obj in query]
- return json_success(json.dumps(tags))
-
- @has_access_api
- @expose("/tags///", methods=["POST"])
- def post(self, object_type: ObjectTypes, object_id: int) -> FlaskResponse:
- """
- ---
- post:
- description: >-
- Add new tags to an object..
- requestBody:
- array of tag names
- """
- data = dict()
- data["tags"] = request.get_json(force=True)
- try:
- CreateCustomTagCommand(object_type, object_id, data).run()
- return Response(status=201)
- except TagInvalidError as ex:
- logger.error(
- "Invalid tag: %s",
- str(ex),
- )
- return Response(response="tag invalid", status=422)
- except TagCreateFailedError as ex:
- logger.error(
- "Error creating model %s: %s",
- self.__class__.__name__,
- str(ex),
- exc_info=True,
- )
- return Response(response="Error creating model", status=422)
-
- @has_access_api
- @expose("/tags///", methods=["DELETE"])
- def delete_tagged_objects( # pylint: disable=no-self-use
- self, object_type: ObjectTypes, object_id: int
- ) -> FlaskResponse:
- """Remove tags from an object."""
- tag_names = request.get_json(force=True)
- if not tag_names:
- return Response(status=403)
-
- db.session.query(TaggedObject).filter(
- and_(
- TaggedObject.object_type == object_type,
- TaggedObject.object_id == object_id,
- ),
- TaggedObject.tag.has(Tag.name.in_(tag_names)),
- ).delete(synchronize_session=False)
- db.session.commit()
-
- return Response(status=204) # 204 NO CONTENT
-
- @has_access_api
- @expose("/tags", methods=["DELETE"])
- def delete_tags(self) -> FlaskResponse: # pylint: disable=no-self-use
- """Remove tags, and all tagged objects with that tag"""
- tag_names = request.get_json(force=True)
- if not tag_names:
- return Response(status=403)
- db.session()
- db.session.query(TaggedObject).filter(
- TaggedObject.tag.has(Tag.name.in_(tag_names)),
- ).delete(synchronize_session=False)
- db.session.query(Tag).filter(
- Tag.name.in_(tag_names),
- ).delete(synchronize_session=False)
- db.session.commit()
-
- return Response(status=204) # 204 NO CONTENT
-
- @has_access_api
- @expose("/tagged_objects/", methods=["GET", "POST"])
- def tagged_objects(self) -> FlaskResponse: # pylint: disable=no-self-use
- tags = [
- process_template(tag)
- for tag in request.args.get("tags", "").split(",")
- if tag
- ]
- if not tags:
- return json_success(json.dumps([]))
-
- # filter types
- types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
-
- results: List[Dict[str, Any]] = []
-
- # dashboards
- if not types or "dashboard" in types:
- dashboards = (
- db.session.query(Dashboard)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.dashboard.name,
- "name": obj.dashboard_title,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in dashboards
- )
-
- # charts
- if not types or "chart" in types:
- charts = (
- db.session.query(Slice)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.chart.name,
- "name": obj.slice_name,
- "url": obj.url,
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in charts
- )
-
- # saved queries
- if not types or "query" in types:
- saved_queries = (
- db.session.query(SavedQuery)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == SavedQuery.id,
- TaggedObject.object_type == ObjectTypes.query,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.query.name,
- "name": obj.label,
- "url": obj.url(),
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in saved_queries
- )
-
- # datasets
- if not types or "dataset" in types:
- datasets = (
- db.session.query(SqlaTable)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == SqlaTable.id,
- TaggedObject.object_type == ObjectTypes.dataset,
- ),
- )
- .join(Tag, TaggedObject.tag_id == Tag.id)
- .filter(Tag.name.in_(tags))
- )
- results.extend(
- {
- "id": obj.id,
- "type": ObjectTypes.dataset.name,
- "name": obj.table_name,
- "url": obj.sql_url(),
- "changed_on": obj.changed_on,
- "created_by": obj.created_by_fk,
- "creator": obj.creator(),
- }
- for obj in datasets
- )
-
- return json_success(json.dumps(results, default=utils.core.json_int_dttm_ser))
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 2afce53ef2f1c..088346cdc4f85 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -24,14 +24,21 @@
import pytest
import prison
from sqlalchemy.sql import func
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from superset.models.sql_lab import SavedQuery
import tests.integration_tests.test_app
from superset import db, security_manager
from superset.common.db_query_status import QueryStatus
from superset.models.core import Database
from superset.utils.database import get_example_database, get_main_database
-from superset.tags.models import Tag, TagTypes
-
+from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
+from tests.integration_tests.fixtures.world_bank_dashboard import (
+ load_world_bank_dashboard_with_slices,
+ load_world_bank_data,
+)
+from tests.integration_tests.fixtures.tags import with_tagging_system_feature
from tests.integration_tests.base_tests import SupersetTestCase
TAGS_FIXTURE_COUNT = 10
@@ -63,6 +70,20 @@ def insert_tag(
db.session.commit()
return tag
+ def insert_tagged_object(
+ self,
+ tag_id: int,
+ object_id: int,
+ object_type: ObjectTypes,
+ ) -> TaggedObject:
+ tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
+ tagged_object = TaggedObject(
+ tag=tag, object_id=object_id, object_type=object_type.name
+ )
+ db.session.add(tagged_object)
+ db.session.commit()
+ return tagged_object
+
@pytest.fixture()
def create_tags(self):
with self.create_app().app_context():
@@ -77,7 +98,6 @@ def create_tags(self):
tag_type="custom",
)
)
-
yield tags
# rollback changes
@@ -139,3 +159,192 @@ def test_get_list_tag(self):
assert data["count"] == TAGS_FIXTURE_COUNT
# check expected columns
assert data["list_columns"] == TAGS_LIST_COLUMNS
+
+ # test add tagged objects
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ def test_add_tagged_objects(self):
+ self.login(username="admin")
+ dashboard_id = 1
+ dashboard_type = ObjectTypes.dashboard.value
+ uri = f"api/v1/tag/{dashboard_type}/{dashboard_id}/"
+ example_tag_names = ["example_tag_1", "example_tag_2"]
+ data = {"tags": example_tag_names}
+ rv = self.client.post(uri, json=data, follow_redirects=True)
+ # successful request
+ self.assertEqual(rv.status_code, 201)
+ # check that tags were created in database
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
+ self.assertEqual(tags.count(), 2)
+ # check that tagged objects were created
+ tag_ids = [tags[0].id, tags[1].id]
+ tagged_objects = db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id.in_(tag_ids)
+ )
+ self.assertEqual(tagged_objects.count(), 2)
+ self.assertEqual(tagged_objects.first().object_id, 1)
+ self.assertEqual(tagged_objects.first().object_type, ObjectTypes.dashboard)
+ self.assertEqual(tagged_objects[1].object_id, 1)
+ self.assertEqual(tagged_objects[1].object_type, ObjectTypes.dashboard)
+ # clean up tags and tagged objects
+ tagged_objects.delete()
+ tags.delete()
+ db.session.commit()
+
+ # test delete tagged object
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("create_tags")
+ def test_delete_tagged_objects(self):
+ self.login(username="admin")
+ dashboard_id = 1
+ dashboard_type = ObjectTypes.dashboard
+ tag_names = ["example_tag_1", "example_tag_2"]
+ tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
+ assert tags.count() == 2
+ self.insert_tagged_object(
+ tag_id=tags.first().id, object_id=dashboard_id, object_type=dashboard_type
+ )
+ self.insert_tagged_object(
+ tag_id=tags[1].id, object_id=dashboard_id, object_type=dashboard_type
+ )
+ tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tags.first().id,
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ )
+ .first()
+ )
+ other_tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tags[1].id,
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ )
+ .first()
+ )
+ assert tagged_object is not None
+ uri = f"api/v1/tag/{dashboard_type.value}/{dashboard_id}/"
+ data = {"tag": tags.first().name}
+ rv = self.client.delete(uri, json=data, follow_redirects=True)
+ # successful request
+ self.assertEqual(rv.status_code, 200)
+ # ensure that tagged object no longer exists
+ tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tags.first().id,
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ )
+ .first()
+ )
+ assert not tagged_object
+ # ensure the other tagged objects still exist
+ other_tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ TaggedObject.tag_id == tags[1].id,
+ )
+ .first()
+ )
+ assert other_tagged_object is not None
+ # clean up tagged object
+ db.session.delete(other_tagged_object)
+
+ # test get objects
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("create_tags")
+ def test_get_objects_by_tag(self):
+ self.login(username="admin")
+ dashboard_id = 1
+ dashboard_type = ObjectTypes.dashboard
+ tag_names = ["example_tag_1", "example_tag_2"]
+ tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
+ for tag in tags:
+ self.insert_tagged_object(
+ tag_id=tag.id, object_id=dashboard_id, object_type=dashboard_type
+ )
+ tagged_objects = db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id.in_([tag.id for tag in tags]),
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ )
+ self.assertEqual(tagged_objects.count(), 2)
+ uri = f'api/v1/tag/get_objects/?tags={",".join(tag_names)}'
+ rv = self.client.get(uri)
+ # successful request
+ self.assertEqual(rv.status_code, 200)
+ fetched_objects = rv.json["result"]
+ self.assertEqual(len(fetched_objects), 1)
+ self.assertEqual(fetched_objects[0]["id"], 1)
+ # clean up tagged object
+ tagged_objects.delete()
+
+ # test get all objects
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("create_tags")
+ def test_get_all_objects(self):
+ self.login(username="admin")
+ dashboard_id = 1
+ dashboard_type = ObjectTypes.dashboard
+ tag_names = ["example_tag_1", "example_tag_2"]
+ tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
+ for tag in tags:
+ self.insert_tagged_object(
+ tag_id=tag.id, object_id=dashboard_id, object_type=dashboard_type
+ )
+ num_objects = (
+ db.session.query(Dashboard).count()
+ + db.session.query(Slice).count()
+ + db.session.query(SavedQuery).count()
+ )
+
+ tagged_objects = db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id.in_([tag.id for tag in tags]),
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == dashboard_type.name,
+ )
+ self.assertEqual(tagged_objects.count(), 2)
+ uri = "api/v1/tag/get_objects/"
+ rv = self.client.get(uri)
+ # successful request
+ self.assertEqual(rv.status_code, 200)
+ fetched_objects = rv.json["result"]
+ # check that all tagged objects are fetched
+ # when tagging system is false, there will only be the one dashboard
+ self.assertEqual(len(fetched_objects), 1)
+ self.assertEqual(fetched_objects[0]["id"], 1)
+ # clean up tagged object
+ tagged_objects.delete()
+
+ # test delete tags
+ @pytest.mark.usefixtures("create_tags")
+ def test_delete_tags(self):
+ self.login(username="admin")
+ # check that tags exist in the database
+ example_tag_names = ["example_tag_1", "example_tag_2", "example_tag_3"]
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
+ self.assertEqual(tags.count(), 3)
+ # delete the first tag
+ uri = "api/v1/tag/"
+ data = example_tag_names[:1]
+ rv = self.client.delete(uri, json=data, follow_redirects=True)
+ # successful request
+ self.assertEqual(rv.status_code, 200)
+ # check that tag does not exist in the database
+ tag = db.session.query(Tag).filter(Tag.name == example_tag_names[0]).first()
+ assert tag is None
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
+ self.assertEqual(tags.count(), 2)
+ # delete multiple tags
+ data = example_tag_names[1:]
+ rv = self.client.delete(uri, json=data, follow_redirects=True)
+ # successful request
+ self.assertEqual(rv.status_code, 200)
+ # check that tags are all gone
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
+ self.assertEqual(tags.count(), 0)
diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py
new file mode 100644
index 0000000000000..76c4f75694b86
--- /dev/null
+++ b/tests/integration_tests/tags/commands_tests.py
@@ -0,0 +1,158 @@
+# 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 itertools
+import json
+from unittest.mock import MagicMock, patch
+
+import pytest
+import yaml
+from superset.tags.commands.create import CreateCustomTagCommand
+from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
+from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
+from werkzeug.utils import secure_filename
+
+from superset import db, security_manager
+from superset.commands.exceptions import CommandInvalidError
+from superset.commands.importers.exceptions import IncorrectVersionError
+from superset.connectors.sqla.models import SqlaTable
+from superset.dashboards.commands.exceptions import DashboardNotFoundError
+from superset.dashboards.commands.export import (
+ append_charts,
+ ExportDashboardsCommand,
+ get_default_position,
+)
+from superset.dashboards.commands.importers import v0, v1
+from superset.models.core import Database
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from tests.integration_tests.base_tests import SupersetTestCase
+from tests.integration_tests.fixtures.importexport import (
+ chart_config,
+ dashboard_config,
+ dashboard_export,
+ dashboard_metadata_config,
+ database_config,
+ dataset_config,
+ dataset_metadata_config,
+)
+from tests.integration_tests.fixtures.world_bank_dashboard import (
+ load_world_bank_dashboard_with_slices,
+ load_world_bank_data,
+)
+from tests.integration_tests.fixtures.tags import with_tagging_system_feature
+
+# test create command
+class TestCreateCustomTagCommand(SupersetTestCase):
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ def test_create_custom_tag_command(self):
+ example_dashboard = (
+ db.session.query(Dashboard).filter_by(slug="world_health").one()
+ )
+ example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ command = CreateCustomTagCommand(
+ ObjectTypes.dashboard.value,
+ example_dashboard.id,
+ example_tags
+ )
+ command.run()
+
+ created_tags = db.session.query(Tag).join(TaggedObject).filter(
+ TaggedObject.object_id == example_dashboard.id,
+ Tag.type == TagTypes.custom
+ ).all()
+ assert example_tags == [tag.name for tag in created_tags]
+
+ # cleanup
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
+ db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id.in_([tag.id for tag in tags])
+ ).delete()
+ tags.delete()
+ db.session.commit()
+
+# test delete tags command
+class TestDeleteTagsCommand(SupersetTestCase):
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ def test_delete_tags_command(self):
+ example_dashboard = (
+ db.session.query(Dashboard).filter_by(slug="world_health").one()
+ )
+ example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ command = CreateCustomTagCommand(
+ ObjectTypes.dashboard.value,
+ example_dashboard.id,
+ example_tags
+ )
+ command.run()
+
+ created_tags = db.session.query(Tag).join(TaggedObject).filter(
+ TaggedObject.object_id == example_dashboard.id,
+ Tag.type == TagTypes.custom
+ ).all()
+ assert example_tags == [tag.name for tag in created_tags]
+
+ command = DeleteTagsCommand(example_tags)
+ command.run()
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
+ assert tags.count() == 0
+
+# test delete tagged objects command
+class TestDeleteTaggedObjectCommand(SupersetTestCase):
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ def test_delete_tags_command(self):
+ # create tagged objects
+ example_dashboard = (
+ db.session.query(Dashboard).filter_by(slug="world_health").one()
+ )
+ example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ command = CreateCustomTagCommand(
+ ObjectTypes.dashboard.value,
+ example_dashboard.id,
+ example_tags
+ )
+ command.run()
+
+ tagged_objects = db.session.query(TaggedObject).join(Tag).filter(
+ TaggedObject.object_id == example_dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ Tag.name.in_(example_tags)
+ )
+ assert tagged_objects.count() == 2
+ # delete one of the tagged objects
+ command = DeleteTaggedObjectCommand(
+ object_type=ObjectTypes.dashboard.value,
+ object_id=example_dashboard.id,
+ tag=example_tags[0]
+ )
+ command.run()
+ tagged_objects = db.session.query(TaggedObject).join(Tag).filter(
+ TaggedObject.object_id == example_dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ Tag.name.in_(example_tags)
+ )
+ assert tagged_objects.count() == 1
+
+ # cleanup
+ tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
+ db.session.query(TaggedObject).filter(
+ TaggedObject.tag_id.in_([tag.id for tag in tags])
+ ).delete()
+ tags.delete()
+ db.session.commit()
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
index 2a3a13ae0d73a..0ad2c927d176c 100644
--- a/tests/integration_tests/tags/dao_tests.py
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -20,10 +20,13 @@
import time
from unittest.mock import patch
import pytest
-from superset.dao.exceptions import DAOCreateFailedError
+from superset.dao.exceptions import DAOCreateFailedError, DAOException
+from superset.models.slice import Slice
+from superset.models.sql_lab import SavedQuery
from superset.tags.dao import TagDAO
from superset.tags.exceptions import InvalidTagNameError
-from superset.tags.models import ObjectTypes, Tag
+from superset.tags.models import ObjectTypes, Tag, TaggedObject
+from tests.integration_tests.tags.api_tests import TAGS_FIXTURE_COUNT
import tests.integration_tests.test_app # pylint: disable=unused-import
from superset import db, security_manager
@@ -38,29 +41,218 @@
class TestTagsDAO(SupersetTestCase):
+ def insert_tag(
+ self,
+ name: str,
+ tag_type: str,
+ ) -> Tag:
+ tag_name = name.strip()
+ tag = Tag(
+ name=tag_name,
+ type=tag_type,
+ )
+ db.session.add(tag)
+ db.session.commit()
+ return tag
+
+ def insert_tagged_object(
+ self,
+ tag_id: int,
+ object_id: int,
+ object_type: ObjectTypes,
+ ) -> TaggedObject:
+ tag = db.session.query(Tag).filter(Tag.id == tag_id).first()
+ tagged_object = TaggedObject(
+ tag=tag, object_id=object_id, object_type=object_type.name
+ )
+ db.session.add(tagged_object)
+ db.session.commit()
+ return tagged_object
+
+ @pytest.fixture()
+ def create_tags(self):
+ with self.create_app().app_context():
+ # clear tags table
+ tags = db.session.query(Tag).delete()
+ db.session.commit()
+ tags = []
+ for cx in range(TAGS_FIXTURE_COUNT):
+ tags.append(
+ self.insert_tag(
+ name=f"example_tag_{cx}",
+ tag_type="custom",
+ )
+ )
+ yield tags
+ db.session.commit()
+
+ @pytest.fixture()
+ def create_tagged_objects(self):
+ with self.create_app().app_context():
+ # clear tags table
+ tags = db.session.query(Tag).delete()
+ tags = []
+ for cx in range(TAGS_FIXTURE_COUNT):
+ tags.append(
+ self.insert_tag(
+ name=f"example_tag_{cx}",
+ tag_type="custom",
+ )
+ )
+ # clear tagged objects table
+ tagged_objects = db.session.query(TaggedObject).delete()
+ tagged_objects = []
+ for tag in tags:
+ tagged_objects.append(
+ self.insert_tagged_object(
+ object_id=1, object_type=ObjectTypes.dashboard, tag_id=tag.id
+ )
+ )
+ yield tagged_objects
+ db.session.commit()
+
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@pytest.mark.usefixtures("with_tagging_system_feature")
- def test_create_tag(self):
+ # test create tag
+ def test_create_tagged_objects(self):
# test that a tag cannot be added if it has ':' in it
- try:
+ with pytest.raises(DAOCreateFailedError):
TagDAO.create_custom_tagged_objects(
- object_type=ObjectTypes.dashboard,
+ object_type=ObjectTypes.dashboard.name,
object_id=1,
- properties={"tags": ["invalid:example tag 1"]},
+ tag_names=["invalid:example tag 1"],
)
- except Exception as e:
- assert type(e) is DAOCreateFailedError
# test that a tag can be added if it has a valid name
- try:
- TagDAO.create_custom_tagged_objects(
- object_type=ObjectTypes.dashboard,
- object_id=1,
- properties={"tags": ["example tag 1"]},
- )
- except Exception as e:
- # should not be an exception
- assert e is None
-
+ TagDAO.create_custom_tagged_objects(
+ object_type=ObjectTypes.dashboard.name,
+ object_id=1,
+ tag_names=["example tag 1"],
+ )
# check if tag exists
assert db.session.query(Tag).filter(Tag.name == "example tag 1").first()
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tagged_objects")
+ # test get objects from tag
+ def test_get_objects_from_tag(self):
+ # get objects
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(
+ ["example_tag_1", "example_tag_2"]
+ )
+ assert len(tagged_objects) == 1
+
+ # test get objects from tag with type
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(
+ ["example_tag_1", "example_tag_2"], obj_types=["dashboard", "chart"]
+ )
+ assert len(tagged_objects) == 1
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(
+ ["example_tag_1", "example_tag_2"], obj_types=["chart"]
+ )
+ assert len(tagged_objects) == 0
+ # test get all objects
+ num_charts = db.session.query(Slice).count()
+ num_objects = (
+ db.session.query(Dashboard).count()
+ + num_charts
+ + db.session.query(SavedQuery).count()
+ )
+ tagged_objects = TagDAO.get_tagged_objects_for_tags()
+ assert len(tagged_objects) == num_objects
+ # test get all objects by type
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(
+ obj_types=["dashboard", "chart", "saved_queries"]
+ )
+ assert len(tagged_objects) == num_objects
+ # test objects are retrieved by type
+ tagged_objects = TagDAO.get_tagged_objects_for_tags(obj_types=["chart"])
+ assert len(tagged_objects) == num_charts
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tagged_objects")
+ def test_find_tagged_object(self):
+ tag = db.session.query(Tag).filter(Tag.name == "example_tag_1").first()
+ tagged_object = TagDAO.find_tagged_object(
+ object_id=1, object_type=ObjectTypes.dashboard.name, tag_id=tag.id
+ )
+ assert tagged_object is not None
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tagged_objects")
+ def test_find_by_name(self):
+ # test tag can be found
+ tag = TagDAO.find_by_name("example_tag_1")
+ assert tag is not None
+ # tag that doesnt exist
+ tag = TagDAO.find_by_name("invalid_tag_1")
+ assert tag is None
+ # tag was not created
+ assert db.session.query(Tag).filter(Tag.name == "invalid_tag_1").first() is None
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tagged_objects")
+ def test_get_by_name(self):
+ # test tag can be found
+ tag = TagDAO.get_by_name("example_tag_1")
+ assert tag is not None
+ # tag that doesnt exist is added
+ tag = TagDAO.get_by_name("invalid_tag_1")
+ assert tag is not None
+ # tag was created
+ tag = db.session.query(Tag).filter(Tag.name == "invalid_tag_1").first()
+ assert tag is not None
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tags")
+ def test_delete_tags(self):
+ tag_names = ["example_tag_1", "example_tag_2"]
+ for tag_name in tag_names:
+ tag = db.session.query(Tag).filter(Tag.name == tag_name).first()
+ assert tag is not None
+
+ TagDAO.delete_tags(tag_names)
+
+ for tag_name in tag_names:
+ tag = db.session.query(Tag).filter(Tag.name == tag_name).first()
+ assert tag is None
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ @pytest.mark.usefixtures("create_tagged_objects")
+ def test_delete_tagged_object(self):
+ tag = db.session.query(Tag).filter(Tag.name == "example_tag_1").first()
+ tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tag.id,
+ TaggedObject.object_id == 1,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ )
+ .first()
+ )
+ assert tagged_object is not None
+ TagDAO.delete_tagged_object(
+ object_type=ObjectTypes.dashboard.name, object_id=1, tag_name=tag.name
+ )
+ tagged_object = (
+ db.session.query(TaggedObject)
+ .filter(
+ TaggedObject.tag_id == tag.id,
+ TaggedObject.object_id == 1,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ )
+ .first()
+ )
+ assert tagged_object is None
+
+ @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("with_tagging_system_feature")
+ def test_validate_tag_name(self):
+ assert TagDAO.validate_tag_name("example_tag_name") is True
+ assert TagDAO.validate_tag_name("invalid:tag_name") is False
From a8974e7c39499825c642717934fef073bc81349b Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Thu, 2 Feb 2023 13:01:18 -0500
Subject: [PATCH 66/76] fixed lint and apispec errors
---
superset-frontend/src/tags.ts | 25 ++--
.../views/CRUD/allentities/AllEntities.tsx | 16 +--
superset/initialization/__init__.py | 2 +-
superset/tags/api.py | 84 ++++++++------
superset/tags/commands/delete.py | 41 +++----
superset/tags/commands/utils.py | 3 +-
superset/tags/schemas.py | 2 +
tests/integration_tests/tags/api_tests.py | 22 ++--
.../integration_tests/tags/commands_tests.py | 109 ++++++++++--------
9 files changed, 165 insertions(+), 139 deletions(-)
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index d13be2f0ffa90..718cd4551c7f2 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -17,6 +17,7 @@
* under the License.
*/
import { JsonObject, SupersetClient } from '@superset-ui/core';
+import rison from 'rison';
import Tag from 'src/types/TagType';
export const OBJECT_TYPES_VALUES = Object.freeze([
@@ -80,7 +81,7 @@ export function fetchTags(
})
.then(({ json }) =>
callback(
- json['result']['tags'].filter(
+ json.result.tags.filter(
(tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes,
),
),
@@ -116,10 +117,9 @@ export function deleteTaggedObjects(
throw new Error(msg);
}
SupersetClient.delete({
- endpoint: `/api/v1/tag/${map_object_type_to_id(objectType)}/${objectId}/`,
- body: JSON.stringify({ tag: tag.name }),
- parseMethod: 'json',
- headers: { 'Content-Type': 'application/json' },
+ endpoint: `/api/v1/tag/${map_object_type_to_id(objectType)}/${objectId}/${
+ tag.name
+ }`,
})
.then(({ json }) =>
json
@@ -137,12 +137,9 @@ export function deleteTags(
callback: (text: string) => void,
error: (response: string) => void,
) {
- const tags_str = JSON.stringify(tags.map(tag => tag.name) as string[]);
+ const tag_names = tags.map(tag => tag.name) as string[];
SupersetClient.delete({
- endpoint: `/api/v1/tag/`,
- body: tags_str,
- parseMethod: 'json',
- headers: { 'Content-Type': 'application/json' },
+ endpoint: `/api/v1/tag/?q=${rison.encode(tag_names)}`,
})
.then(({ json }) =>
json.message
@@ -178,7 +175,11 @@ export function addTag(
const objectTypeId = map_object_type_to_id(objectType);
SupersetClient.post({
endpoint: `/api/v1/tag/${objectTypeId}/${objectId}/`,
- body: JSON.stringify({ tags: [tag] }),
+ body: JSON.stringify({
+ properties: {
+ tags: [tag],
+ },
+ }),
parseMethod: 'json',
headers: { 'Content-Type': 'application/json' },
})
@@ -196,6 +197,6 @@ export function fetchObjects(
url += `&types=${types}`;
}
SupersetClient.get({ endpoint: url })
- .then(({ json }) => callback(json['result']))
+ .then(({ json }) => callback(json.result))
.catch(response => error(response));
}
diff --git a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
index 6c85c676eb9dc..995c12734cf41 100644
--- a/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
+++ b/superset-frontend/src/views/CRUD/allentities/AllEntities.tsx
@@ -20,11 +20,11 @@ import React, { useMemo } from 'react';
import { ensureIsArray, styled, t } from '@superset-ui/core';
import { StringParam, useQueryParam } from 'use-query-params';
import withToasts from 'src/components/MessageToasts/withToasts';
-import AllEntitiesTable from './AllEntitiesTable';
-import { AsyncSelect } from 'src/components';
+import AsyncSelect from 'src/components/Select/AsyncSelect';
import { SelectValue } from 'antd/lib/select';
import { loadTags } from 'src/components/Tags/utils';
import { getValue } from 'src/components/Select/utils';
+import AllEntitiesTable from './AllEntitiesTable';
const AllEntitiesContainer = styled.div`
${({ theme }) => `
@@ -62,11 +62,13 @@ function AllEntities() {
setTagsQuery(tagSearch);
};
- const tagsValue = useMemo(() => {
- return tagsQuery
- ? tagsQuery.split(',').map(tag => ({ value: tag, label: tag }))
- : [];
- }, [tagsQuery]);
+ const tagsValue = useMemo(
+ () =>
+ tagsQuery
+ ? tagsQuery.split(',').map(tag => ({ value: tag, label: tag }))
+ : [],
+ [tagsQuery],
+ );
return (
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 2583add85b26f..cda0651456b9f 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -150,8 +150,8 @@ def init_views(self) -> None:
from superset.reports.api import ReportScheduleRestApi
from superset.reports.logs.api import ReportExecutionLogRestApi
from superset.security.api import SecurityRestApi
- from superset.tags.api import TagRestApi
from superset.sqllab.api import SqlLabRestApi
+ from superset.tags.api import TagRestApi
from superset.views.access_requests import AccessRequestsModelView
from superset.views.alerts import AlertView, ReportView
from superset.views.all_entities import TaggedObjectsModelView, TaggedObjectView
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 4036fac2811e1..828c08ad21226 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -15,9 +15,10 @@
# specific language governing permissions and limitations
# under the License.
import logging
+from typing import Any
from flask import request, Response
-from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.api import expose, protect, rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
@@ -35,6 +36,7 @@
from superset.tags.dao import TagDAO
from superset.tags.models import ObjectTypes, Tag
from superset.tags.schemas import (
+ delete_tags_schema,
openapi_spec_methods_override,
TaggedObjectEntityResponseSchema,
TagGetResponseSchema,
@@ -107,7 +109,13 @@ class TagRestApi(BaseSupersetModelRestApi):
openapi_spec_tag = "Tags"
""" Override the name set for this collection of endpoints """
- openapi_spec_component_schemas = (TagGetResponseSchema,)
+ openapi_spec_component_schemas = (
+ TagGetResponseSchema,
+ TaggedObjectEntityResponseSchema,
+ )
+ apispec_parameter_schemas = {
+ "delete_tags_schema": delete_tags_schema,
+ }
openapi_spec_methods = openapi_spec_methods_override
""" Overrides GET methods OpenApi descriptions """
@@ -128,7 +136,7 @@ def __repr__(self) -> str:
log_to_statsd=False,
)
def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
- """Adds tags to an object.
+ """Adds tags to an object. Creates new tags if they do not already exist
---
post:
description: >-
@@ -139,7 +147,13 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
content:
application/json:
schema:
- $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
+ type: object
+ properties:
+ tags:
+ description: list of tag names to add to object
+ type: array
+ items:
+ type: string
parameters:
- in: path
schema:
@@ -152,15 +166,6 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
responses:
201:
description: Tag added
- content:
- application/json:
- schema:
- type: object
- properties:
- id:
- type: number
- result:
- $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
302:
description: Redirects to the current digest
400:
@@ -173,13 +178,14 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
$ref: '#/components/responses/500'
"""
try:
- tags = request.json["tags"]
+ tags = request.json["properties"]["tags"]
# This validates custom Schema with custom validations
CreateCustomTagCommand(object_type, object_id, tags).run()
return self.response(201)
except KeyError:
return self.response(
- 400, message="Missing required 'tags' field in request body"
+ 400,
+ message="Missing required 'tags' field under 'properties' in request body",
)
except TagInvalidError:
return self.response(422, message="Invalid tag")
@@ -192,7 +198,7 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
)
return self.response_422(message=str(ex))
- @expose("///", methods=["DELETE"])
+ @expose("////", methods=["DELETE"])
@protect()
@safe
@statsd_metrics
@@ -201,21 +207,18 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
log_to_statsd=True,
)
def delete_tagged_object(
- self, object_type: ObjectTypes, object_id: int
+ self, object_type: ObjectTypes, object_id: int, tag: str
) -> Response:
"""Deletes a Tagged Object
---
delete:
description: >-
Deletes a Tagged Object.
- requestBody:
- description: Tag name
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/{{self.__class__.__name__}}.delete'
parameters:
+ - in: path
+ schema:
+ type: string
+ name: tag
- in: path
schema:
type: integer
@@ -246,7 +249,6 @@ def delete_tagged_object(
$ref: '#/components/responses/500'
"""
try:
- tag = request.json["tag"]
DeleteTaggedObjectCommand(object_type, object_id, tag).run()
return self.response(200, message="OK")
except TagInvalidError:
@@ -268,25 +270,35 @@ def delete_tagged_object(
@protect()
@safe
@statsd_metrics
+ @rison(delete_tags_schema)
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
log_to_statsd=False,
)
- def bulk_delete(self) -> Response:
+ def bulk_delete(self, **kwargs: Any) -> Response:
"""Delete Tags
---
delete:
description: >-
- Deletes multiple Tags.
- requestBody:
- description: List of Tag names
- required: true
- name: tags
+ Deletes multiple Tags. This will remove all tagged objects with this tag
+ parameters:
+ - in: query
+ name: q
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/delete_tags_schema'
+
responses:
200:
description: Deletes multiple Tags
content:
application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
401:
$ref: '#/components/responses/401'
403:
@@ -298,7 +310,7 @@ def bulk_delete(self) -> Response:
500:
$ref: '#/components/responses/500'
"""
- tags = request.json
+ tags = kwargs["rison"]
try:
DeleteTagsCommand(tags).run()
return self.response(200, message=f"Deleted {len(tags)} tags")
@@ -330,16 +342,16 @@ def get_objects(self) -> Response:
name: tag_id
responses:
200:
- description: Objects fetched
+ description: List of tagged objects associated with a Tag
content:
application/json:
schema:
type: object
properties:
- id:
- type: number
result:
- $ref: '#/components/schemas/{{self.__class__.__name__}}.post'
+ type: array
+ items:
+ $ref: '#/components/schemas/TaggedObjectEntityResponseSchema'
302:
description: Redirects to the current digest
400:
diff --git a/superset/tags/commands/delete.py b/superset/tags/commands/delete.py
index 2113c576f6bec..63a514e5996d7 100644
--- a/superset/tags/commands/delete.py
+++ b/superset/tags/commands/delete.py
@@ -17,15 +17,14 @@
import logging
from typing import List
-
from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.tags.commands.exceptions import (
TagDeleteFailedError,
+ TaggedObjectDeleteFailedError,
+ TaggedObjectNotFoundError,
TagInvalidError,
TagNotFoundError,
- TaggedObjectDeleteFailedError,
- TaggedObjectNotFoundError
)
from superset.tags.commands.utils import to_object_type
from superset.tags.dao import TagDAO
@@ -47,10 +46,9 @@ def run(self) -> None:
object_type = to_object_type(self._object_type)
if object_type is None:
raise TaggedObjectDeleteFailedError(
- f'invalid object type {self._object_type}')
- TagDAO.delete_tagged_object(
- object_type, self._object_id, self._tag
- )
+ f"invalid object type {self._object_type}"
+ )
+ TagDAO.delete_tagged_object(object_type, self._object_id, self._tag)
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise TaggedObjectDeleteFailedError() from ex
@@ -63,27 +61,30 @@ def validate(self) -> None:
# Validate tagged object exists
tag = TagDAO.find_by_name(self._tag)
if not tag:
- exceptions.append(TaggedObjectDeleteFailedError(
- f'could not find tag: {self._tag}'
- ))
+ exceptions.append(
+ TaggedObjectDeleteFailedError(f"could not find tag: {self._tag}")
+ )
else:
# Validate object type
object_type = to_object_type(self._object_type)
if object_type is None:
- exceptions.append(TaggedObjectDeleteFailedError(
- f'invalid object type {self._object_type}'))
+ exceptions.append(
+ TaggedObjectDeleteFailedError(
+ f"invalid object type {self._object_type}"
+ )
+ )
else:
tagged_object = TagDAO.find_tagged_object(
- object_type=object_type,
- object_id=self._object_id,
- tag_id=tag.id
+ object_type=object_type, object_id=self._object_id, tag_id=tag.id
)
if tagged_object is None:
- exceptions.append(TaggedObjectNotFoundError(
- object_id=self._object_id,
- object_type=object_type.name,
- tag_name=self._tag
- ))
+ exceptions.append(
+ TaggedObjectNotFoundError(
+ object_id=self._object_id,
+ object_type=object_type.name,
+ tag_name=self._tag,
+ )
+ )
if exceptions:
exception = TagInvalidError()
exception.add_list(exceptions)
diff --git a/superset/tags/commands/utils.py b/superset/tags/commands/utils.py
index 89035503a391a..2993365b7ac75 100644
--- a/superset/tags/commands/utils.py
+++ b/superset/tags/commands/utils.py
@@ -16,6 +16,7 @@
# under the License.
from typing import Optional, Union
+
from superset.tags.models import ObjectTypes
@@ -23,6 +24,6 @@ def to_object_type(object_type: Union[ObjectTypes, int, str]) -> Optional[Object
if isinstance(object_type, ObjectTypes):
return object_type
for type_ in ObjectTypes:
- if (object_type in [type_.value, type_.name]):
+ if object_type in [type_.value, type_.name]:
return type_
return None
diff --git a/superset/tags/schemas.py b/superset/tags/schemas.py
index 0496eef916afb..2081adf69fa80 100644
--- a/superset/tags/schemas.py
+++ b/superset/tags/schemas.py
@@ -18,6 +18,8 @@
from superset.dashboards.schemas import UserSchema
+delete_tags_schema = {"type": "array", "items": {"type": "string"}}
+
object_type_description = "A title for the tag."
openapi_spec_methods_override = {
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 088346cdc4f85..30f7382af1148 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -168,7 +168,7 @@ def test_add_tagged_objects(self):
dashboard_type = ObjectTypes.dashboard.value
uri = f"api/v1/tag/{dashboard_type}/{dashboard_id}/"
example_tag_names = ["example_tag_1", "example_tag_2"]
- data = {"tags": example_tag_names}
+ data = {"properties": {"tags": example_tag_names}}
rv = self.client.post(uri, json=data, follow_redirects=True)
# successful request
self.assertEqual(rv.status_code, 201)
@@ -225,9 +225,8 @@ def test_delete_tagged_objects(self):
.first()
)
assert tagged_object is not None
- uri = f"api/v1/tag/{dashboard_type.value}/{dashboard_id}/"
- data = {"tag": tags.first().name}
- rv = self.client.delete(uri, json=data, follow_redirects=True)
+ uri = f"api/v1/tag/{dashboard_type.value}/{dashboard_id}/{tags.first().name}"
+ rv = self.client.delete(uri, follow_redirects=True)
# successful request
self.assertEqual(rv.status_code, 200)
# ensure that tagged object no longer exists
@@ -297,12 +296,6 @@ def test_get_all_objects(self):
self.insert_tagged_object(
tag_id=tag.id, object_id=dashboard_id, object_type=dashboard_type
)
- num_objects = (
- db.session.query(Dashboard).count()
- + db.session.query(Slice).count()
- + db.session.query(SavedQuery).count()
- )
-
tagged_objects = db.session.query(TaggedObject).filter(
TaggedObject.tag_id.in_([tag.id for tag in tags]),
TaggedObject.object_id == dashboard_id,
@@ -330,9 +323,8 @@ def test_delete_tags(self):
tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
self.assertEqual(tags.count(), 3)
# delete the first tag
- uri = "api/v1/tag/"
- data = example_tag_names[:1]
- rv = self.client.delete(uri, json=data, follow_redirects=True)
+ uri = f"api/v1/tag/?q={prison.dumps(example_tag_names[:1])}"
+ rv = self.client.delete(uri, follow_redirects=True)
# successful request
self.assertEqual(rv.status_code, 200)
# check that tag does not exist in the database
@@ -341,8 +333,8 @@ def test_delete_tags(self):
tags = db.session.query(Tag).filter(Tag.name.in_(example_tag_names))
self.assertEqual(tags.count(), 2)
# delete multiple tags
- data = example_tag_names[1:]
- rv = self.client.delete(uri, json=data, follow_redirects=True)
+ uri = f"api/v1/tag/?q={prison.dumps(example_tag_names[1:])}"
+ rv = self.client.delete(uri, follow_redirects=True)
# successful request
self.assertEqual(rv.status_code, 200)
# check that tags are all gone
diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py
index 76c4f75694b86..43fe4836e0d80 100644
--- a/tests/integration_tests/tags/commands_tests.py
+++ b/tests/integration_tests/tags/commands_tests.py
@@ -20,9 +20,6 @@
import pytest
import yaml
-from superset.tags.commands.create import CreateCustomTagCommand
-from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
-from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
from werkzeug.utils import secure_filename
from superset import db, security_manager
@@ -39,6 +36,9 @@
from superset.models.core import Database
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
+from superset.tags.commands.create import CreateCustomTagCommand
+from superset.tags.commands.delete import DeleteTaggedObjectCommand, DeleteTagsCommand
+from superset.tags.models import ObjectTypes, Tag, TaggedObject, TagTypes
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.fixtures.importexport import (
chart_config,
@@ -49,11 +49,12 @@
dataset_config,
dataset_metadata_config,
)
+from tests.integration_tests.fixtures.tags import with_tagging_system_feature
from tests.integration_tests.fixtures.world_bank_dashboard import (
load_world_bank_dashboard_with_slices,
load_world_bank_data,
)
-from tests.integration_tests.fixtures.tags import with_tagging_system_feature
+
# test create command
class TestCreateCustomTagCommand(SupersetTestCase):
@@ -63,28 +64,32 @@ def test_create_custom_tag_command(self):
example_dashboard = (
db.session.query(Dashboard).filter_by(slug="world_health").one()
)
- example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value,
- example_dashboard.id,
- example_tags
- )
+ ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ )
command.run()
- created_tags = db.session.query(Tag).join(TaggedObject).filter(
- TaggedObject.object_id == example_dashboard.id,
- Tag.type == TagTypes.custom
- ).all()
+ created_tags = (
+ db.session.query(Tag)
+ .join(TaggedObject)
+ .filter(
+ TaggedObject.object_id == example_dashboard.id,
+ Tag.type == TagTypes.custom,
+ )
+ .all()
+ )
assert example_tags == [tag.name for tag in created_tags]
-
+
# cleanup
tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
db.session.query(TaggedObject).filter(
- TaggedObject.tag_id.in_([tag.id for tag in tags])
- ).delete()
+ TaggedObject.tag_id.in_([tag.id for tag in tags])
+ ).delete()
tags.delete()
db.session.commit()
+
# test delete tags command
class TestDeleteTagsCommand(SupersetTestCase):
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@@ -93,25 +98,29 @@ def test_delete_tags_command(self):
example_dashboard = (
db.session.query(Dashboard).filter_by(slug="world_health").one()
)
- example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value,
- example_dashboard.id,
- example_tags
- )
+ ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ )
command.run()
- created_tags = db.session.query(Tag).join(TaggedObject).filter(
- TaggedObject.object_id == example_dashboard.id,
- Tag.type == TagTypes.custom
- ).all()
+ created_tags = (
+ db.session.query(Tag)
+ .join(TaggedObject)
+ .filter(
+ TaggedObject.object_id == example_dashboard.id,
+ Tag.type == TagTypes.custom,
+ )
+ .all()
+ )
assert example_tags == [tag.name for tag in created_tags]
-
+
command = DeleteTagsCommand(example_tags)
command.run()
tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
assert tags.count() == 0
+
# test delete tagged objects command
class TestDeleteTaggedObjectCommand(SupersetTestCase):
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@@ -121,38 +130,44 @@ def test_delete_tags_command(self):
example_dashboard = (
db.session.query(Dashboard).filter_by(slug="world_health").one()
)
- example_tags = ['create custom tag example 1', 'create custom tag example 2']
+ example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
- ObjectTypes.dashboard.value,
- example_dashboard.id,
- example_tags
- )
+ ObjectTypes.dashboard.value, example_dashboard.id, example_tags
+ )
command.run()
- tagged_objects = db.session.query(TaggedObject).join(Tag).filter(
- TaggedObject.object_id == example_dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
- Tag.name.in_(example_tags)
- )
+ tagged_objects = (
+ db.session.query(TaggedObject)
+ .join(Tag)
+ .filter(
+ TaggedObject.object_id == example_dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ Tag.name.in_(example_tags),
+ )
+ )
assert tagged_objects.count() == 2
# delete one of the tagged objects
command = DeleteTaggedObjectCommand(
- object_type=ObjectTypes.dashboard.value,
- object_id=example_dashboard.id,
- tag=example_tags[0]
- )
+ object_type=ObjectTypes.dashboard.value,
+ object_id=example_dashboard.id,
+ tag=example_tags[0],
+ )
command.run()
- tagged_objects = db.session.query(TaggedObject).join(Tag).filter(
- TaggedObject.object_id == example_dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard.name,
- Tag.name.in_(example_tags)
- )
+ tagged_objects = (
+ db.session.query(TaggedObject)
+ .join(Tag)
+ .filter(
+ TaggedObject.object_id == example_dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard.name,
+ Tag.name.in_(example_tags),
+ )
+ )
assert tagged_objects.count() == 1
# cleanup
tags = db.session.query(Tag).filter(Tag.name.in_(example_tags))
db.session.query(TaggedObject).filter(
- TaggedObject.tag_id.in_([tag.id for tag in tags])
- ).delete()
+ TaggedObject.tag_id.in_([tag.id for tag in tags])
+ ).delete()
tags.delete()
db.session.commit()
From ddaa260ff880f2851adb257cb5aff124da3a2e70 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Thu, 2 Feb 2023 14:40:40 -0500
Subject: [PATCH 67/76] fixed lint errors and old tagging tests
---
superset-frontend/src/tags.ts | 15 ---------------
superset/tags/api.py | 17 +++++++++++------
tests/integration_tests/tagging_tests.py | 12 ------------
3 files changed, 11 insertions(+), 33 deletions(-)
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index 718cd4551c7f2..2cb6e27412697 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -88,21 +88,6 @@ export function fetchTags(
)
.catch(response => error(response));
}
-
-export function fetchSuggestions(
- { includeTypes = false },
- callback: (json: JsonObject) => void,
- error: (response: Response) => void,
-) {
- SupersetClient.get({ endpoint: '/taggedobjectview/tags/suggestions/' })
- .then(({ json }) =>
- callback(
- json.filter((tag: Tag) => tag.name.indexOf(':') === -1 || includeTypes),
- ),
- )
- .catch(response => error(response));
-}
-
export function deleteTaggedObjects(
{ objectType, objectId }: { objectType: string; objectId: number },
tag: Tag,
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 828c08ad21226..178e0e3765143 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -132,7 +132,8 @@ def __repr__(self) -> str:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
+ action=lambda self, *
+ args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
log_to_statsd=False,
)
def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
@@ -185,7 +186,7 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
except KeyError:
return self.response(
400,
- message="Missing required 'tags' field under 'properties' in request body",
+ message="Missing required field 'tags' in 'properties'",
)
except TagInvalidError:
return self.response(422, message="Invalid tag")
@@ -203,7 +204,8 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
+ action=lambda self, *
+ args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
log_to_statsd=True,
)
def delete_tagged_object(
@@ -272,7 +274,8 @@ def delete_tagged_object(
@statsd_metrics
@rison(delete_tags_schema)
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
+ action=lambda self, *
+ args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
log_to_statsd=False,
)
def bulk_delete(self, **kwargs: Any) -> Response:
@@ -326,7 +329,8 @@ def bulk_delete(self, **kwargs: Any) -> Response:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_objects",
+ action=lambda self, *
+ args, **kwargs: f"{self.__class__.__name__}.get_objects",
log_to_statsd=False,
)
def get_objects(self) -> Response:
@@ -365,7 +369,8 @@ def get_objects(self) -> Response:
"""
tags = [tag for tag in request.args.get("tags", "").split(",") if tag]
# filter types
- types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
+ types = [type_ for type_ in request.args.get(
+ "types", "").split(",") if type_]
try:
tagged_objects = TagDAO.get_tagged_objects_for_tags(tags, types)
diff --git a/tests/integration_tests/tagging_tests.py b/tests/integration_tests/tagging_tests.py
index 4ee10041d2c53..71fb7e4e4e89d 100644
--- a/tests/integration_tests/tagging_tests.py
+++ b/tests/integration_tests/tagging_tests.py
@@ -42,18 +42,6 @@ def clear_tagged_object_table(self):
db.session.query(TaggedObject).delete()
db.session.commit()
- @with_feature_flags(TAGGING_SYSTEM=False)
- def test_tag_view_disabled(self):
- self.login("admin")
- response = self.client.get("/tagview/tags/suggestions/")
- self.assertEqual(404, response.status_code)
-
- @with_feature_flags(TAGGING_SYSTEM=True)
- def test_tag_view_enabled(self):
- self.login("admin")
- response = self.client.get("/tagview/tags/suggestions/")
- self.assertNotEqual(404, response.status_code)
-
@pytest.mark.usefixtures("with_tagging_system_feature")
def test_dataset_tagging(self):
"""
From 8e157e30b40f62a3359b657a5b89ded84e1c3174 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Thu, 2 Feb 2023 15:01:40 -0500
Subject: [PATCH 68/76] fixed precommit error
---
superset/tags/api.py | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 178e0e3765143..5fa63af5f622b 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -132,8 +132,7 @@ def __repr__(self) -> str:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *
- args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
log_to_statsd=False,
)
def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
@@ -204,8 +203,7 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *
- args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
log_to_statsd=True,
)
def delete_tagged_object(
@@ -274,8 +272,7 @@ def delete_tagged_object(
@statsd_metrics
@rison(delete_tags_schema)
@event_logger.log_this_with_context(
- action=lambda self, *
- args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.bulk_delete",
log_to_statsd=False,
)
def bulk_delete(self, **kwargs: Any) -> Response:
@@ -329,8 +326,7 @@ def bulk_delete(self, **kwargs: Any) -> Response:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *
- args, **kwargs: f"{self.__class__.__name__}.get_objects",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_objects",
log_to_statsd=False,
)
def get_objects(self) -> Response:
@@ -369,8 +365,7 @@ def get_objects(self) -> Response:
"""
tags = [tag for tag in request.args.get("tags", "").split(",") if tag]
# filter types
- types = [type_ for type_ in request.args.get(
- "types", "").split(",") if type_]
+ types = [type_ for type_ in request.args.get("types", "").split(",") if type_]
try:
tagged_objects = TagDAO.get_tagged_objects_for_tags(tags, types)
From cfa1c6c47d8ee516eac4305fbac9871cdb7e18d4 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Thu, 2 Feb 2023 15:18:39 -0500
Subject: [PATCH 69/76] fixed pylint error
---
superset/tags/api.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 5fa63af5f622b..44b729ea38d40 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -132,7 +132,7 @@ def __repr__(self) -> str:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_tagged_objects",
+ action=lambda self, *args, **kwargs: Any,
log_to_statsd=False,
)
def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
@@ -203,7 +203,7 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete_tagged_object",
+ action=lambda self, *args, **kwargs: Any,
log_to_statsd=True,
)
def delete_tagged_object(
From 2c22fba9e77c599cf74ab254f0e39ccaee47acaf Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Fri, 3 Feb 2023 09:01:30 -0500
Subject: [PATCH 70/76] fixed pre-commit errors
---
superset/constants.py | 4 ++--
superset/tags/api.py | 12 ++++++------
tests/integration_tests/tags/api_tests.py | 1 +
3 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/superset/constants.py b/superset/constants.py
index d781276340e68..07bbe6c07cea5 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -145,8 +145,8 @@ class RouteMethod: # pylint: disable=too-few-public-methods
"stop_query": "read",
"get_objects": "read",
"get_all_objects": "read",
- "add_tagged_objects": "write",
- "delete_tagged_object": "write",
+ "add_objects": "write",
+ "delete_object": "write",
}
EXTRA_FORM_DATA_APPEND_KEYS = {
diff --git a/superset/tags/api.py b/superset/tags/api.py
index 44b729ea38d40..d7c57c0320705 100644
--- a/superset/tags/api.py
+++ b/superset/tags/api.py
@@ -59,8 +59,8 @@ class TagRestApi(BaseSupersetModelRestApi):
"bulk_delete",
"get_objects",
"get_all_objects",
- "add_tagged_objects",
- "delete_tagged_object",
+ "add_objects",
+ "delete_object",
}
resource_name = "tag"
@@ -132,10 +132,10 @@ def __repr__(self) -> str:
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: Any,
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.add_objects",
log_to_statsd=False,
)
- def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
+ def add_objects(self, object_type: ObjectTypes, object_id: int) -> Response:
"""Adds tags to an object. Creates new tags if they do not already exist
---
post:
@@ -203,10 +203,10 @@ def add_tagged_objects(self, object_type: ObjectTypes, object_id: int) -> Respon
@safe
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self, *args, **kwargs: Any,
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete_object",
log_to_statsd=True,
)
- def delete_tagged_object(
+ def delete_object(
self, object_type: ObjectTypes, object_id: int, tag: str
) -> Response:
"""Deletes a Tagged Object
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 30f7382af1148..468025062e623 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -98,6 +98,7 @@ def create_tags(self):
tag_type="custom",
)
)
+ db.session.commit()
yield tags
# rollback changes
From 27d6b6a7c0b6db7dc4667d683155cc34f76d20dd Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Fri, 3 Feb 2023 09:01:56 -0500
Subject: [PATCH 71/76] removed unnecessary commit
---
tests/integration_tests/tags/api_tests.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 468025062e623..30f7382af1148 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -98,7 +98,6 @@ def create_tags(self):
tag_type="custom",
)
)
- db.session.commit()
yield tags
# rollback changes
From f19ee774ff76861621619875b3c36d454fe5f54b Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Mon, 6 Feb 2023 09:19:04 -0500
Subject: [PATCH 72/76] fixed api tests and dao tests
---
superset/tags/dao.py | 6 ++---
tests/integration_tests/tags/api_tests.py | 18 ++++++++++----
tests/integration_tests/tags/dao_tests.py | 29 ++++++++++++++++-------
3 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index 3d68098a6a1c7..297cd4ea8d864 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -169,7 +169,7 @@ def get_tagged_objects_for_tags(
results: List[Dict[str, Any]] = []
# dashboards
- if not obj_types or "dashboard" in obj_types:
+ if (not obj_types) or ("dashboard" in obj_types):
dashboards = (
db.session.query(Dashboard)
.join(
@@ -197,7 +197,7 @@ def get_tagged_objects_for_tags(
)
# charts
- if not obj_types or "chart" in obj_types:
+ if (not obj_types) or ("chart" in obj_types):
charts = (
db.session.query(Slice)
.join(
@@ -224,7 +224,7 @@ def get_tagged_objects_for_tags(
)
# saved queries
- if not obj_types or "query" in obj_types:
+ if (not obj_types) or ("query" in obj_types):
saved_queries = (
db.session.query(SavedQuery)
.join(
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 30f7382af1148..5a27aa8a967ce 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -34,6 +34,10 @@
from superset.models.core import Database
from superset.utils.database import get_example_database, get_main_database
from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+ load_birth_names_dashboard_with_slices,
+ load_birth_names_data
+)
from tests.integration_tests.fixtures.world_bank_dashboard import (
load_world_bank_dashboard_with_slices,
load_world_bank_data,
@@ -98,7 +102,7 @@ def create_tags(self):
tag_type="custom",
)
)
- yield tags
+ yield
# rollback changes
for tag in tags:
@@ -162,6 +166,7 @@ def test_get_list_tag(self):
# test add tagged objects
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_add_tagged_objects(self):
self.login(username="admin")
dashboard_id = 1
@@ -192,6 +197,7 @@ def test_add_tagged_objects(self):
# test delete tagged object
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@pytest.mark.usefixtures("create_tags")
def test_delete_tagged_objects(self):
self.login(username="admin")
@@ -256,6 +262,7 @@ def test_delete_tagged_objects(self):
# test get objects
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@pytest.mark.usefixtures("create_tags")
def test_get_objects_by_tag(self):
self.login(username="admin")
@@ -285,9 +292,11 @@ def test_get_objects_by_tag(self):
# test get all objects
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@pytest.mark.usefixtures("create_tags")
def test_get_all_objects(self):
self.login(username="admin")
+ # tag the dashboard with id 1
dashboard_id = 1
dashboard_type = ObjectTypes.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
@@ -302,15 +311,14 @@ def test_get_all_objects(self):
TaggedObject.object_type == dashboard_type.name,
)
self.assertEqual(tagged_objects.count(), 2)
+ self.assertEqual(tagged_objects.first().object_id, dashboard_id)
uri = "api/v1/tag/get_objects/"
rv = self.client.get(uri)
# successful request
self.assertEqual(rv.status_code, 200)
fetched_objects = rv.json["result"]
- # check that all tagged objects are fetched
- # when tagging system is false, there will only be the one dashboard
- self.assertEqual(len(fetched_objects), 1)
- self.assertEqual(fetched_objects[0]["id"], 1)
+ # check that the dashboard object was fetched
+ assert dashboard_id in [obj['id'] for obj in fetched_objects]
# clean up tagged object
tagged_objects.delete()
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
index 0ad2c927d176c..165cbd1f9077e 100644
--- a/tests/integration_tests/tags/dao_tests.py
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -17,6 +17,7 @@
# isort:skip_file
import copy
import json
+from operator import and_
import time
from unittest.mock import patch
import pytest
@@ -153,19 +154,29 @@ def test_get_objects_from_tag(self):
)
assert len(tagged_objects) == 0
# test get all objects
- num_charts = db.session.query(Slice).count()
- num_objects = (
- db.session.query(Dashboard).count()
+ num_charts = db.session.query(Slice).join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Slice.id,
+ TaggedObject.object_type == ObjectTypes.chart,
+ ),
+ ).distinct(Slice.id).count()
+ num_charts_and_dashboards = (
+ db.session.query(Dashboard)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
+ ),
+ ).distinct(Slice.id).count()
+ num_charts
- + db.session.query(SavedQuery).count()
)
- tagged_objects = TagDAO.get_tagged_objects_for_tags()
- assert len(tagged_objects) == num_objects
- # test get all objects by type
+ # gets all tagged objects of type dashboard and chart
tagged_objects = TagDAO.get_tagged_objects_for_tags(
- obj_types=["dashboard", "chart", "saved_queries"]
+ obj_types=["dashboard", "chart"]
)
- assert len(tagged_objects) == num_objects
+ assert len(tagged_objects) == num_charts_and_dashboards
# test objects are retrieved by type
tagged_objects = TagDAO.get_tagged_objects_for_tags(obj_types=["chart"])
assert len(tagged_objects) == num_charts
From 14e2159c2d2876c150192b3a176d30adae5f2b11 Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Mon, 6 Feb 2023 09:52:34 -0500
Subject: [PATCH 73/76] fixed precommit error
---
tests/integration_tests/tags/api_tests.py | 4 +--
tests/integration_tests/tags/dao_tests.py | 35 ++++++++++++++---------
2 files changed, 23 insertions(+), 16 deletions(-)
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 5a27aa8a967ce..4a1e04b5e602e 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -36,7 +36,7 @@
from superset.tags.models import ObjectTypes, Tag, TagTypes, TaggedObject
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices,
- load_birth_names_data
+ load_birth_names_data,
)
from tests.integration_tests.fixtures.world_bank_dashboard import (
load_world_bank_dashboard_with_slices,
@@ -318,7 +318,7 @@ def test_get_all_objects(self):
self.assertEqual(rv.status_code, 200)
fetched_objects = rv.json["result"]
# check that the dashboard object was fetched
- assert dashboard_id in [obj['id'] for obj in fetched_objects]
+ assert dashboard_id in [obj["id"] for obj in fetched_objects]
# clean up tagged object
tagged_objects.delete()
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
index 165cbd1f9077e..054916f764f56 100644
--- a/tests/integration_tests/tags/dao_tests.py
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -154,22 +154,29 @@ def test_get_objects_from_tag(self):
)
assert len(tagged_objects) == 0
# test get all objects
- num_charts = db.session.query(Slice).join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Slice.id,
- TaggedObject.object_type == ObjectTypes.chart,
- ),
- ).distinct(Slice.id).count()
+ num_charts = (
+ db.session.query(Slice)
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Slice.id,
+ TaggedObject.object_type == ObjectTypes.chart,
+ ),
+ )
+ .distinct(Slice.id)
+ .count()
+ )
num_charts_and_dashboards = (
db.session.query(Dashboard)
- .join(
- TaggedObject,
- and_(
- TaggedObject.object_id == Dashboard.id,
- TaggedObject.object_type == ObjectTypes.dashboard,
- ),
- ).distinct(Slice.id).count()
+ .join(
+ TaggedObject,
+ and_(
+ TaggedObject.object_id == Dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
+ ),
+ )
+ .distinct(Slice.id)
+ .count()
+ num_charts
)
# gets all tagged objects of type dashboard and chart
From ecb02fc64ec3e8aaeea65069cefcbbfb06e6eb9e Mon Sep 17 00:00:00 2001
From: GITHUB_USERNAME
Date: Mon, 6 Feb 2023 14:37:50 -0500
Subject: [PATCH 74/76] fixed tagging tests for postrges and mysql
---
superset/tags/dao.py | 34 +++++++----
tests/integration_tests/tags/api_tests.py | 59 ++++++++++++++-----
.../integration_tests/tags/commands_tests.py | 4 +-
tests/integration_tests/tags/dao_tests.py | 35 +++++++++--
4 files changed, 96 insertions(+), 36 deletions(-)
diff --git a/superset/tags/dao.py b/superset/tags/dao.py
index 297cd4ea8d864..c676b4ab3c25c 100644
--- a/superset/tags/dao.py
+++ b/superset/tags/dao.py
@@ -18,6 +18,8 @@
from operator import and_
from typing import Any, Dict, List, Optional
+from sqlalchemy.exc import SQLAlchemyError
+
from superset.dao.base import BaseDAO
from superset.dao.exceptions import DAOCreateFailedError, DAODeleteFailedError
from superset.extensions import db
@@ -35,8 +37,10 @@ class TagDAO(BaseDAO):
@staticmethod
def validate_tag_name(tag_name: str) -> bool:
- if ":" in tag_name:
- return False
+ invalid_characters = [":", ","]
+ for invalid_character in invalid_characters:
+ if invalid_character in tag_name:
+ return False
return True
@staticmethod
@@ -47,7 +51,7 @@ def create_custom_tagged_objects(
for name in tag_names:
if not TagDAO.validate_tag_name(name):
raise DAOCreateFailedError(
- message="Invalid Tag Name (cannot contain ':')"
+ message="Invalid Tag Name (cannot contain ':' or ',')"
)
type_ = TagTypes.custom
tag_name = name.strip()
@@ -83,14 +87,12 @@ def delete_tagged_object(
object_type: {object_type} \
and tag name: "{tag_name}" could not be found'
)
- deleted = tagged_object.delete()
- if not deleted:
- raise DAODeleteFailedError(
- message=f'Tagged object with object_id: {object_id} \
- object_type: {object_type} \
- and tag name: "{tag_name}" could not be deleted'
- )
- db.session.commit()
+ try:
+ db.session.delete(tagged_object.one())
+ db.session.commit()
+ except SQLAlchemyError as ex: # pragma: no cover
+ db.session.rollback()
+ raise DAODeleteFailedError(exception=ex) from ex
@staticmethod
def delete_tags(tag_names: List[str]) -> None:
@@ -105,8 +107,14 @@ def delete_tags(tag_names: List[str]) -> None:
message=f"Tag with name {tag_name} does not exist."
)
tags_to_delete.append(tag_name)
- db.session.query(Tag).filter(Tag.name.in_(tags_to_delete)).delete()
- db.session.commit()
+ tag_objects = db.session.query(Tag).filter(Tag.name.in_(tags_to_delete))
+ for tag in tag_objects:
+ try:
+ db.session.delete(tag)
+ db.session.commit()
+ except SQLAlchemyError as ex: # pragma: no cover
+ db.session.rollback()
+ raise DAODeleteFailedError(exception=ex) from ex
@staticmethod
def get_by_name(name: str, type_: TagTypes = TagTypes.custom) -> Tag:
diff --git a/tests/integration_tests/tags/api_tests.py b/tests/integration_tests/tags/api_tests.py
index 4a1e04b5e602e..7bf21da4fcd71 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -92,8 +92,10 @@ def insert_tagged_object(
def create_tags(self):
with self.create_app().app_context():
# clear tags table
- tags = db.session.query(Tag).delete()
- db.session.commit()
+ tags = db.session.query(Tag)
+ for tag in tags:
+ db.session.delete(tag)
+ db.session.commit()
tags = []
for cx in range(TAGS_FIXTURE_COUNT):
tags.append(
@@ -107,7 +109,7 @@ def create_tags(self):
# rollback changes
for tag in tags:
db.session.delete(tag)
- db.session.commit()
+ db.session.commit()
def test_get_tag(self):
"""
@@ -169,7 +171,21 @@ def test_get_list_tag(self):
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_add_tagged_objects(self):
self.login(username="admin")
- dashboard_id = 1
+ # clean up tags and tagged objects
+ tags = db.session.query(Tag)
+ for tag in tags:
+ db.session.delete(tag)
+ db.session.commit()
+ tagged_objects = db.session.query(TaggedObject)
+ for tagged_object in tagged_objects:
+ db.session.delete(tagged_object)
+ db.session.commit()
+ dashboard = (
+ db.session.query(Dashboard)
+ .filter(Dashboard.dashboard_title == "World Bank's Data")
+ .first()
+ )
+ dashboard_id = dashboard.id
dashboard_type = ObjectTypes.dashboard.value
uri = f"api/v1/tag/{dashboard_type}/{dashboard_id}/"
example_tag_names = ["example_tag_1", "example_tag_2"]
@@ -183,17 +199,18 @@ def test_add_tagged_objects(self):
# check that tagged objects were created
tag_ids = [tags[0].id, tags[1].id]
tagged_objects = db.session.query(TaggedObject).filter(
- TaggedObject.tag_id.in_(tag_ids)
+ TaggedObject.tag_id.in_(tag_ids),
+ TaggedObject.object_id == dashboard_id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
)
- self.assertEqual(tagged_objects.count(), 2)
- self.assertEqual(tagged_objects.first().object_id, 1)
- self.assertEqual(tagged_objects.first().object_type, ObjectTypes.dashboard)
- self.assertEqual(tagged_objects[1].object_id, 1)
- self.assertEqual(tagged_objects[1].object_type, ObjectTypes.dashboard)
+ assert tagged_objects.count() == 2
# clean up tags and tagged objects
- tagged_objects.delete()
- tags.delete()
- db.session.commit()
+ for tagged_object in tagged_objects:
+ db.session.delete(tagged_object)
+ db.session.commit()
+ for tag in tags:
+ db.session.delete(tag)
+ db.session.commit()
# test delete tagged object
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@@ -266,7 +283,12 @@ def test_delete_tagged_objects(self):
@pytest.mark.usefixtures("create_tags")
def test_get_objects_by_tag(self):
self.login(username="admin")
- dashboard_id = 1
+ dashboard = (
+ db.session.query(Dashboard)
+ .filter(Dashboard.dashboard_title == "World Bank's Data")
+ .first()
+ )
+ dashboard_id = dashboard.id
dashboard_type = ObjectTypes.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
@@ -286,7 +308,7 @@ def test_get_objects_by_tag(self):
self.assertEqual(rv.status_code, 200)
fetched_objects = rv.json["result"]
self.assertEqual(len(fetched_objects), 1)
- self.assertEqual(fetched_objects[0]["id"], 1)
+ self.assertEqual(fetched_objects[0]["id"], dashboard_id)
# clean up tagged object
tagged_objects.delete()
@@ -297,7 +319,12 @@ def test_get_objects_by_tag(self):
def test_get_all_objects(self):
self.login(username="admin")
# tag the dashboard with id 1
- dashboard_id = 1
+ dashboard = (
+ db.session.query(Dashboard)
+ .filter(Dashboard.dashboard_title == "World Bank's Data")
+ .first()
+ )
+ dashboard_id = dashboard.id
dashboard_type = ObjectTypes.dashboard
tag_names = ["example_tag_1", "example_tag_2"]
tags = db.session.query(Tag).filter(Tag.name.in_(tag_names))
diff --git a/tests/integration_tests/tags/commands_tests.py b/tests/integration_tests/tags/commands_tests.py
index 43fe4836e0d80..8f44d2ebda0dd 100644
--- a/tests/integration_tests/tags/commands_tests.py
+++ b/tests/integration_tests/tags/commands_tests.py
@@ -96,7 +96,9 @@ class TestDeleteTagsCommand(SupersetTestCase):
@pytest.mark.usefixtures("with_tagging_system_feature")
def test_delete_tags_command(self):
example_dashboard = (
- db.session.query(Dashboard).filter_by(slug="world_health").one()
+ db.session.query(Dashboard)
+ .filter_by(dashboard_title="World Bank's Data")
+ .one()
)
example_tags = ["create custom tag example 1", "create custom tag example 2"]
command = CreateCustomTagCommand(
diff --git a/tests/integration_tests/tags/dao_tests.py b/tests/integration_tests/tags/dao_tests.py
index 054916f764f56..0234b2c8c2dfd 100644
--- a/tests/integration_tests/tags/dao_tests.py
+++ b/tests/integration_tests/tags/dao_tests.py
@@ -74,7 +74,10 @@ def insert_tagged_object(
def create_tags(self):
with self.create_app().app_context():
# clear tags table
- tags = db.session.query(Tag).delete()
+ tags = db.session.query(Tag)
+ for tag in tags:
+ db.session.delete(tag)
+ db.session.commit()
db.session.commit()
tags = []
for cx in range(TAGS_FIXTURE_COUNT):
@@ -91,7 +94,10 @@ def create_tags(self):
def create_tagged_objects(self):
with self.create_app().app_context():
# clear tags table
- tags = db.session.query(Tag).delete()
+ tags = db.session.query(Tag)
+ for tag in tags:
+ db.session.delete(tag)
+ db.session.commit()
tags = []
for cx in range(TAGS_FIXTURE_COUNT):
tags.append(
@@ -101,12 +107,18 @@ def create_tagged_objects(self):
)
)
# clear tagged objects table
- tagged_objects = db.session.query(TaggedObject).delete()
+ tagged_objects = db.session.query(TaggedObject)
+ for tagged_obj in tagged_objects:
+ db.session.delete(tagged_obj)
+ db.session.commit()
tagged_objects = []
+ dashboard_id = 1
for tag in tags:
tagged_objects.append(
self.insert_tagged_object(
- object_id=1, object_type=ObjectTypes.dashboard, tag_id=tag.id
+ object_id=dashboard_id,
+ object_type=ObjectTypes.dashboard,
+ tag_id=tag.id,
)
)
yield tagged_objects
@@ -135,9 +147,20 @@ def test_create_tagged_objects(self):
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@pytest.mark.usefixtures("with_tagging_system_feature")
- @pytest.mark.usefixtures("create_tagged_objects")
+ @pytest.mark.usefixtures("create_tags")
# test get objects from tag
def test_get_objects_from_tag(self):
+ # create tagged objects
+ dashboard = (
+ db.session.query(Dashboard)
+ .filter(Dashboard.dashboard_title == "World Bank's Data")
+ .first()
+ )
+ dashboard_id = dashboard.id
+ tag = db.session.query(Tag).filter_by(name="example_tag_1").one()
+ self.insert_tagged_object(
+ object_id=dashboard_id, object_type=ObjectTypes.dashboard, tag_id=tag.id
+ )
# get objects
tagged_objects = TagDAO.get_tagged_objects_for_tags(
["example_tag_1", "example_tag_2"]
@@ -175,7 +198,7 @@ def test_get_objects_from_tag(self):
TaggedObject.object_type == ObjectTypes.dashboard,
),
)
- .distinct(Slice.id)
+ .distinct(Dashboard.id)
.count()
+ num_charts
)
From 876cf4da0f33f865bb2bcef5c50564b5237721a0 Mon Sep 17 00:00:00 2001
From: cccs-RyanK
Date: Mon, 13 Feb 2023 11:22:34 -0500
Subject: [PATCH 75/76] post merge fixes
---
superset-frontend/src/pages/ChartList/index.tsx | 5 -----
1 file changed, 5 deletions(-)
diff --git a/superset-frontend/src/pages/ChartList/index.tsx b/superset-frontend/src/pages/ChartList/index.tsx
index e7e9239afb371..e02d848a5dcfd 100644
--- a/superset-frontend/src/pages/ChartList/index.tsx
+++ b/superset-frontend/src/pages/ChartList/index.tsx
@@ -151,11 +151,6 @@ interface ChartListProps {
};
}
-type ChartLinkedDashboard = {
- id: number;
- dashboard_title: string;
-};
-
const StyledActions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
From c1080fcd62232541e3c0da205057dc8e5e7ac0fd Mon Sep 17 00:00:00 2001
From: cccs-RyanK
Date: Thu, 16 Feb 2023 07:52:06 -0500
Subject: [PATCH 76/76] small frontend changes
---
.../src/explore/components/PropertiesModal/index.tsx | 2 +-
superset-frontend/src/tags.ts | 3 +--
.../src/views/CRUD/dashboard/DashboardList.tsx | 8 +++++++-
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index cc15be0b6c224..9465fc87b9184 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -422,7 +422,7 @@ function PropertiesModal({
{isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && (
- {t('Tags')}
+ {t('Tags')}
)}
{isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && (
diff --git a/superset-frontend/src/tags.ts b/superset-frontend/src/tags.ts
index 2cb6e27412697..ff0b8f3a339d3 100644
--- a/superset-frontend/src/tags.ts
+++ b/superset-frontend/src/tags.ts
@@ -50,8 +50,7 @@ export function fetchAllTags(
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
- const url = `/api/v1/tag`;
- SupersetClient.get({ endpoint: url })
+ SupersetClient.get({ endpoint: `/api/v1/tag` })
.then(({ json }) => callback(json))
.catch(response => error(response));
}
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index fa90465345bd9..d26900a29d3ff 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -366,7 +366,13 @@ function DashboardList(props: DashboardListProps) {
row: {
original: { tags = [] },
},
- }: any) => (
+ }: {
+ row: {
+ original: {
+ tags: Tag[];
+ };
+ };
+ }) => (
// Only show custom type tags