You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2026/04/23 17:37:25 UTC

(superset) 08/25: Fix lint/tests

This is an automated email from the ASF dual-hosted git repository.

betodealmeida pushed a commit to branch backup/semantic-layer-rename-database-dataset-before-rebase-20260423-133321
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4490bffd500a4275408b042634f28e1790add233
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Mar 11 15:03:43 2026 -0400

    Fix lint/tests
---
 .../features/semanticLayers/jsonFormsHelpers.tsx   |  34 +-
 .../semanticViews/AddSemanticViewModal.tsx         |  98 +++---
 superset-frontend/src/pages/DatasetList/index.tsx  |  32 +-
 tests/unit_tests/semantic_layers/api_test.py       | 388 +++++++++++++++++++++
 4 files changed, 470 insertions(+), 82 deletions(-)

diff --git a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
index ced9992f427..bc5c5f54cca 100644
--- a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
+++ b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx
@@ -76,6 +76,23 @@ const constEntry = {
   renderer: ConstRenderer,
 };
 
+/**
+ * Checks whether all dependency values are filled (non-empty).
+ * Handles nested objects (like auth) by checking they have at least one key.
+ */
+export function areDependenciesSatisfied(
+  dependencies: string[],
+  data: Record<string, unknown>,
+): boolean {
+  return dependencies.every(dep => {
+    const value = data[dep];
+    if (value === null || value === undefined || value === '') return false;
+    if (typeof value === 'object' && Object.keys(value).length === 0)
+      return false;
+    return true;
+  });
+}
+
 /**
  * Renderer for fields marked `x-dynamic` in the JSON Schema.
  * Shows a loading spinner inside the input while the schema is being
@@ -218,23 +235,6 @@ export function getDynamicDependencies(
   return deps;
 }
 
-/**
- * Checks whether all dependency values are filled (non-empty).
- * Handles nested objects (like auth) by checking they have at least one key.
- */
-export function areDependenciesSatisfied(
-  dependencies: string[],
-  data: Record<string, unknown>,
-): boolean {
-  return dependencies.every(dep => {
-    const value = data[dep];
-    if (value === null || value === undefined || value === '') return false;
-    if (typeof value === 'object' && Object.keys(value).length === 0)
-      return false;
-    return true;
-  });
-}
-
 /**
  * Serializes the dependency values for a set of fields into a stable string
  * for comparison, so we only re-fetch when dependency values actually change.
diff --git a/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx b/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx
index 1658a4cac35..f7ee1b110c2 100644
--- a/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx
+++ b/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx
@@ -129,31 +129,6 @@ export default function AddSemanticViewModal({
   const [selectedViews, setSelectedViews] = useState<Set<string>>(new Set());
   const [loadingViews, setLoadingViews] = useState(false);
 
-  // Reset state when modal closes
-  useEffect(() => {
-    if (show) {
-      fetchLayers();
-    } else {
-      setStep('layer');
-      setLayers([]);
-      setSelectedLayerUuid(null);
-      setLoading(false);
-      setSaving(false);
-      setRuntimeSchema(null);
-      setRuntimeUiSchema(undefined);
-      setRuntimeData({});
-      setRefreshingSchema(false);
-      setHasRuntimeErrors(false);
-      errorsRef.current = [];
-      lastDepSnapshotRef.current = '';
-      dynamicDepsRef.current = {};
-      setAvailableViews([]);
-      setSelectedViews(new Set());
-      setLoadingViews(false);
-      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
-    }
-  }, [show]); // eslint-disable-line react-hooks/exhaustive-deps
-
   const fetchLayers = async () => {
     setLoading(true);
     try {
@@ -173,6 +148,30 @@ export default function AddSemanticViewModal({
     }
   };
 
+  const fetchViews = useCallback(
+    async (uuid: string, rData: Record<string, unknown>) => {
+      setLoadingViews(true);
+      setStep('select');
+      try {
+        const { json } = await SupersetClient.post({
+          endpoint: `/api/v1/semantic_layer/${uuid}/views`,
+          jsonPayload: { runtime_data: rData },
+        });
+        const views: AvailableView[] = json.result ?? [];
+        setAvailableViews(views);
+        // Pre-select views that are already added (disabled anyway)
+        setSelectedViews(
+          new Set(views.filter(v => v.already_added).map(v => v.name)),
+        );
+      } catch {
+        addDangerToast(t('An error occurred while fetching available views'));
+      } finally {
+        setLoadingViews(false);
+      }
+    },
+    [addDangerToast],
+  );
+
   const applyRuntimeSchema = useCallback((rawSchema: JsonSchema) => {
     const schema = sanitizeSchema(rawSchema);
     setRuntimeSchema(schema);
@@ -218,32 +217,33 @@ export default function AddSemanticViewModal({
         else setRefreshingSchema(false);
       }
     },
-    [addDangerToast, applyRuntimeSchema], // eslint-disable-line react-hooks/exhaustive-deps
+    [addDangerToast, applyRuntimeSchema, fetchViews],
   );
 
-  const fetchViews = useCallback(
-    async (uuid: string, rData: Record<string, unknown>) => {
-      setLoadingViews(true);
-      setStep('select');
-      try {
-        const { json } = await SupersetClient.post({
-          endpoint: `/api/v1/semantic_layer/${uuid}/views`,
-          jsonPayload: { runtime_data: rData },
-        });
-        const views: AvailableView[] = json.result ?? [];
-        setAvailableViews(views);
-        // Pre-select views that are already added (disabled anyway)
-        setSelectedViews(
-          new Set(views.filter(v => v.already_added).map(v => v.name)),
-        );
-      } catch {
-        addDangerToast(t('An error occurred while fetching available views'));
-      } finally {
-        setLoadingViews(false);
-      }
-    },
-    [addDangerToast],
-  );
+  // Reset state when modal closes
+  useEffect(() => {
+    if (show) {
+      fetchLayers();
+    } else {
+      setStep('layer');
+      setLayers([]);
+      setSelectedLayerUuid(null);
+      setLoading(false);
+      setSaving(false);
+      setRuntimeSchema(null);
+      setRuntimeUiSchema(undefined);
+      setRuntimeData({});
+      setRefreshingSchema(false);
+      setHasRuntimeErrors(false);
+      errorsRef.current = [];
+      lastDepSnapshotRef.current = '';
+      dynamicDepsRef.current = {};
+      setAvailableViews([]);
+      setSelectedViews(new Set());
+      setLoadingViews(false);
+      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
+    }
+  }, [show]); // eslint-disable-line react-hooks/exhaustive-deps
 
   const maybeRefreshRuntimeSchema = useCallback(
     (data: Record<string, unknown>) => {
diff --git a/superset-frontend/src/pages/DatasetList/index.tsx b/superset-frontend/src/pages/DatasetList/index.tsx
index 107e8e936fe..49b2bdc4676 100644
--- a/superset-frontend/src/pages/DatasetList/index.tsx
+++ b/superset-frontend/src/pages/DatasetList/index.tsx
@@ -407,6 +407,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
     [addDangerToast, setPreparingExport],
   );
 
+  const handleSemanticViewDelete = ({ id, table_name: tableName }: Dataset) => {
+    SupersetClient.delete({
+      endpoint: `/api/v1/semantic_view/${id}`,
+    }).then(
+      () => {
+        refreshData();
+        addSuccessToast(t('Deleted: %s', tableName));
+      },
+      createErrorHandler(errMsg =>
+        addDangerToast(
+          t('There was an issue deleting %s: %s', tableName, errMsg),
+        ),
+      ),
+    );
+  };
+
   const columns = useMemo(
     () => [
       {
@@ -989,22 +1005,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
     );
   };
 
-  const handleSemanticViewDelete = ({ id, table_name: tableName }: Dataset) => {
-    SupersetClient.delete({
-      endpoint: `/api/v1/semantic_view/${id}`,
-    }).then(
-      () => {
-        refreshData();
-        addSuccessToast(t('Deleted: %s', tableName));
-      },
-      createErrorHandler(errMsg =>
-        addDangerToast(
-          t('There was an issue deleting %s: %s', tableName, errMsg),
-        ),
-      ),
-    );
-  };
-
   const handleBulkDatasetDelete = (datasetsToDelete: Dataset[]) => {
     const datasets = datasetsToDelete.filter(
       d => d.source_type !== 'semantic_layer',
diff --git a/tests/unit_tests/semantic_layers/api_test.py b/tests/unit_tests/semantic_layers/api_test.py
index c6482c0f139..c65cc6a7780 100644
--- a/tests/unit_tests/semantic_layers/api_test.py
+++ b/tests/unit_tests/semantic_layers/api_test.py
@@ -28,6 +28,8 @@ from superset.commands.semantic_layer.exceptions import (
     SemanticLayerInvalidError,
     SemanticLayerNotFoundError,
     SemanticLayerUpdateFailedError,
+    SemanticViewCreateFailedError,
+    SemanticViewDeleteFailedError,
     SemanticViewForbiddenError,
     SemanticViewInvalidError,
     SemanticViewNotFoundError,
@@ -1488,3 +1490,389 @@ def test_connections_source_type_semantic_layer_only(
     assert result["source_type"] == "semantic_layer"
     # Only one query (SemanticLayer), no Database query
     mock_db_session.query.assert_called_once()
+
+
+# =============================================================================
+# SemanticViewRestApi.post (bulk create) tests
+# =============================================================================
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_bulk_create(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST / bulk creates semantic views."""
+    new_model = MagicMock()
+    new_model.uuid = uuid_lib.uuid4()
+    new_model.name = "View 1"
+
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.CreateSemanticViewCommand",
+    )
+    mock_command.return_value.run.return_value = new_model
+
+    payload = {
+        "views": [
+            {
+                "name": "View 1",
+                "semantic_layer_uuid": str(uuid_lib.uuid4()),
+                "configuration": {"database": "db1"},
+            },
+        ],
+    }
+    response = client.post("/api/v1/semantic_view/", json=payload)
+
+    assert response.status_code == 201
+    result = response.json["result"]
+    assert len(result["created"]) == 1
+    assert result["created"][0]["name"] == "View 1"
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_empty_views(
+    client: Any,
+    full_api_access: None,
+) -> None:
+    """Test POST / returns 400 when no views provided."""
+    response = client.post("/api/v1/semantic_view/", json={"views": []})
+
+    assert response.status_code == 400
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_validation_error(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST / collects validation errors for individual views."""
+    # Missing required field "semantic_layer_uuid"
+    payload = {
+        "views": [
+            {"name": "Bad View"},
+        ],
+    }
+    response = client.post("/api/v1/semantic_view/", json=payload)
+
+    assert response.status_code == 422
+    result = response.json["result"]
+    assert len(result["errors"]) == 1
+    assert result["errors"][0]["name"] == "Bad View"
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_layer_not_found(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST / collects layer-not-found errors."""
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.CreateSemanticViewCommand",
+    )
+    mock_command.return_value.run.side_effect = SemanticLayerNotFoundError()
+
+    payload = {
+        "views": [
+            {
+                "name": "View 1",
+                "semantic_layer_uuid": str(uuid_lib.uuid4()),
+                "configuration": {},
+            },
+        ],
+    }
+    response = client.post("/api/v1/semantic_view/", json=payload)
+
+    assert response.status_code == 422
+    result = response.json["result"]
+    assert len(result["errors"]) == 1
+    assert result["errors"][0]["error"] == "Semantic layer not found"
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_create_failed(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST / collects create-failed errors."""
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.CreateSemanticViewCommand",
+    )
+    mock_command.return_value.run.side_effect = SemanticViewCreateFailedError()
+
+    payload = {
+        "views": [
+            {
+                "name": "View 1",
+                "semantic_layer_uuid": str(uuid_lib.uuid4()),
+                "configuration": {},
+            },
+        ],
+    }
+    response = client.post("/api/v1/semantic_view/", json=payload)
+
+    assert response.status_code == 422
+    result = response.json["result"]
+    assert len(result["errors"]) == 1
+
+
+@SEMANTIC_LAYERS_APP
+def test_post_semantic_view_partial_success(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST / returns 201 with partial success (some created, some errors)."""
+    new_model = MagicMock()
+    new_model.uuid = uuid_lib.uuid4()
+    new_model.name = "Good View"
+
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.CreateSemanticViewCommand",
+    )
+    mock_command.return_value.run.side_effect = [
+        new_model,
+        SemanticLayerNotFoundError(),
+    ]
+
+    layer_uuid = str(uuid_lib.uuid4())
+    payload = {
+        "views": [
+            {
+                "name": "Good View",
+                "semantic_layer_uuid": layer_uuid,
+                "configuration": {},
+            },
+            {
+                "name": "Bad View",
+                "semantic_layer_uuid": layer_uuid,
+                "configuration": {},
+            },
+        ],
+    }
+    response = client.post("/api/v1/semantic_view/", json=payload)
+
+    assert response.status_code == 201
+    result = response.json["result"]
+    assert len(result["created"]) == 1
+    assert len(result["errors"]) == 1
+
+
+# =============================================================================
+# SemanticViewRestApi.delete tests
+# =============================================================================
+
+
+@SEMANTIC_LAYERS_APP
+def test_delete_semantic_view(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test DELETE /<pk> deletes a semantic view."""
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.DeleteSemanticViewCommand",
+    )
+    mock_command.return_value.run.return_value = None
+
+    response = client.delete("/api/v1/semantic_view/1")
+
+    assert response.status_code == 200
+    mock_command.assert_called_once_with("1")
+
+
+@SEMANTIC_LAYERS_APP
+def test_delete_semantic_view_not_found(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test DELETE /<pk> returns 404 when view not found."""
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.DeleteSemanticViewCommand",
+    )
+    mock_command.return_value.run.side_effect = SemanticViewNotFoundError()
+
+    response = client.delete("/api/v1/semantic_view/999")
+
+    assert response.status_code == 404
+
+
+@SEMANTIC_LAYERS_APP
+def test_delete_semantic_view_failed(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test DELETE /<pk> returns 422 when deletion fails."""
+    mock_command = mocker.patch(
+        "superset.semantic_layers.api.DeleteSemanticViewCommand",
+    )
+    mock_command.return_value.run.side_effect = SemanticViewDeleteFailedError()
+
+    response = client.delete("/api/v1/semantic_view/1")
+
+    assert response.status_code == 422
+
+
+# =============================================================================
+# SemanticLayerRestApi.views tests
+# =============================================================================
+
+
+@SEMANTIC_LAYERS_APP
+def test_get_views(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST /<uuid>/views returns available views."""
+    test_uuid = str(uuid_lib.uuid4())
+    mock_layer = MagicMock()
+    mock_layer.uuid = uuid_lib.uuid4()
+
+    mock_view1 = MagicMock()
+    mock_view1.name = "View A"
+    mock_view2 = MagicMock()
+    mock_view2.name = "View B"
+    mock_layer.implementation.get_semantic_views.return_value = [
+        mock_view1,
+        mock_view2,
+    ]
+
+    mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
+    mock_dao.find_by_uuid.return_value = mock_layer
+
+    mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
+    mock_sv_dao.find_by_semantic_layer.return_value = []
+
+    response = client.post(
+        f"/api/v1/semantic_layer/{test_uuid}/views",
+        json={"runtime_data": {"database": "mydb"}},
+    )
+
+    assert response.status_code == 200
+    result = response.json["result"]
+    assert len(result) == 2
+    assert result[0]["name"] == "View A"
+    assert result[0]["already_added"] is False
+    assert result[1]["name"] == "View B"
+
+
+@SEMANTIC_LAYERS_APP
+def test_get_views_with_existing(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST /<uuid>/views marks already-added views."""
+    test_uuid = str(uuid_lib.uuid4())
+    mock_layer = MagicMock()
+    mock_layer.uuid = uuid_lib.uuid4()
+
+    mock_view = MagicMock()
+    mock_view.name = "Existing View"
+    mock_layer.implementation.get_semantic_views.return_value = [mock_view]
+
+    mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
+    mock_dao.find_by_uuid.return_value = mock_layer
+
+    existing_view = MagicMock()
+    existing_view.name = "Existing View"
+    existing_view.configuration = '{"database": "mydb"}'
+
+    mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
+    mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
+
+    response = client.post(
+        f"/api/v1/semantic_layer/{test_uuid}/views",
+        json={"runtime_data": {"database": "mydb"}},
+    )
+
+    assert response.status_code == 200
+    result = response.json["result"]
+    assert len(result) == 1
+    assert result[0]["name"] == "Existing View"
+    assert result[0]["already_added"] is True
+
+
+@SEMANTIC_LAYERS_APP
+def test_get_views_not_found(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST /<uuid>/views returns 404 when layer not found."""
+    mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
+    mock_dao.find_by_uuid.return_value = None
+
+    response = client.post(
+        f"/api/v1/semantic_layer/{uuid_lib.uuid4()}/views",
+        json={},
+    )
+
+    assert response.status_code == 404
+
+
+@SEMANTIC_LAYERS_APP
+def test_get_views_exception(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST /<uuid>/views returns 400 when implementation raises."""
+    test_uuid = str(uuid_lib.uuid4())
+    mock_layer = MagicMock()
+    mock_layer.uuid = uuid_lib.uuid4()
+    mock_layer.implementation.get_semantic_views.side_effect = ValueError(
+        "Connection failed"
+    )
+
+    mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
+    mock_dao.find_by_uuid.return_value = mock_layer
+
+    response = client.post(
+        f"/api/v1/semantic_layer/{test_uuid}/views",
+        json={"runtime_data": {}},
+    )
+
+    assert response.status_code == 400
+    assert "Connection failed" in response.json["message"]
+
+
+@SEMANTIC_LAYERS_APP
+def test_get_views_existing_dict_config(
+    client: Any,
+    full_api_access: None,
+    mocker: MockerFixture,
+) -> None:
+    """Test POST /<uuid>/views handles dict configuration on existing views."""
+    test_uuid = str(uuid_lib.uuid4())
+    mock_layer = MagicMock()
+    mock_layer.uuid = uuid_lib.uuid4()
+
+    mock_view = MagicMock()
+    mock_view.name = "View X"
+    mock_layer.implementation.get_semantic_views.return_value = [mock_view]
+
+    mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
+    mock_dao.find_by_uuid.return_value = mock_layer
+
+    existing_view = MagicMock()
+    existing_view.name = "View X"
+    existing_view.configuration = {"key": "val"}  # dict, not string
+
+    mock_sv_dao = mocker.patch("superset.semantic_layers.api.SemanticViewDAO")
+    mock_sv_dao.find_by_semantic_layer.return_value = [existing_view]
+
+    response = client.post(
+        f"/api/v1/semantic_layer/{test_uuid}/views",
+        json={"runtime_data": {"key": "val"}},
+    )
+
+    assert response.status_code == 200
+    result = response.json["result"]
+    assert result[0]["already_added"] is True