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