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/02/11 18:56:38 UTC
(superset) branch semantic-layer-ui-semantic-view created (now 8358d9beaea)
This is an automated email from the ASF dual-hosted git repository.
beto pushed a change to branch semantic-layer-ui-semantic-view
in repository https://gitbox.apache.org/repos/asf/superset.git
at 8358d9beaea feat: UI for semantic views
This branch includes the following new commits:
new 4ade90aeb44 feat: API for semantic views
new 8358d9beaea feat: UI for semantic views
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/02: feat: API for semantic views
Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch semantic-layer-ui-semantic-view
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 4ade90aeb44675ed325a19fd225c190b2d895700
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Tue Feb 10 19:24:36 2026 -0500
feat: API for semantic views
---
superset/commands/semantic_layer/__init__.py | 16 ++
superset/commands/semantic_layer/exceptions.py | 41 ++++
superset/commands/semantic_layer/update.py | 67 ++++++
superset/initialization/__init__.py | 2 +
superset/semantic_layers/api.py | 128 +++++++++++
superset/semantic_layers/schemas.py | 22 ++
.../unit_tests/commands/semantic_layer/__init__.py | 16 ++
.../commands/semantic_layer/exceptions_test.py | 48 ++++
.../commands/semantic_layer/update_test.py | 104 +++++++++
tests/unit_tests/semantic_layers/api_test.py | 244 +++++++++++++++++++++
tests/unit_tests/semantic_layers/schemas_test.py | 79 +++++++
11 files changed, 767 insertions(+)
diff --git a/superset/commands/semantic_layer/__init__.py b/superset/commands/semantic_layer/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/superset/commands/semantic_layer/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/superset/commands/semantic_layer/exceptions.py b/superset/commands/semantic_layer/exceptions.py
new file mode 100644
index 00000000000..3e2fad94752
--- /dev/null
+++ b/superset/commands/semantic_layer/exceptions.py
@@ -0,0 +1,41 @@
+# 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 superset.commands.exceptions import (
+ CommandException,
+ CommandInvalidError,
+ ForbiddenError,
+ UpdateFailedError,
+)
+
+
+class SemanticViewNotFoundError(CommandException):
+ status = 404
+ message = _("Semantic view does not exist")
+
+
+class SemanticViewForbiddenError(ForbiddenError):
+ message = _("Changing this semantic view is forbidden")
+
+
+class SemanticViewInvalidError(CommandInvalidError):
+ message = _("Semantic view parameters are invalid.")
+
+
+class SemanticViewUpdateFailedError(UpdateFailedError):
+ message = _("Semantic view could not be updated.")
diff --git a/superset/commands/semantic_layer/update.py b/superset/commands/semantic_layer/update.py
new file mode 100644
index 00000000000..4ce8da07646
--- /dev/null
+++ b/superset/commands/semantic_layer/update.py
@@ -0,0 +1,67 @@
+# 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 annotations
+
+import logging
+from functools import partial
+from typing import Any
+
+from flask_appbuilder.models.sqla import Model
+from sqlalchemy.exc import SQLAlchemyError
+
+from superset import security_manager
+from superset.commands.base import BaseCommand
+from superset.commands.semantic_layer.exceptions import (
+ SemanticViewForbiddenError,
+ SemanticViewNotFoundError,
+ SemanticViewUpdateFailedError,
+)
+from superset.daos.semantic_layer import SemanticViewDAO
+from superset.exceptions import SupersetSecurityException
+from superset.semantic_layers.models import SemanticView
+from superset.utils.decorators import on_error, transaction
+
+logger = logging.getLogger(__name__)
+
+
+class UpdateSemanticViewCommand(BaseCommand):
+ def __init__(self, model_id: int, data: dict[str, Any]):
+ self._model_id = model_id
+ self._properties = data.copy()
+ self._model: SemanticView | None = None
+
+ @transaction(
+ on_error=partial(
+ on_error,
+ catches=(SQLAlchemyError, ValueError),
+ reraise=SemanticViewUpdateFailedError,
+ )
+ )
+ def run(self) -> Model:
+ self.validate()
+ assert self._model
+ return SemanticViewDAO.update(self._model, attributes=self._properties)
+
+ def validate(self) -> None:
+ self._model = SemanticViewDAO.find_by_id(self._model_id)
+ if not self._model:
+ raise SemanticViewNotFoundError()
+
+ try:
+ security_manager.raise_for_ownership(self._model)
+ except SupersetSecurityException as ex:
+ raise SemanticViewForbiddenError() from ex
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 3a34d315bf5..2f90db3d308 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -178,6 +178,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
from superset.reports.api import ReportScheduleRestApi
from superset.reports.logs.api import ReportExecutionLogRestApi
from superset.row_level_security.api import RLSRestApi
+ from superset.semantic_layers.api import SemanticViewRestApi
from superset.security.api import (
RoleRestAPI,
SecurityRestApi,
@@ -265,6 +266,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_api(ReportExecutionLogRestApi)
appbuilder.add_api(RLSRestApi)
appbuilder.add_api(SavedQueryRestApi)
+ appbuilder.add_api(SemanticViewRestApi)
appbuilder.add_api(TagRestApi)
appbuilder.add_api(SqlLabRestApi)
appbuilder.add_api(SqlLabPermalinkRestApi)
diff --git a/superset/semantic_layers/api.py b/superset/semantic_layers/api.py
new file mode 100644
index 00000000000..6cf5ea0e5e9
--- /dev/null
+++ b/superset/semantic_layers/api.py
@@ -0,0 +1,128 @@
+# 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 annotations
+
+import logging
+
+from flask import request, Response
+from flask_appbuilder.api import expose, protect
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from marshmallow import ValidationError
+
+from superset import event_logger
+from superset.commands.semantic_layer.exceptions import (
+ SemanticViewForbiddenError,
+ SemanticViewInvalidError,
+ SemanticViewNotFoundError,
+ SemanticViewUpdateFailedError,
+)
+from superset.commands.semantic_layer.update import UpdateSemanticViewCommand
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
+from superset.semantic_layers.models import SemanticView
+from superset.semantic_layers.schemas import SemanticViewPutSchema
+from superset.views.base_api import (
+ BaseSupersetModelRestApi,
+ requires_json,
+ statsd_metrics,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class SemanticViewRestApi(BaseSupersetModelRestApi):
+ datamodel = SQLAInterface(SemanticView)
+
+ resource_name = "semantic_view"
+ allow_browser_login = True
+ class_permission_name = "SemanticView"
+ method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
+ include_route_methods = {"put"}
+
+ edit_model_schema = SemanticViewPutSchema()
+
+ @expose("/<pk>", methods=("PUT",))
+ @protect()
+ @statsd_metrics
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
+ log_to_statsd=False,
+ )
+ @requires_json
+ def put(self, pk: int) -> Response:
+ """Update a semantic view.
+ ---
+ put:
+ summary: Update a semantic view
+ parameters:
+ - in: path
+ schema:
+ type: integer
+ name: pk
+ requestBody:
+ description: Semantic view schema
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
+ responses:
+ 200:
+ description: Semantic view changed
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: number
+ result:
+ $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
+ 400:
+ $ref: '#/components/responses/400'
+ 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:
+ item = self.edit_model_schema.load(request.json)
+ except ValidationError as error:
+ return self.response_400(message=error.messages)
+ try:
+ changed_model = UpdateSemanticViewCommand(pk, item).run()
+ response = self.response(200, id=changed_model.id, result=item)
+ except SemanticViewNotFoundError:
+ response = self.response_404()
+ except SemanticViewForbiddenError:
+ response = self.response_403()
+ except SemanticViewInvalidError as ex:
+ response = self.response_422(message=ex.normalized_messages())
+ except SemanticViewUpdateFailedError as ex:
+ logger.error(
+ "Error updating model %s: %s",
+ self.__class__.__name__,
+ str(ex),
+ exc_info=True,
+ )
+ response = self.response_422(message=str(ex))
+ return response
diff --git a/superset/semantic_layers/schemas.py b/superset/semantic_layers/schemas.py
new file mode 100644
index 00000000000..b86a5398937
--- /dev/null
+++ b/superset/semantic_layers/schemas.py
@@ -0,0 +1,22 @@
+# 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 marshmallow import fields, Schema
+
+
+class SemanticViewPutSchema(Schema):
+ description = fields.String(allow_none=True)
+ cache_timeout = fields.Integer(allow_none=True)
diff --git a/tests/unit_tests/commands/semantic_layer/__init__.py b/tests/unit_tests/commands/semantic_layer/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/tests/unit_tests/commands/semantic_layer/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/unit_tests/commands/semantic_layer/exceptions_test.py b/tests/unit_tests/commands/semantic_layer/exceptions_test.py
new file mode 100644
index 00000000000..fb6728a36ab
--- /dev/null
+++ b/tests/unit_tests/commands/semantic_layer/exceptions_test.py
@@ -0,0 +1,48 @@
+# 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 superset.commands.semantic_layer.exceptions import (
+ SemanticViewForbiddenError,
+ SemanticViewInvalidError,
+ SemanticViewNotFoundError,
+ SemanticViewUpdateFailedError,
+)
+
+
+def test_semantic_view_not_found_error() -> None:
+ """Test SemanticViewNotFoundError has correct status and message."""
+ error = SemanticViewNotFoundError()
+ assert error.status == 404
+ assert str(error.message) == "Semantic view does not exist"
+
+
+def test_semantic_view_forbidden_error() -> None:
+ """Test SemanticViewForbiddenError has correct message."""
+ error = SemanticViewForbiddenError()
+ assert str(error.message) == "Changing this semantic view is forbidden"
+
+
+def test_semantic_view_invalid_error() -> None:
+ """Test SemanticViewInvalidError has correct message."""
+ error = SemanticViewInvalidError()
+ assert str(error.message) == "Semantic view parameters are invalid."
+
+
+def test_semantic_view_update_failed_error() -> None:
+ """Test SemanticViewUpdateFailedError has correct message."""
+ error = SemanticViewUpdateFailedError()
+ assert str(error.message) == "Semantic view could not be updated."
diff --git a/tests/unit_tests/commands/semantic_layer/update_test.py b/tests/unit_tests/commands/semantic_layer/update_test.py
new file mode 100644
index 00000000000..4c0ad47f8b9
--- /dev/null
+++ b/tests/unit_tests/commands/semantic_layer/update_test.py
@@ -0,0 +1,104 @@
+# 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 unittest.mock import MagicMock
+
+import pytest
+from pytest_mock import MockerFixture
+
+from superset.commands.semantic_layer.exceptions import (
+ SemanticViewForbiddenError,
+ SemanticViewNotFoundError,
+)
+from superset.commands.semantic_layer.update import UpdateSemanticViewCommand
+from superset.exceptions import SupersetSecurityException
+
+
+def test_update_semantic_view_success(mocker: MockerFixture) -> None:
+ """Test successful update of a semantic view."""
+ mock_model = MagicMock()
+ mock_model.id = 1
+
+ dao = mocker.patch(
+ "superset.commands.semantic_layer.update.SemanticViewDAO",
+ )
+ dao.find_by_id.return_value = mock_model
+ dao.update.return_value = mock_model
+
+ mocker.patch(
+ "superset.commands.semantic_layer.update.security_manager",
+ )
+
+ data = {"description": "Updated", "cache_timeout": 300}
+ result = UpdateSemanticViewCommand(1, data).run()
+
+ assert result == mock_model
+ dao.find_by_id.assert_called_once_with(1)
+ dao.update.assert_called_once_with(mock_model, attributes=data)
+
+
+def test_update_semantic_view_not_found(mocker: MockerFixture) -> None:
+ """Test that SemanticViewNotFoundError is raised when model is missing."""
+ dao = mocker.patch(
+ "superset.commands.semantic_layer.update.SemanticViewDAO",
+ )
+ dao.find_by_id.return_value = None
+
+ with pytest.raises(SemanticViewNotFoundError):
+ UpdateSemanticViewCommand(999, {"description": "test"}).run()
+
+
+def test_update_semantic_view_forbidden(mocker: MockerFixture) -> None:
+ """Test that SemanticViewForbiddenError is raised on ownership failure."""
+ mock_model = MagicMock()
+
+ dao = mocker.patch(
+ "superset.commands.semantic_layer.update.SemanticViewDAO",
+ )
+ dao.find_by_id.return_value = mock_model
+
+ sm = mocker.patch(
+ "superset.commands.semantic_layer.update.security_manager",
+ )
+ # Use a regular MagicMock for raise_for_ownership to avoid AsyncMock issues
+ sm.raise_for_ownership = MagicMock(
+ side_effect=SupersetSecurityException(MagicMock()),
+ )
+
+ with pytest.raises(SemanticViewForbiddenError):
+ UpdateSemanticViewCommand(1, {"description": "test"}).run()
+
+
+def test_update_semantic_view_copies_data(mocker: MockerFixture) -> None:
+ """Test that the command copies input data and does not mutate it."""
+ mock_model = MagicMock()
+
+ dao = mocker.patch(
+ "superset.commands.semantic_layer.update.SemanticViewDAO",
+ )
+ dao.find_by_id.return_value = mock_model
+ dao.update.return_value = mock_model
+
+ mocker.patch(
+ "superset.commands.semantic_layer.update.security_manager",
+ )
+
+ original_data = {"description": "Original"}
+ UpdateSemanticViewCommand(1, original_data).run()
+
+ # The original dict should not have been modified
+ assert original_data == {"description": "Original"}
diff --git a/tests/unit_tests/semantic_layers/api_test.py b/tests/unit_tests/semantic_layers/api_test.py
new file mode 100644
index 00000000000..67faf78d8de
--- /dev/null
+++ b/tests/unit_tests/semantic_layers/api_test.py
@@ -0,0 +1,244 @@
+# 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 Any
+from unittest.mock import MagicMock
+
+from pytest_mock import MockerFixture
+
+from superset.commands.semantic_layer.exceptions import (
+ SemanticViewForbiddenError,
+ SemanticViewInvalidError,
+ SemanticViewNotFoundError,
+ SemanticViewUpdateFailedError,
+)
+
+
+def test_put_semantic_view(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test successful PUT updates a semantic view."""
+ changed_model = MagicMock()
+ changed_model.id = 1
+
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.return_value = changed_model
+
+ payload = {"description": "Updated description", "cache_timeout": 300}
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json=payload,
+ )
+
+ assert response.status_code == 200
+ assert response.json["id"] == 1
+ assert response.json["result"] == payload
+ mock_command.assert_called_once_with("1", payload)
+
+
+def test_put_semantic_view_not_found(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT returns 404 when semantic view does not exist."""
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.side_effect = SemanticViewNotFoundError()
+
+ response = client.put(
+ "/api/v1/semantic_view/999",
+ json={"description": "Updated"},
+ )
+
+ assert response.status_code == 404
+
+
+def test_put_semantic_view_forbidden(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT returns 403 when user lacks ownership."""
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.side_effect = SemanticViewForbiddenError()
+
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json={"description": "Updated"},
+ )
+
+ assert response.status_code == 403
+
+
+def test_put_semantic_view_invalid(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT returns 422 when validation fails."""
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.side_effect = SemanticViewInvalidError()
+
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json={"description": "Updated"},
+ )
+
+ assert response.status_code == 422
+
+
+def test_put_semantic_view_update_failed(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT returns 422 when the update operation fails."""
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.side_effect = SemanticViewUpdateFailedError()
+
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json={"description": "Updated"},
+ )
+
+ assert response.status_code == 422
+
+
+def test_put_semantic_view_bad_request(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT returns 400 when the request payload has invalid fields."""
+ # Marshmallow raises ValidationError for unknown fields
+ mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json={"invalid_field": "value"},
+ )
+
+ assert response.status_code == 400
+
+
+def test_put_semantic_view_description_only(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT with only description field."""
+ changed_model = MagicMock()
+ changed_model.id = 1
+
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.return_value = changed_model
+
+ payload = {"description": "New description"}
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json=payload,
+ )
+
+ assert response.status_code == 200
+ assert response.json["result"] == payload
+
+
+def test_put_semantic_view_cache_timeout_only(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT with only cache_timeout field."""
+ changed_model = MagicMock()
+ changed_model.id = 2
+
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.return_value = changed_model
+
+ payload = {"cache_timeout": 600}
+ response = client.put(
+ "/api/v1/semantic_view/2",
+ json=payload,
+ )
+
+ assert response.status_code == 200
+ assert response.json["id"] == 2
+ assert response.json["result"] == payload
+
+
+def test_put_semantic_view_null_values(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT with null values for both fields."""
+ changed_model = MagicMock()
+ changed_model.id = 1
+
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.return_value = changed_model
+
+ payload = {"description": None, "cache_timeout": None}
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json=payload,
+ )
+
+ assert response.status_code == 200
+ assert response.json["result"] == payload
+
+
+def test_put_semantic_view_empty_payload(
+ client: Any,
+ full_api_access: None,
+ mocker: MockerFixture,
+) -> None:
+ """Test PUT with empty payload."""
+ changed_model = MagicMock()
+ changed_model.id = 1
+
+ mock_command = mocker.patch(
+ "superset.semantic_layers.api.UpdateSemanticViewCommand",
+ )
+ mock_command.return_value.run.return_value = changed_model
+
+ response = client.put(
+ "/api/v1/semantic_view/1",
+ json={},
+ )
+
+ assert response.status_code == 200
diff --git a/tests/unit_tests/semantic_layers/schemas_test.py b/tests/unit_tests/semantic_layers/schemas_test.py
new file mode 100644
index 00000000000..739544cdadc
--- /dev/null
+++ b/tests/unit_tests/semantic_layers/schemas_test.py
@@ -0,0 +1,79 @@
+# 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 pytest
+from marshmallow import ValidationError
+
+from superset.semantic_layers.schemas import SemanticViewPutSchema
+
+
+def test_semantic_view_put_schema_both_fields() -> None:
+ """Test loading both description and cache_timeout."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({"description": "A description", "cache_timeout": 300})
+ assert result == {"description": "A description", "cache_timeout": 300}
+
+
+def test_semantic_view_put_schema_description_only() -> None:
+ """Test loading with only description."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({"description": "Just a description"})
+ assert result == {"description": "Just a description"}
+
+
+def test_semantic_view_put_schema_cache_timeout_only() -> None:
+ """Test loading with only cache_timeout."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({"cache_timeout": 600})
+ assert result == {"cache_timeout": 600}
+
+
+def test_semantic_view_put_schema_empty() -> None:
+ """Test loading empty payload."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({})
+ assert result == {}
+
+
+def test_semantic_view_put_schema_null_description() -> None:
+ """Test that description accepts None."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({"description": None})
+ assert result == {"description": None}
+
+
+def test_semantic_view_put_schema_null_cache_timeout() -> None:
+ """Test that cache_timeout accepts None."""
+ schema = SemanticViewPutSchema()
+ result = schema.load({"cache_timeout": None})
+ assert result == {"cache_timeout": None}
+
+
+def test_semantic_view_put_schema_invalid_cache_timeout() -> None:
+ """Test that non-integer cache_timeout raises ValidationError."""
+ schema = SemanticViewPutSchema()
+ with pytest.raises(ValidationError) as exc_info:
+ schema.load({"cache_timeout": "not_a_number"})
+ assert "cache_timeout" in exc_info.value.messages
+
+
+def test_semantic_view_put_schema_unknown_field() -> None:
+ """Test that unknown fields raise ValidationError."""
+ schema = SemanticViewPutSchema()
+ with pytest.raises(ValidationError) as exc_info:
+ schema.load({"unknown_field": "value"})
+ assert "unknown_field" in exc_info.value.messages
(superset) 02/02: feat: UI for semantic views
Posted by be...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch semantic-layer-ui-semantic-view
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 8358d9beaeaa8ffb23e3f7f303eea6ba2b2a8a86
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Wed Feb 11 11:19:50 2026 -0500
feat: UI for semantic views
---
docker/pythonpath_dev/superset_config.py | 1 +
docs/static/feature-flags.json | 6 +
.../components/Label/reusable/DatasetTypeLabel.tsx | 20 +-
.../superset-ui-core/src/utils/featureFlags.ts | 1 +
.../semanticViews/SemanticViewEditModal.tsx | 120 ++
superset-frontend/src/pages/DatasetList/index.tsx | 223 ++-
superset/config.py | 3 +
superset/datasource/api.py | 267 +++-
superset/semantic_layers/models.py | 8 +
superset/static/service-worker.js | 1492 +++++++++++++++++++-
10 files changed, 2089 insertions(+), 52 deletions(-)
diff --git a/docker/pythonpath_dev/superset_config.py b/docker/pythonpath_dev/superset_config.py
index cdf33c2bb9c..ce7f1998708 100644
--- a/docker/pythonpath_dev/superset_config.py
+++ b/docker/pythonpath_dev/superset_config.py
@@ -109,6 +109,7 @@ FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"DATASET_FOLDERS": True,
"ENABLE_EXTENSIONS": True,
+ "SEMANTIC_LAYERS": True,
}
EXTENSIONS_PATH = "/app/docker/extensions"
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json
index 6ca4c2ea818..f87df2d96cd 100644
--- a/docs/static/feature-flags.json
+++ b/docs/static/feature-flags.json
@@ -69,6 +69,12 @@
"lifecycle": "development",
"description": "Expand nested types in Presto into extra columns/arrays. Experimental, doesn't work with all nested types."
},
+ {
+ "name": "SEMANTIC_LAYERS",
+ "default": false,
+ "lifecycle": "development",
+ "description": "Enable semantic layers and show semantic views alongside datasets"
+ },
{
"name": "TABLE_V2_TIME_COMPARISON_ENABLED",
"default": false,
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx b/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
index d8567d93b2a..f3d19617742 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/Label/reusable/DatasetTypeLabel.tsx
@@ -23,7 +23,7 @@ import { Label } from '..';
// Define the prop types for DatasetTypeLabel
interface DatasetTypeLabelProps {
- datasetType: 'physical' | 'virtual'; // Accepts only 'physical' or 'virtual'
+ datasetType: 'physical' | 'virtual' | 'semantic_view';
}
const SIZE = 's'; // Define the size as a constant
@@ -32,6 +32,24 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType,
}) => {
const theme = useTheme();
+
+ if (datasetType === 'semantic_view') {
+ return (
+ <Label
+ icon={
+ <Icons.ApartmentOutlined
+ iconSize={SIZE}
+ iconColor={theme.colorInfo}
+ />
+ }
+ type="info"
+ style={{ color: theme.colorInfo }}
+ >
+ {t('Semantic')}
+ </Label>
+ );
+ }
+
const label: string =
datasetType === 'physical' ? t('Physical') : t('Virtual');
const icon =
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index 46f832cc094..dbfd146914a 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -57,6 +57,7 @@ export enum FeatureFlag {
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
Matrixify = 'MATRIXIFY',
ScheduledQueries = 'SCHEDULED_QUERIES',
+ SemanticLayers = 'SEMANTIC_LAYERS',
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
SshTunneling = 'SSH_TUNNELING',
diff --git a/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx b/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx
new file mode 100644
index 00000000000..ed90d165b9c
--- /dev/null
+++ b/superset-frontend/src/features/semanticViews/SemanticViewEditModal.tsx
@@ -0,0 +1,120 @@
+/**
+ * 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 { useState, useEffect } from 'react';
+import { t } from '@apache-superset/core';
+import { styled } from '@apache-superset/core/ui';
+import { SupersetClient } from '@superset-ui/core';
+import { Input, InputNumber } from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import {
+ StandardModal,
+ ModalFormField,
+ MODAL_STANDARD_WIDTH,
+} from 'src/components/Modal';
+
+const ModalContent = styled.div`
+ padding: ${({ theme }) => theme.sizeUnit * 4}px;
+`;
+
+interface SemanticViewEditModalProps {
+ show: boolean;
+ onHide: () => void;
+ onSave: () => void;
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+ semanticView: {
+ id: number;
+ table_name: string;
+ description?: string | null;
+ cache_timeout?: number | null;
+ } | null;
+}
+
+export default function SemanticViewEditModal({
+ show,
+ onHide,
+ onSave,
+ addDangerToast,
+ addSuccessToast,
+ semanticView,
+}: SemanticViewEditModalProps) {
+ const [description, setDescription] = useState<string>('');
+ const [cacheTimeout, setCacheTimeout] = useState<number | null>(null);
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ if (semanticView) {
+ setDescription(semanticView.description || '');
+ setCacheTimeout(semanticView.cache_timeout ?? null);
+ }
+ }, [semanticView]);
+
+ const handleSave = async () => {
+ if (!semanticView) return;
+ setSaving(true);
+ try {
+ await SupersetClient.put({
+ endpoint: `/api/v1/semantic_view/${semanticView.id}`,
+ jsonPayload: {
+ description: description || null,
+ cache_timeout: cacheTimeout,
+ },
+ });
+ addSuccessToast(t('Semantic view updated'));
+ onSave();
+ onHide();
+ } catch {
+ addDangerToast(t('An error occurred while saving the semantic view'));
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+ <StandardModal
+ show={show}
+ onHide={onHide}
+ onSave={handleSave}
+ title={t('Edit %s', semanticView?.table_name || '')}
+ icon={<Icons.EditOutlined />}
+ isEditMode
+ width={MODAL_STANDARD_WIDTH}
+ saveLoading={saving}
+ >
+ <ModalContent>
+ <ModalFormField label={t('Description')}>
+ <Input.TextArea
+ value={description}
+ onChange={e => setDescription(e.target.value)}
+ rows={4}
+ />
+ </ModalFormField>
+ <ModalFormField label={t('Cache timeout')}>
+ <InputNumber
+ value={cacheTimeout}
+ onChange={value => setCacheTimeout(value as number | null)}
+ min={0}
+ placeholder={t('Duration in seconds')}
+ style={{ width: '100%' }}
+ />
+ </ModalFormField>
+ </ModalContent>
+ </StandardModal>
+ );
+}
diff --git a/superset-frontend/src/pages/DatasetList/index.tsx b/superset-frontend/src/pages/DatasetList/index.tsx
index 7822e15a9e5..ceafdd03a42 100644
--- a/superset-frontend/src/pages/DatasetList/index.tsx
+++ b/superset-frontend/src/pages/DatasetList/index.tsx
@@ -17,7 +17,12 @@
* under the License.
*/
import { t } from '@apache-superset/core';
-import { getExtensionsRegistry, SupersetClient } from '@superset-ui/core';
+import {
+ getExtensionsRegistry,
+ SupersetClient,
+ isFeatureEnabled,
+ FeatureFlag,
+} from '@superset-ui/core';
import { styled, useTheme, css } from '@apache-superset/core/ui';
import { FunctionComponent, useState, useMemo, useCallback, Key } from 'react';
import { Link, useHistory } from 'react-router-dom';
@@ -48,6 +53,7 @@ import {
ListViewFilterOperator as FilterOperator,
type ListViewProps,
type ListViewFilters,
+ type ListViewFetchDataConfig,
} from 'src/components';
import { Typography } from '@superset-ui/core/components/Typography';
import handleResourceExport from 'src/utils/export';
@@ -65,6 +71,7 @@ import {
CONFIRM_OVERWRITE_MESSAGE,
} from 'src/features/datasets/constants';
import DuplicateDatasetModal from 'src/features/datasets/DuplicateDatasetModal';
+import SemanticViewEditModal from 'src/features/semanticViews/SemanticViewEditModal';
import { useSelector } from 'react-redux';
import { QueryObjectColumns } from 'src/views/CRUD/types';
import { WIDER_DROPDOWN_WIDTH } from 'src/components/ListView/utils';
@@ -118,13 +125,16 @@ type Dataset = {
database: {
id: string;
database_name: string;
- };
+ } | null;
kind: string;
+ source_type?: 'database' | 'semantic_layer';
explore_url: string;
id: number;
owners: Array<Owner>;
schema: string;
table_name: string;
+ description?: string | null;
+ cache_timeout?: number | null;
};
interface VirtualDataset extends Dataset {
@@ -150,18 +160,90 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const history = useHistory();
const theme = useTheme();
const {
- state: {
- loading,
- resourceCount: datasetCount,
- resourceCollection: datasets,
- bulkSelectEnabled,
- },
+ state: { bulkSelectEnabled },
hasPerm,
- fetchData,
toggleBulkSelect,
- refreshData,
} = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
+ // Combined endpoint state
+ const [datasets, setDatasets] = useState<Dataset[]>([]);
+ const [datasetCount, setDatasetCount] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [lastFetchConfig, setLastFetchConfig] =
+ useState<ListViewFetchDataConfig | null>(null);
+ const [currentSourceFilter, setCurrentSourceFilter] = useState<string>('');
+
+ const fetchData = useCallback((config: ListViewFetchDataConfig) => {
+ setLastFetchConfig(config);
+ setLoading(true);
+ const { pageIndex, pageSize, sortBy, filters: filterValues } = config;
+
+ // Separate source_type filter from other filters
+ const sourceTypeFilter = filterValues.find(f => f.id === 'source_type');
+
+ // Track source filter for conditional Type filter visibility
+ const sourceVal =
+ sourceTypeFilter?.value && typeof sourceTypeFilter.value === 'object'
+ ? (sourceTypeFilter.value as { value: string }).value
+ : ((sourceTypeFilter?.value as string) ?? '');
+ setCurrentSourceFilter(sourceVal);
+ const otherFilters = filterValues
+ .filter(f => f.id !== 'source_type')
+ .filter(
+ ({ value }) => value !== '' && value !== null && value !== undefined,
+ )
+ .map(({ id, operator: opr, value }) => ({
+ col: id,
+ opr,
+ value:
+ value && typeof value === 'object' && 'value' in value
+ ? value.value
+ : value,
+ }));
+
+ // Add source_type filter for the combined endpoint
+ const sourceTypeValue =
+ sourceTypeFilter?.value && typeof sourceTypeFilter.value === 'object'
+ ? (sourceTypeFilter.value as { value: string }).value
+ : sourceTypeFilter?.value;
+ if (sourceTypeValue) {
+ otherFilters.push({
+ col: 'source_type',
+ opr: 'eq',
+ value: sourceTypeValue,
+ });
+ }
+
+ const queryParams = rison.encode_uri({
+ order_column: sortBy[0].id,
+ order_direction: sortBy[0].desc ? 'desc' : 'asc',
+ page: pageIndex,
+ page_size: pageSize,
+ ...(otherFilters.length ? { filters: otherFilters } : {}),
+ });
+
+ return SupersetClient.get({
+ endpoint: `/api/v1/datasource/?q=${queryParams}`,
+ })
+ .then(({ json = {} }) => {
+ setDatasets(json.result);
+ setDatasetCount(json.count);
+ })
+ .catch(() => {
+ addDangerToast(t('An error occurred while fetching datasets'));
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }, []);
+
+ const refreshData = useCallback(() => {
+ if (lastFetchConfig) {
+ return fetchData(lastFetchConfig);
+ }
+ return undefined;
+ }, [lastFetchConfig, fetchData]);
+
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
| (Dataset & {
charts: any;
@@ -176,6 +258,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const [datasetCurrentlyDuplicating, setDatasetCurrentlyDuplicating] =
useState<VirtualDataset | null>(null);
+ const [svCurrentlyEditing, setSvCurrentlyEditing] = useState<Dataset | null>(
+ null,
+ );
+
const [importingDataset, showImportModal] = useState<boolean>(false);
const [passwordFields, setPasswordFields] = useState<string[]>([]);
const [preparingExport, setPreparingExport] = useState<boolean>(false);
@@ -366,12 +452,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
id: 'kind',
},
{
+ Cell: ({
+ row: {
+ original: { database },
+ },
+ }: any) => database?.database_name || '-',
Header: t('Database'),
accessor: 'database.database_name',
size: 'xl',
id: 'database.database_name',
},
{
+ Cell: ({
+ row: {
+ original: { schema },
+ },
+ }: any) => schema || '-',
Header: t('Schema'),
accessor: 'schema',
size: 'lg',
@@ -414,9 +510,40 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true,
id: 'sql',
},
+ {
+ accessor: 'source_type',
+ hidden: true,
+ disableSortBy: true,
+ id: 'source_type',
+ },
{
Cell: ({ row: { original } }: any) => {
- // Verify owner or isAdmin
+ const isSemanticView = original.source_type === 'semantic_layer';
+
+ // Semantic view: only show edit button
+ if (isSemanticView) {
+ if (!canEdit) return null;
+ return (
+ <Actions className="actions">
+ <Tooltip
+ id="edit-action-tooltip"
+ title={t('Edit')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={() => setSvCurrentlyEditing(original)}
+ >
+ <Icons.EditOutlined iconSize="l" />
+ </span>
+ </Tooltip>
+ </Actions>
+ );
+ }
+
+ // Dataset: full set of actions
const allowEdit =
original.owners.map((o: Owner) => o.id).includes(user.userId) ||
isUserAdmin(user);
@@ -519,6 +646,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const filterTypes: ListViewFilters = useMemo(
() => [
+ ...(isFeatureEnabled(FeatureFlag.SemanticLayers)
+ ? [
+ {
+ Header: t('Source'),
+ key: 'source_type',
+ id: 'source_type',
+ input: 'select' as const,
+ operator: FilterOperator.Equals,
+ unfilteredLabel: t('All'),
+ selects: [
+ { label: t('Database'), value: 'database' },
+ { label: t('Semantic Layer'), value: 'semantic_layer' },
+ ],
+ },
+ ]
+ : []),
{
Header: t('Name'),
key: 'search',
@@ -526,18 +669,42 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
input: 'search',
operator: FilterOperator.Contains,
},
- {
- Header: t('Type'),
- key: 'sql',
- id: 'sql',
- input: 'select',
- operator: FilterOperator.DatasetIsNullOrEmpty,
- unfilteredLabel: 'All',
- selects: [
- { label: t('Virtual'), value: false },
- { label: t('Physical'), value: true },
- ],
- },
+ ...(isFeatureEnabled(FeatureFlag.SemanticLayers)
+ ? [
+ {
+ Header: t('Type'),
+ key: 'sql',
+ id: 'sql',
+ input: 'select' as const,
+ operator: FilterOperator.DatasetIsNullOrEmpty,
+ unfilteredLabel: 'All',
+ selects: [
+ ...(currentSourceFilter !== 'semantic_layer'
+ ? [
+ { label: t('Physical'), value: true },
+ { label: t('Virtual'), value: false },
+ ]
+ : []),
+ ...(currentSourceFilter !== 'database'
+ ? [{ label: t('Semantic View'), value: 'semantic_view' }]
+ : []),
+ ],
+ },
+ ]
+ : [
+ {
+ Header: t('Type'),
+ key: 'sql',
+ id: 'sql',
+ input: 'select' as const,
+ operator: FilterOperator.DatasetIsNullOrEmpty,
+ unfilteredLabel: 'All',
+ selects: [
+ { label: t('Physical'), value: true },
+ { label: t('Virtual'), value: false },
+ ],
+ },
+ ]),
{
Header: t('Database'),
key: 'database',
@@ -628,7 +795,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
},
],
- [user],
+ [user, currentSourceFilter],
);
const menuData: SubMenuProps = {
@@ -880,6 +1047,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
onHide={closeDatasetDuplicateModal}
onDuplicate={handleDatasetDuplicate}
/>
+ <SemanticViewEditModal
+ show={!!svCurrentlyEditing}
+ onHide={() => setSvCurrentlyEditing(null)}
+ onSave={refreshData}
+ addDangerToast={addDangerToast}
+ addSuccessToast={addSuccessToast}
+ semanticView={svCurrentlyEditing}
+ />
<ConfirmStatusChange
title={t('Please confirm')}
description={t(
diff --git a/superset/config.py b/superset/config.py
index 9731d80bd58..b46ce0ca373 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -562,6 +562,9 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# in addition to relative timeshifts (e.g., "1 day ago")
# @lifecycle: development
"DATE_RANGE_TIMESHIFTS_ENABLED": False,
+ # Enable semantic layers and show semantic views alongside datasets
+ # @lifecycle: development
+ "SEMANTIC_LAYERS": False,
# Enables advanced data type support
# @lifecycle: development
"ENABLE_ADVANCED_DATA_TYPES": False,
diff --git a/superset/datasource/api.py b/superset/datasource/api.py
index da5f7e3ded8..ca8a235192b 100644
--- a/superset/datasource/api.py
+++ b/superset/datasource/api.py
@@ -15,17 +15,23 @@
# specific language governing permissions and limitations
# under the License.
import logging
+from typing import Any
from flask import current_app as app, request
-from flask_appbuilder.api import expose, protect, safe
+from flask_appbuilder.api import expose, protect, rison, safe
+from flask_appbuilder.api.schemas import get_list_schema
+from sqlalchemy import and_, func, literal, or_, select, union_all
-from superset import event_logger
-from superset.connectors.sqla.models import BaseDatasource
+from superset import db, event_logger, is_feature_enabled, security_manager
+from superset.connectors.sqla import models as sqla_models
+from superset.connectors.sqla.models import BaseDatasource, SqlaTable
from superset.daos.datasource import DatasourceDAO
from superset.daos.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError
from superset.exceptions import SupersetSecurityException
+from superset.semantic_layers.models import SemanticView
from superset.superset_typing import FlaskResponse
from superset.utils.core import apply_max_row_limit, DatasourceType, SqlExpressionType
+from superset.utils.filters import get_dataset_access_filters
from superset.views.base_api import BaseSupersetApi, statsd_metrics
logger = logging.getLogger(__name__)
@@ -303,3 +309,258 @@ class DatasourceRestApi(BaseSupersetApi):
f"Invalid expression type: {expression_type}. "
f"Valid types are: column, metric, where, having"
) from None
+
+ @expose("/", methods=("GET",))
+ @safe
+ @statsd_metrics
+ @rison(get_list_schema)
+ @event_logger.log_this_with_context(
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
+ ".combined_list",
+ log_to_statsd=False,
+ )
+ def combined_list(self, **kwargs: Any) -> FlaskResponse:
+ """List datasets and semantic views combined.
+ ---
+ get:
+ summary: List datasets and semantic views combined
+ parameters:
+ - in: query
+ name: q
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/get_list_schema'
+ responses:
+ 200:
+ description: Combined list of datasets and semantic views
+ 401:
+ $ref: '#/components/responses/401'
+ 403:
+ $ref: '#/components/responses/403'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ if not security_manager.can_access("can_read", "Dataset"):
+ return self.response(403, message="Access denied")
+
+ args = kwargs.get("rison", {})
+ page = args.get("page", 0)
+ page_size = args.get("page_size", 25)
+ order_column = args.get("order_column", "changed_on")
+ order_direction = args.get("order_direction", "desc")
+ filters = args.get("filters", [])
+
+ # Extract source_type, name search, sql (type), and type_filter
+ source_type = "all"
+ name_filter = None
+ sql_filter = None # None = no filter, True = physical, False = virtual
+ type_filter = None # None = no filter, "semantic_view" / True / False
+ for f in filters:
+ if f.get("col") == "source_type":
+ source_type = f.get("value", "all")
+ elif f.get("col") == "table_name" and f.get("opr") == "ct":
+ name_filter = f.get("value")
+ elif f.get("col") == "sql":
+ val = f.get("value")
+ if val == "semantic_view":
+ type_filter = "semantic_view"
+ else:
+ sql_filter = val
+
+ # If semantic layers feature flag is off, only show datasets
+ if not is_feature_enabled("SEMANTIC_LAYERS"):
+ source_type = "database"
+
+ # Map sort columns
+ sort_col_map = {
+ "changed_on_delta_humanized": "changed_on",
+ "table_name": "table_name",
+ }
+ sort_col_name = sort_col_map.get(order_column, "changed_on")
+
+ # Build dataset subquery
+ ds_q = select(
+ SqlaTable.id.label("item_id"),
+ literal("database").label("source_type"),
+ SqlaTable.changed_on,
+ SqlaTable.table_name,
+ ).select_from(SqlaTable.__table__)
+
+ # Apply security filters for datasets
+ if not security_manager.can_access_all_datasources():
+ ds_q = ds_q.join(
+ sqla_models.Database,
+ sqla_models.Database.id == SqlaTable.database_id,
+ )
+ ds_q = ds_q.where(get_dataset_access_filters(SqlaTable))
+
+ # Apply name filter to datasets
+ if name_filter:
+ ds_q = ds_q.where(SqlaTable.table_name.ilike(f"%{name_filter}%"))
+
+ # Apply sql (type) filter to datasets
+ if sql_filter is not None:
+ if sql_filter:
+ # Physical: sql is null or empty
+ ds_q = ds_q.where(
+ or_(SqlaTable.sql.is_(None), SqlaTable.sql == "")
+ )
+ else:
+ # Virtual: sql is not null and not empty
+ ds_q = ds_q.where(
+ and_(SqlaTable.sql.isnot(None), SqlaTable.sql != "")
+ )
+ # Selecting Physical/Virtual implicitly means "database only"
+ if source_type == "all":
+ source_type = "database"
+
+ # Handle type_filter = "semantic_view"
+ if type_filter == "semantic_view":
+ source_type = "semantic_layer"
+
+ # Build semantic view subquery
+ sv_q = select(
+ SemanticView.id.label("item_id"),
+ literal("semantic_layer").label("source_type"),
+ SemanticView.changed_on,
+ SemanticView.name.label("table_name"),
+ ).select_from(SemanticView.__table__)
+
+ # Apply name filter to semantic views
+ if name_filter:
+ sv_q = sv_q.where(SemanticView.name.ilike(f"%{name_filter}%"))
+
+ # Build combined query based on source_type
+ if source_type == "database":
+ combined = ds_q.subquery()
+ elif source_type == "semantic_layer":
+ combined = sv_q.subquery()
+ else:
+ combined = union_all(ds_q, sv_q).subquery()
+
+ # Count total
+ count_q = select(func.count()).select_from(combined)
+ total_count = db.session.execute(count_q).scalar() or 0
+
+ # Sort and paginate
+ sort_col = combined.c[sort_col_name]
+ if order_direction == "desc":
+ sort_col = sort_col.desc()
+ else:
+ sort_col = sort_col.asc()
+
+ paginated_q = (
+ select(
+ combined.c.item_id,
+ combined.c.source_type,
+ )
+ .order_by(sort_col)
+ .offset(page * page_size)
+ .limit(page_size)
+ )
+ rows = db.session.execute(paginated_q).fetchall()
+
+ # Collect IDs by type
+ dataset_ids = [r.item_id for r in rows if r.source_type == "database"]
+ sv_ids = [r.item_id for r in rows if r.source_type == "semantic_layer"]
+
+ # Fetch full ORM objects
+ datasets_map: dict[int, SqlaTable] = {}
+ if dataset_ids:
+ ds_objs = (
+ db.session.query(SqlaTable)
+ .filter(SqlaTable.id.in_(dataset_ids))
+ .all()
+ )
+ datasets_map = {obj.id: obj for obj in ds_objs}
+
+ sv_map: dict[int, SemanticView] = {}
+ if sv_ids:
+ sv_objs = (
+ db.session.query(SemanticView)
+ .filter(SemanticView.id.in_(sv_ids))
+ .all()
+ )
+ sv_map = {obj.id: obj for obj in sv_objs}
+
+ # Serialize in UNION order
+ result = []
+ for row in rows:
+ if row.source_type == "database":
+ obj = datasets_map.get(row.item_id)
+ if obj:
+ result.append(self._serialize_dataset(obj))
+ else:
+ obj = sv_map.get(row.item_id)
+ if obj:
+ result.append(self._serialize_semantic_view(obj))
+
+ return self.response(200, count=total_count, result=result)
+
+ @staticmethod
+ def _serialize_dataset(obj: SqlaTable) -> dict[str, Any]:
+ changed_by = obj.changed_by
+ return {
+ "id": obj.id,
+ "uuid": str(obj.uuid),
+ "table_name": obj.table_name,
+ "kind": obj.kind,
+ "source_type": "database",
+ "description": obj.description,
+ "explore_url": obj.explore_url,
+ "database": {
+ "id": obj.database_id,
+ "database_name": obj.database.database_name,
+ }
+ if obj.database
+ else None,
+ "schema": obj.schema,
+ "sql": obj.sql,
+ "extra": obj.extra,
+ "owners": [
+ {
+ "id": o.id,
+ "first_name": o.first_name,
+ "last_name": o.last_name,
+ }
+ for o in obj.owners
+ ],
+ "changed_by_name": obj.changed_by_name,
+ "changed_by": {
+ "first_name": changed_by.first_name,
+ "last_name": changed_by.last_name,
+ }
+ if changed_by
+ else None,
+ "changed_on_delta_humanized": obj.changed_on_delta_humanized(),
+ "changed_on_utc": obj.changed_on_utc(),
+ }
+
+ @staticmethod
+ def _serialize_semantic_view(obj: SemanticView) -> dict[str, Any]:
+ changed_by = obj.changed_by
+ return {
+ "id": obj.id,
+ "uuid": str(obj.uuid),
+ "table_name": obj.name,
+ "kind": "semantic_view",
+ "source_type": "semantic_layer",
+ "description": obj.description,
+ "cache_timeout": obj.cache_timeout,
+ "explore_url": obj.explore_url,
+ "database": None,
+ "schema": None,
+ "sql": None,
+ "extra": None,
+ "owners": [],
+ "changed_by_name": obj.changed_by_name,
+ "changed_by": {
+ "first_name": changed_by.first_name,
+ "last_name": changed_by.last_name,
+ }
+ if changed_by
+ else None,
+ "changed_on_delta_humanized": obj.changed_on_delta_humanized(),
+ "changed_on_utc": obj.changed_on_utc(),
+ }
diff --git a/superset/semantic_layers/models.py b/superset/semantic_layers/models.py
index 6bcc48c6871..7a229485412 100644
--- a/superset/semantic_layers/models.py
+++ b/superset/semantic_layers/models.py
@@ -205,6 +205,14 @@ class SemanticView(AuditMixinNullable, Model):
def get_query_str(self, query_obj: QueryObjectDict) -> str:
return "Not implemented for semantic layers"
+ @property
+ def table_name(self) -> str:
+ return self.name
+
+ @property
+ def kind(self) -> str:
+ return "semantic_view"
+
@property
def uid(self) -> str:
return self.implementation.uid()
diff --git a/superset/static/service-worker.js b/superset/static/service-worker.js
index 43cb14a4894..85638a05975 100644
--- a/superset/static/service-worker.js
+++ b/superset/static/service-worker.js
@@ -1,27 +1,1471 @@
-/**
- * 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.
+/*
+ * ATTENTION: An "eval-source-map" devtool has been used.
+ * This devtool is neither made for production nor for readable output files.
+ * It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
+ * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
+ * or disable the default devtool with "devtool: false".
+ * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
+/******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ var __webpack_modules__ = ({
-// Minimal service worker for PWA file handling support
-self.addEventListener('install', event => {
- event.waitUntil(self.skipWaiting());
-});
+/***/ "./src/service-worker.ts"
+/*!*******************************!*\
+ !*** ./src/service-worker.ts ***!
+ \*******************************/
+() {
-self.addEventListener('activate', event => {
- event.waitUntil(self.clients.claim());
-});
+eval("{/**\n * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership. The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2 [...]
+
+/***/ }
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Check if module exists (development only)
+/******/ if (__webpack_modules__[moduleId] === undefined) {
+/******/ var e = new Error("Cannot find module '" + moduleId + "'");
+/******/ e.code = 'MODULE_NOT_FOUND';
+/******/ throw e;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ id: moduleId,
+/******/ loaded: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ var execOptions = { id: moduleId, module: module, factory: __webpack_modules__[moduleId], require: __webpack_require__ };
+/******/ __webpack_require__.i.forEach(function(handler) { handler(execOptions); });
+/******/ module = execOptions.module;
+/******/ execOptions.factory.call(module.exports, module, module.exports, execOptions.require);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = __webpack_modules__;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = __webpack_module_cache__;
+/******/
+/******/ // expose the module execution interceptor
+/******/ __webpack_require__.i = [];
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/chunk loaded */
+/******/ (() => {
+/******/ var deferred = [];
+/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
+/******/ if(chunkIds) {
+/******/ priority = priority || 0;
+/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
+/******/ deferred[i] = [chunkIds, fn, priority];
+/******/ return;
+/******/ }
+/******/ var notFulfilled = Infinity;
+/******/ for (var i = 0; i < deferred.length; i++) {
+/******/ var [chunkIds, fn, priority] = deferred[i];
+/******/ var fulfilled = true;
+/******/ for (var j = 0; j < chunkIds.length; j++) {
+/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
+/******/ chunkIds.splice(j--, 1);
+/******/ } else {
+/******/ fulfilled = false;
+/******/ if(priority < notFulfilled) notFulfilled = priority;
+/******/ }
+/******/ }
+/******/ if(fulfilled) {
+/******/ deferred.splice(i--, 1)
+/******/ var r = fn();
+/******/ if (r !== undefined) result = r;
+/******/ }
+/******/ }
+/******/ return result;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/compat get default export */
+/******/ (() => {
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = (module) => {
+/******/ var getter = module && module.__esModule ?
+/******/ () => (module['default']) :
+/******/ () => (module);
+/******/ __webpack_require__.d(getter, { a: getter });
+/******/ return getter;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/create fake namespace object */
+/******/ (() => {
+/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
+/******/ var leafPrototypes;
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 16: return value when it's Promise-like
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = this(value);
+/******/ if(mode & 8) return value;
+/******/ if(typeof value === 'object' && value) {
+/******/ if((mode & 4) && value.__esModule) return value;
+/******/ if((mode & 16) && typeof value.then === 'function') return value;
+/******/ }
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ var def = {};
+/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
+/******/ for(var current = mode & 2 && value; (typeof current == 'object' || typeof current == 'function') && !~leafPrototypes.indexOf(current); current = getProto(current)) {
+/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
+/******/ }
+/******/ def['default'] = () => (value);
+/******/ __webpack_require__.d(ns, def);
+/******/ return ns;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/get javascript update chunk filename */
+/******/ (() => {
+/******/ // This function allow to reference all chunks
+/******/ __webpack_require__.hu = (chunkId) => {
+/******/ // return url for filenames based on template
+/******/ return "" + chunkId + "." + __webpack_require__.h() + ".hot-update.js";
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/get update manifest filename */
+/******/ (() => {
+/******/ __webpack_require__.hmrF = () => ("service-worker." + __webpack_require__.h() + ".hot-update.json");
+/******/ })();
+/******/
+/******/ /* webpack/runtime/getFullHash */
+/******/ (() => {
+/******/ __webpack_require__.h = () => ("87e83073d75b3c4295d1")
+/******/ })();
+/******/
+/******/ /* webpack/runtime/harmony module decorator */
+/******/ (() => {
+/******/ __webpack_require__.hmd = (module) => {
+/******/ module = Object.create(module);
+/******/ if (!module.children) module.children = [];
+/******/ Object.defineProperty(module, 'exports', {
+/******/ enumerable: true,
+/******/ set: () => {
+/******/ throw new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);
+/******/ }
+/******/ });
+/******/ return module;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/load script */
+/******/ (() => {
+/******/ var inProgress = {};
+/******/ var dataWebpackPrefix = "superset:";
+/******/ // loadScript function to load a script via script tag
+/******/ __webpack_require__.l = (url, done, key, chunkId) => {
+/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
+/******/ var script, needAttach;
+/******/ if(key !== undefined) {
+/******/ var scripts = document.getElementsByTagName("script");
+/******/ for(var i = 0; i < scripts.length; i++) {
+/******/ var s = scripts[i];
+/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
+/******/ }
+/******/ }
+/******/ if(!script) {
+/******/ needAttach = true;
+/******/ script = document.createElement('script');
+/******/
+/******/ script.charset = 'utf-8';
+/******/ if (__webpack_require__.nc) {
+/******/ script.setAttribute("nonce", __webpack_require__.nc);
+/******/ }
+/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
+/******/
+/******/ script.src = url;
+/******/ }
+/******/ inProgress[url] = [done];
+/******/ var onScriptComplete = (prev, event) => {
+/******/ // avoid mem leaks in IE.
+/******/ script.onerror = script.onload = null;
+/******/ clearTimeout(timeout);
+/******/ var doneFns = inProgress[url];
+/******/ delete inProgress[url];
+/******/ script.parentNode && script.parentNode.removeChild(script);
+/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
+/******/ if(prev) return prev(event);
+/******/ }
+/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
+/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
+/******/ script.onload = onScriptComplete.bind(null, script.onload);
+/******/ needAttach && document.head.appendChild(script);
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/node module decorator */
+/******/ (() => {
+/******/ __webpack_require__.nmd = (module) => {
+/******/ module.paths = [];
+/******/ if (!module.children) module.children = [];
+/******/ return module;
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/sharing */
+/******/ (() => {
+/******/ __webpack_require__.S = {};
+/******/ var initPromises = {};
+/******/ var initTokens = {};
+/******/ __webpack_require__.I = (name, initScope) => {
+/******/ if(!initScope) initScope = [];
+/******/ // handling circular init calls
+/******/ var initToken = initTokens[name];
+/******/ if(!initToken) initToken = initTokens[name] = {};
+/******/ if(initScope.indexOf(initToken) >= 0) return;
+/******/ initScope.push(initToken);
+/******/ // only runs once
+/******/ if(initPromises[name]) return initPromises[name];
+/******/ // creates a new share scope if needed
+/******/ if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
+/******/ // runs all init snippets from all modules reachable
+/******/ var scope = __webpack_require__.S[name];
+/******/ var warn = (msg) => {
+/******/ if (typeof console !== "undefined" && console.warn) console.warn(msg);
+/******/ };
+/******/ var uniqueName = "superset";
+/******/ var register = (name, version, factory, eager) => {
+/******/ var versions = scope[name] = scope[name] || {};
+/******/ var activeVersion = versions[version];
+/******/ if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
+/******/ };
+/******/ var initExternal = (id) => {
+/******/ var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
+/******/ try {
+/******/ var module = __webpack_require__(id);
+/******/ if(!module) return;
+/******/ var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
+/******/ if(module.then) return promises.push(module.then(initFn, handleError));
+/******/ var initResult = initFn(module);
+/******/ if(initResult && initResult.then) return promises.push(initResult['catch'](handleError));
+/******/ } catch(err) { handleError(err); }
+/******/ }
+/******/ var promises = [];
+/******/ switch(name) {
+/******/ case "default": {
+/******/ register("antd", "5.27.6", () => (() => (__webpack_require__(/*! ./node_modules/antd/es/index.js */ "./node_modules/antd/es/index.js"))), 1);
+/******/ register("react-dom", "17.0.2", () => (() => (__webpack_require__(/*! ./node_modules/react-dom/index.js */ "./node_modules/react-dom/index.js"))), 1);
+/******/ register("react", "17.0.2", () => (() => (__webpack_require__(/*! ./node_modules/react/index.js */ "./node_modules/react/index.js"))), 1);
+/******/ }
+/******/ break;
+/******/ }
+/******/ if(!promises.length) return initPromises[name] = 1;
+/******/ return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hot module replacement */
+/******/ (() => {
+/******/ var currentModuleData = {};
+/******/ var installedModules = __webpack_require__.c;
+/******/
+/******/ // module and require creation
+/******/ var currentChildModule;
+/******/ var currentParents = [];
+/******/
+/******/ // status
+/******/ var registeredStatusHandlers = [];
+/******/ var currentStatus = "idle";
+/******/
+/******/ // while downloading
+/******/ var blockingPromises = 0;
+/******/ var blockingPromisesWaiting = [];
+/******/
+/******/ // The update info
+/******/ var currentUpdateApplyHandlers;
+/******/ var queuedInvalidatedModules;
+/******/
+/******/ __webpack_require__.hmrD = currentModuleData;
+/******/
+/******/ __webpack_require__.i.push(function (options) {
+/******/ var module = options.module;
+/******/ var require = createRequire(options.require, options.id);
+/******/ module.hot = createModuleHotObject(options.id, module);
+/******/ module.parents = currentParents;
+/******/ module.children = [];
+/******/ currentParents = [];
+/******/ options.require = require;
+/******/ });
+/******/
+/******/ __webpack_require__.hmrC = {};
+/******/ __webpack_require__.hmrI = {};
+/******/
+/******/ function createRequire(require, moduleId) {
+/******/ var me = installedModules[moduleId];
+/******/ if (!me) return require;
+/******/ var fn = function (request) {
+/******/ if (me.hot.active) {
+/******/ if (installedModules[request]) {
+/******/ var parents = installedModules[request].parents;
+/******/ if (parents.indexOf(moduleId) === -1) {
+/******/ parents.push(moduleId);
+/******/ }
+/******/ } else {
+/******/ currentParents = [moduleId];
+/******/ currentChildModule = request;
+/******/ }
+/******/ if (me.children.indexOf(request) === -1) {
+/******/ me.children.push(request);
+/******/ }
+/******/ } else {
+/******/ console.warn(
+/******/ "[HMR] unexpected require(" +
+/******/ request +
+/******/ ") from disposed module " +
+/******/ moduleId
+/******/ );
+/******/ currentParents = [];
+/******/ }
+/******/ return require(request);
+/******/ };
+/******/ var createPropertyDescriptor = function (name) {
+/******/ return {
+/******/ configurable: true,
+/******/ enumerable: true,
+/******/ get: function () {
+/******/ return require[name];
+/******/ },
+/******/ set: function (value) {
+/******/ require[name] = value;
+/******/ }
+/******/ };
+/******/ };
+/******/ for (var name in require) {
+/******/ if (Object.prototype.hasOwnProperty.call(require, name) && name !== "e") {
+/******/ Object.defineProperty(fn, name, createPropertyDescriptor(name));
+/******/ }
+/******/ }
+/******/ fn.e = function (chunkId, fetchPriority) {
+/******/ return trackBlockingPromise(require.e(chunkId, fetchPriority));
+/******/ };
+/******/ return fn;
+/******/ }
+/******/
+/******/ function createModuleHotObject(moduleId, me) {
+/******/ var _main = currentChildModule !== moduleId;
+/******/ var hot = {
+/******/ // private stuff
+/******/ _acceptedDependencies: {},
+/******/ _acceptedErrorHandlers: {},
+/******/ _declinedDependencies: {},
+/******/ _selfAccepted: false,
+/******/ _selfDeclined: false,
+/******/ _selfInvalidated: false,
+/******/ _disposeHandlers: [],
+/******/ _main: _main,
+/******/ _requireSelf: function () {
+/******/ currentParents = me.parents.slice();
+/******/ currentChildModule = _main ? undefined : moduleId;
+/******/ __webpack_require__(moduleId);
+/******/ },
+/******/
+/******/ // Module API
+/******/ active: true,
+/******/ accept: function (dep, callback, errorHandler) {
+/******/ if (dep === undefined) hot._selfAccepted = true;
+/******/ else if (typeof dep === "function") hot._selfAccepted = dep;
+/******/ else if (typeof dep === "object" && dep !== null) {
+/******/ for (var i = 0; i < dep.length; i++) {
+/******/ hot._acceptedDependencies[dep[i]] = callback || function () {};
+/******/ hot._acceptedErrorHandlers[dep[i]] = errorHandler;
+/******/ }
+/******/ } else {
+/******/ hot._acceptedDependencies[dep] = callback || function () {};
+/******/ hot._acceptedErrorHandlers[dep] = errorHandler;
+/******/ }
+/******/ },
+/******/ decline: function (dep) {
+/******/ if (dep === undefined) hot._selfDeclined = true;
+/******/ else if (typeof dep === "object" && dep !== null)
+/******/ for (var i = 0; i < dep.length; i++)
+/******/ hot._declinedDependencies[dep[i]] = true;
+/******/ else hot._declinedDependencies[dep] = true;
+/******/ },
+/******/ dispose: function (callback) {
+/******/ hot._disposeHandlers.push(callback);
+/******/ },
+/******/ addDisposeHandler: function (callback) {
+/******/ hot._disposeHandlers.push(callback);
+/******/ },
+/******/ removeDisposeHandler: function (callback) {
+/******/ var idx = hot._disposeHandlers.indexOf(callback);
+/******/ if (idx >= 0) hot._disposeHandlers.splice(idx, 1);
+/******/ },
+/******/ invalidate: function () {
+/******/ this._selfInvalidated = true;
+/******/ switch (currentStatus) {
+/******/ case "idle":
+/******/ currentUpdateApplyHandlers = [];
+/******/ Object.keys(__webpack_require__.hmrI).forEach(function (key) {
+/******/ __webpack_require__.hmrI[key](
+/******/ moduleId,
+/******/ currentUpdateApplyHandlers
+/******/ );
+/******/ });
+/******/ setStatus("ready");
+/******/ break;
+/******/ case "ready":
+/******/ Object.keys(__webpack_require__.hmrI).forEach(function (key) {
+/******/ __webpack_require__.hmrI[key](
+/******/ moduleId,
+/******/ currentUpdateApplyHandlers
+/******/ );
+/******/ });
+/******/ break;
+/******/ case "prepare":
+/******/ case "check":
+/******/ case "dispose":
+/******/ case "apply":
+/******/ (queuedInvalidatedModules = queuedInvalidatedModules || []).push(
+/******/ moduleId
+/******/ );
+/******/ break;
+/******/ default:
+/******/ // ignore requests in error states
+/******/ break;
+/******/ }
+/******/ },
+/******/
+/******/ // Management API
+/******/ check: hotCheck,
+/******/ apply: hotApply,
+/******/ status: function (l) {
+/******/ if (!l) return currentStatus;
+/******/ registeredStatusHandlers.push(l);
+/******/ },
+/******/ addStatusHandler: function (l) {
+/******/ registeredStatusHandlers.push(l);
+/******/ },
+/******/ removeStatusHandler: function (l) {
+/******/ var idx = registeredStatusHandlers.indexOf(l);
+/******/ if (idx >= 0) registeredStatusHandlers.splice(idx, 1);
+/******/ },
+/******/
+/******/ // inherit from previous dispose call
+/******/ data: currentModuleData[moduleId]
+/******/ };
+/******/ currentChildModule = undefined;
+/******/ return hot;
+/******/ }
+/******/
+/******/ function setStatus(newStatus) {
+/******/ currentStatus = newStatus;
+/******/ var results = [];
+/******/
+/******/ for (var i = 0; i < registeredStatusHandlers.length; i++)
+/******/ results[i] = registeredStatusHandlers[i].call(null, newStatus);
+/******/
+/******/ return Promise.all(results).then(function () {});
+/******/ }
+/******/
+/******/ function unblock() {
+/******/ if (--blockingPromises === 0) {
+/******/ setStatus("ready").then(function () {
+/******/ if (blockingPromises === 0) {
+/******/ var list = blockingPromisesWaiting;
+/******/ blockingPromisesWaiting = [];
+/******/ for (var i = 0; i < list.length; i++) {
+/******/ list[i]();
+/******/ }
+/******/ }
+/******/ });
+/******/ }
+/******/ }
+/******/
+/******/ function trackBlockingPromise(promise) {
+/******/ switch (currentStatus) {
+/******/ case "ready":
+/******/ setStatus("prepare");
+/******/ /* fallthrough */
+/******/ case "prepare":
+/******/ blockingPromises++;
+/******/ promise.then(unblock, unblock);
+/******/ return promise;
+/******/ default:
+/******/ return promise;
+/******/ }
+/******/ }
+/******/
+/******/ function waitForBlockingPromises(fn) {
+/******/ if (blockingPromises === 0) return fn();
+/******/ return new Promise(function (resolve) {
+/******/ blockingPromisesWaiting.push(function () {
+/******/ resolve(fn());
+/******/ });
+/******/ });
+/******/ }
+/******/
+/******/ function hotCheck(applyOnUpdate) {
+/******/ if (currentStatus !== "idle") {
+/******/ throw new Error("check() is only allowed in idle status");
+/******/ }
+/******/ return setStatus("check")
+/******/ .then(__webpack_require__.hmrM)
+/******/ .then(function (update) {
+/******/ if (!update) {
+/******/ return setStatus(applyInvalidatedModules() ? "ready" : "idle").then(
+/******/ function () {
+/******/ return null;
+/******/ }
+/******/ );
+/******/ }
+/******/
+/******/ return setStatus("prepare").then(function () {
+/******/ var updatedModules = [];
+/******/ currentUpdateApplyHandlers = [];
+/******/
+/******/ return Promise.all(
+/******/ Object.keys(__webpack_require__.hmrC).reduce(function (
+/******/ promises,
+/******/ key
+/******/ ) {
+/******/ __webpack_require__.hmrC[key](
+/******/ update.c,
+/******/ update.r,
+/******/ update.m,
+/******/ promises,
+/******/ currentUpdateApplyHandlers,
+/******/ updatedModules,
+/******/ update.css
+/******/ );
+/******/ return promises;
+/******/ }, [])
+/******/ ).then(function () {
+/******/ return waitForBlockingPromises(function () {
+/******/ if (applyOnUpdate) {
+/******/ return internalApply(applyOnUpdate);
+/******/ }
+/******/ return setStatus("ready").then(function () {
+/******/ return updatedModules;
+/******/ });
+/******/ });
+/******/ });
+/******/ });
+/******/ });
+/******/ }
+/******/
+/******/ function hotApply(options) {
+/******/ if (currentStatus !== "ready") {
+/******/ return Promise.resolve().then(function () {
+/******/ throw new Error(
+/******/ "apply() is only allowed in ready status (state: " +
+/******/ currentStatus +
+/******/ ")"
+/******/ );
+/******/ });
+/******/ }
+/******/ return internalApply(options);
+/******/ }
+/******/
+/******/ function internalApply(options) {
+/******/ options = options || {};
+/******/
+/******/ applyInvalidatedModules();
+/******/
+/******/ var results = currentUpdateApplyHandlers.map(function (handler) {
+/******/ return handler(options);
+/******/ });
+/******/ currentUpdateApplyHandlers = undefined;
+/******/
+/******/ var errors = results
+/******/ .map(function (r) {
+/******/ return r.error;
+/******/ })
+/******/ .filter(Boolean);
+/******/
+/******/ if (errors.length > 0) {
+/******/ return setStatus("abort").then(function () {
+/******/ throw errors[0];
+/******/ });
+/******/ }
+/******/
+/******/ // Now in "dispose" phase
+/******/ var disposePromise = setStatus("dispose");
+/******/
+/******/ results.forEach(function (result) {
+/******/ if (result.dispose) result.dispose();
+/******/ });
+/******/
+/******/ // Now in "apply" phase
+/******/ var applyPromise = setStatus("apply");
+/******/
+/******/ var error;
+/******/ var reportError = function (err) {
+/******/ if (!error) error = err;
+/******/ };
+/******/
+/******/ var outdatedModules = [];
+/******/
+/******/ var onAccepted = function () {
+/******/ return Promise.all([disposePromise, applyPromise]).then(function () {
+/******/ // handle errors in accept handlers and self accepted module load
+/******/ if (error) {
+/******/ return setStatus("fail").then(function () {
+/******/ throw error;
+/******/ });
+/******/ }
+/******/
+/******/ if (queuedInvalidatedModules) {
+/******/ return internalApply(options).then(function (list) {
+/******/ outdatedModules.forEach(function (moduleId) {
+/******/ if (list.indexOf(moduleId) < 0) list.push(moduleId);
+/******/ });
+/******/ return list;
+/******/ });
+/******/ }
+/******/
+/******/ return setStatus("idle").then(function () {
+/******/ return outdatedModules;
+/******/ });
+/******/ });
+/******/ };
+/******/
+/******/ return Promise.all(
+/******/ results
+/******/ .filter(function (result) {
+/******/ return result.apply;
+/******/ })
+/******/ .map(function (result) {
+/******/ return result.apply(reportError);
+/******/ })
+/******/ )
+/******/ .then(function (applyResults) {
+/******/ applyResults.forEach(function (modules) {
+/******/ if (modules) {
+/******/ for (var i = 0; i < modules.length; i++) {
+/******/ outdatedModules.push(modules[i]);
+/******/ }
+/******/ }
+/******/ });
+/******/ })
+/******/ .then(onAccepted);
+/******/ }
+/******/
+/******/ function applyInvalidatedModules() {
+/******/ if (queuedInvalidatedModules) {
+/******/ if (!currentUpdateApplyHandlers) currentUpdateApplyHandlers = [];
+/******/ Object.keys(__webpack_require__.hmrI).forEach(function (key) {
+/******/ queuedInvalidatedModules.forEach(function (moduleId) {
+/******/ __webpack_require__.hmrI[key](
+/******/ moduleId,
+/******/ currentUpdateApplyHandlers
+/******/ );
+/******/ });
+/******/ });
+/******/ queuedInvalidatedModules = undefined;
+/******/ return true;
+/******/ }
+/******/ }
+/******/ })();
+/******/
+/******/ /* webpack/runtime/publicPath */
+/******/ (() => {
+/******/ __webpack_require__.p = "/static/assets/";
+/******/ })();
+/******/
+/******/ /* webpack/runtime/react refresh */
+/******/ (() => {
+/******/ const setup = (moduleId) => {
+/******/ const refresh = {
+/******/ moduleId: moduleId,
+/******/ register: (type, id) => {
+/******/ const typeId = moduleId + ' ' + id;
+/******/ refresh.runtime.register(type, typeId);
+/******/ },
+/******/ signature: () => (refresh.runtime.createSignatureFunctionForTransform()),
+/******/ runtime: {
+/******/ createSignatureFunctionForTransform: () => ((type) => (type)),
+/******/ register: x => {}
+/******/ },
+/******/ };
+/******/ return refresh;
+/******/ };
+/******/
+/******/ __webpack_require__.i.push((options) => {
+/******/ const originalFactory = options.factory;
+/******/ options.factory = function(moduleObject, moduleExports, webpackRequire) {
+/******/ const hotRequire = (request) => (webpackRequire(request));
+/******/ const createPropertyDescriptor = (name) => {
+/******/ return {
+/******/ configurable: true,
+/******/ enumerable: true,
+/******/ get: () => (webpackRequire[name]),
+/******/ set: (value) => {
+/******/ webpackRequire[name] = value;
+/******/ },
+/******/ };
+/******/ };
+/******/ for (const name in webpackRequire) {
+/******/ if (name === "$Refresh$") continue;
+/******/ if (Object.prototype.hasOwnProperty.call(webpackRequire, name)) {
+/******/ Object.defineProperty(hotRequire, name, createPropertyDescriptor(name));
+/******/ }
+/******/ }
+/******/ hotRequire.$Refresh$ = setup(options.id);
+/******/ originalFactory.call(this, moduleObject, moduleExports, hotRequire);
+/******/ };
+/******/ });
+/******/ })();
+/******/
+/******/ /* webpack/runtime/consumes */
+/******/ (() => {
+/******/ var parseVersion = (str) => {
+/******/ // see webpack/lib/util/semver.js for original code
+/******/ var p=p=>{return p.split(".").map(p=>{return+p==p?+p:p})},n=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(str),r=n[1]?p(n[1]):[];return n[2]&&(r.length++,r.push.apply(r,p(n[2]))),n[3]&&(r.push([]),r.push.apply(r,p(n[3]))),r;
+/******/ }
+/******/ var versionLt = (a, b) => {
+/******/ // see webpack/lib/util/semver.js for original code
+/******/ a=parseVersion(a),b=parseVersion(b);for(var r=0;;){if(r>=a.length)return r<b.length&&"u"!=(typeof b[r])[0];var e=a[r],n=(typeof e)[0];if(r>=b.length)return"u"==n;var t=b[r],f=(typeof t)[0];if(n!=f)return"o"==n&&"n"==f||("s"==f||"u"==n);if("o"!=n&&"u"!=n&&e!=t)return e<t;r++}
+/******/ }
+/******/ var rangeToString = (range) => {
+/******/ // see webpack/lib/util/semver.js for original code
+/******/ var r=range[0],n="";if(1===range.length)return"*";if(r+.5){n+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var e=1,a=1;a<range.length;a++){e--,n+="u"==(typeof(t=range[a]))[0]?"-":(e>0?".":"")+(e=2,t)}return n}var g=[];for(a=1;a<range.length;a++){var t=range[a];g.push(0===t?"not("+o()+")":1===t?"("+o()+" || "+o()+")":2===t?g.pop()+" "+g.pop():rangeToString(t))}return o();function o(){return g.pop().replace(/^\((.+)\)$/,"$1")}
+/******/ }
+/******/ var satisfy = (range, version) => {
+/******/ // see webpack/lib/util/semver.js for original code
+/******/ if(0 in range){version=parseVersion(version);var e=range[0],r=e<0;r&&(e=-e-1);for(var n=0,i=1,a=!0;;i++,n++){var f,s,g=i<range.length?(typeof range[i])[0]:"";if(n>=version.length||"o"==(s=(typeof(f=version[n]))[0]))return!a||("u"==g?i>e&&!r:""==g!=r);if("u"==s){if(!a||"u"!=g)return!1}else if(a)if(g==s)if(i<=e){if(f!=range[i])return!1}else{if(r?f>range[i]:f<range[i])return!1;f!=range[i]&&(a=!1)}else if("s"!=g&&"n"!=g){if(r||i<=e)return!1;a=!1,i--}else{if(i<=e||s<g!=r)return!1; [...]
+/******/ }
+/******/ var exists = (scope, key) => {
+/******/ return scope && __webpack_require__.o(scope, key);
+/******/ }
+/******/ var get = (entry) => {
+/******/ entry.loaded = 1;
+/******/ return entry.get()
+/******/ };
+/******/ var eagerOnly = (versions) => {
+/******/ return Object.keys(versions).reduce((filtered, version) => {
+/******/ if (versions[version].eager) {
+/******/ filtered[version] = versions[version];
+/******/ }
+/******/ return filtered;
+/******/ }, {});
+/******/ };
+/******/ var findLatestVersion = (scope, key, eager) => {
+/******/ var versions = eager ? eagerOnly(scope[key]) : scope[key];
+/******/ var key = Object.keys(versions).reduce((a, b) => {
+/******/ return !a || versionLt(a, b) ? b : a;
+/******/ }, 0);
+/******/ return key && versions[key];
+/******/ };
+/******/ var findSatisfyingVersion = (scope, key, requiredVersion, eager) => {
+/******/ var versions = eager ? eagerOnly(scope[key]) : scope[key];
+/******/ var key = Object.keys(versions).reduce((a, b) => {
+/******/ if (!satisfy(requiredVersion, b)) return a;
+/******/ return !a || versionLt(a, b) ? b : a;
+/******/ }, 0);
+/******/ return key && versions[key]
+/******/ };
+/******/ var findSingletonVersionKey = (scope, key, eager) => {
+/******/ var versions = eager ? eagerOnly(scope[key]) : scope[key];
+/******/ return Object.keys(versions).reduce((a, b) => {
+/******/ return !a || (!versions[a].loaded && versionLt(a, b)) ? b : a;
+/******/ }, 0);
+/******/ };
+/******/ var getInvalidSingletonVersionMessage = (scope, key, version, requiredVersion) => {
+/******/ return "Unsatisfied version " + version + " from " + (version && scope[key][version].from) + " of shared singleton module " + key + " (required " + rangeToString(requiredVersion) + ")"
+/******/ };
+/******/ var getInvalidVersionMessage = (scope, scopeName, key, requiredVersion, eager) => {
+/******/ var versions = scope[key];
+/******/ return "No satisfying version (" + rangeToString(requiredVersion) + ")" + (eager ? " for eager consumption" : "") + " of shared module " + key + " found in shared scope " + scopeName + ".\n" +
+/******/ "Available versions: " + Object.keys(versions).map((key) => {
+/******/ return key + " from " + versions[key].from;
+/******/ }).join(", ");
+/******/ };
+/******/ var fail = (msg) => {
+/******/ throw new Error(msg);
+/******/ }
+/******/ var failAsNotExist = (scopeName, key) => {
+/******/ return fail("Shared module " + key + " doesn't exist in shared scope " + scopeName);
+/******/ }
+/******/ var warn = /*#__PURE__*/ (msg) => {
+/******/ if (typeof console !== "undefined" && console.warn) console.warn(msg);
+/******/ };
+/******/ var init = (fn) => (function(scopeName, key, eager, c, d) {
+/******/ var promise = __webpack_require__.I(scopeName);
+/******/ if (promise && promise.then && !eager) {
+/******/ return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], key, false, c, d));
+/******/ }
+/******/ return fn(scopeName, __webpack_require__.S[scopeName], key, eager, c, d);
+/******/ });
+/******/
+/******/ var useFallback = (scopeName, key, fallback) => {
+/******/ return fallback ? fallback() : failAsNotExist(scopeName, key);
+/******/ }
+/******/ var load = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ return get(findLatestVersion(scope, key, eager));
+/******/ });
+/******/ var loadVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
+/******/ if (satisfyingVersion) return get(satisfyingVersion);
+/******/ warn(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager))
+/******/ return get(findLatestVersion(scope, key, eager));
+/******/ });
+/******/ var loadStrictVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ var satisfyingVersion = findSatisfyingVersion(scope, key, requiredVersion, eager);
+/******/ if (satisfyingVersion) return get(satisfyingVersion);
+/******/ if (fallback) return fallback();
+/******/ fail(getInvalidVersionMessage(scope, scopeName, key, requiredVersion, eager));
+/******/ });
+/******/ var loadSingleton = /*#__PURE__*/ init((scopeName, scope, key, eager, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ var version = findSingletonVersionKey(scope, key, eager);
+/******/ return get(scope[key][version]);
+/******/ });
+/******/ var loadSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ var version = findSingletonVersionKey(scope, key, eager);
+/******/ if (!satisfy(requiredVersion, version)) {
+/******/ warn(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
+/******/ }
+/******/ return get(scope[key][version]);
+/******/ });
+/******/ var loadStrictSingletonVersion = /*#__PURE__*/ init((scopeName, scope, key, eager, requiredVersion, fallback) => {
+/******/ if (!exists(scope, key)) return useFallback(scopeName, key, fallback);
+/******/ var version = findSingletonVersionKey(scope, key, eager);
+/******/ if (!satisfy(requiredVersion, version)) {
+/******/ fail(getInvalidSingletonVersionMessage(scope, key, version, requiredVersion));
+/******/ }
+/******/ return get(scope[key][version]);
+/******/ });
+/******/ var installedModules = {};
+/******/ var moduleToHandlerMapping = {
+/******/ "webpack/sharing/consume/default/react-dom/react-dom": () => (loadSingletonVersion("default", "react-dom", true, [1,17,0,2], () => (() => (__webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js"))))),
+/******/ "webpack/sharing/consume/default/react/react": () => (loadSingletonVersion("default", "react", true, [1,17,0,2], () => (() => (__webpack_require__(/*! react */ "./node_modules/react/index.js")))))
+/******/ };
+/******/ var initialConsumes = ["webpack/sharing/consume/default/react-dom/react-dom","webpack/sharing/consume/default/react/react"];
+/******/ initialConsumes.forEach((id) => {
+/******/ __webpack_require__.m[id] = (module) => {
+/******/ // Handle case when module is used sync
+/******/ installedModules[id] = 0;
+/******/ delete __webpack_require__.c[id];
+/******/ var factory = moduleToHandlerMapping[id]();
+/******/ if(typeof factory !== "function") throw new Error("Shared module is not available for eager consumption: " + id);
+/******/ module.exports = factory();
+/******/ }
+/******/ });
+/******/ // no chunk loading of consumes
+/******/ })();
+/******/
+/******/ /* webpack/runtime/jsonp chunk loading */
+/******/ (() => {
+/******/ // no baseURI
+/******/
+/******/ // object to store loaded and loading chunks
+/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
+/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
+/******/ var installedChunks = __webpack_require__.hmrS_jsonp = __webpack_require__.hmrS_jsonp || {
+/******/ "service-worker": 0,
+/******/ "webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef": 0
+/******/ };
+/******/
+/******/ // no chunk on demand loading
+/******/
+/******/ // no prefetching
+/******/
+/******/ // no preloaded
+/******/
+/******/ var currentUpdatedModulesList;
+/******/ var waitingUpdateResolves = {};
+/******/ function loadUpdateChunk(chunkId, updatedModulesList) {
+/******/ currentUpdatedModulesList = updatedModulesList;
+/******/ return new Promise((resolve, reject) => {
+/******/ waitingUpdateResolves[chunkId] = resolve;
+/******/ // start update chunk loading
+/******/ var url = __webpack_require__.p + __webpack_require__.hu(chunkId);
+/******/ // create error before stack unwound to get useful stacktrace later
+/******/ var error = new Error();
+/******/ var loadingEnded = (event) => {
+/******/ if(waitingUpdateResolves[chunkId]) {
+/******/ waitingUpdateResolves[chunkId] = undefined
+/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
+/******/ var realSrc = event && event.target && event.target.src;
+/******/ error.message = 'Loading hot update chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
+/******/ error.name = 'ChunkLoadError';
+/******/ error.type = errorType;
+/******/ error.request = realSrc;
+/******/ reject(error);
+/******/ }
+/******/ };
+/******/ __webpack_require__.l(url, loadingEnded);
+/******/ });
+/******/ }
+/******/
+/******/ globalThis["webpackHotUpdatesuperset"] = (chunkId, moreModules, runtime) => {
+/******/ for(var moduleId in moreModules) {
+/******/ if(__webpack_require__.o(moreModules, moduleId)) {
+/******/ currentUpdate[moduleId] = moreModules[moduleId];
+/******/ if(currentUpdatedModulesList) currentUpdatedModulesList.push(moduleId);
+/******/ }
+/******/ }
+/******/ if(runtime) currentUpdateRuntime.push(runtime);
+/******/ if(waitingUpdateResolves[chunkId]) {
+/******/ waitingUpdateResolves[chunkId]();
+/******/ waitingUpdateResolves[chunkId] = undefined;
+/******/ }
+/******/ };
+/******/
+/******/ var currentUpdateChunks;
+/******/ var currentUpdate;
+/******/ var currentUpdateRemovedChunks;
+/******/ var currentUpdateRuntime;
+/******/ function applyHandler(options) {
+/******/ if (__webpack_require__.f) delete __webpack_require__.f.jsonpHmr;
+/******/ currentUpdateChunks = undefined;
+/******/ function getAffectedModuleEffects(updateModuleId) {
+/******/ var outdatedModules = [updateModuleId];
+/******/ var outdatedDependencies = {};
+/******/
+/******/ var queue = outdatedModules.map(function (id) {
+/******/ return {
+/******/ chain: [id],
+/******/ id: id
+/******/ };
+/******/ });
+/******/ while (queue.length > 0) {
+/******/ var queueItem = queue.pop();
+/******/ var moduleId = queueItem.id;
+/******/ var chain = queueItem.chain;
+/******/ var module = __webpack_require__.c[moduleId];
+/******/ if (
+/******/ !module ||
+/******/ (module.hot._selfAccepted && !module.hot._selfInvalidated)
+/******/ )
+/******/ continue;
+/******/ if (module.hot._selfDeclined) {
+/******/ return {
+/******/ type: "self-declined",
+/******/ chain: chain,
+/******/ moduleId: moduleId
+/******/ };
+/******/ }
+/******/ if (module.hot._main) {
+/******/ return {
+/******/ type: "unaccepted",
+/******/ chain: chain,
+/******/ moduleId: moduleId
+/******/ };
+/******/ }
+/******/ for (var i = 0; i < module.parents.length; i++) {
+/******/ var parentId = module.parents[i];
+/******/ var parent = __webpack_require__.c[parentId];
+/******/ if (!parent) continue;
+/******/ if (parent.hot._declinedDependencies[moduleId]) {
+/******/ return {
+/******/ type: "declined",
+/******/ chain: chain.concat([parentId]),
+/******/ moduleId: moduleId,
+/******/ parentId: parentId
+/******/ };
+/******/ }
+/******/ if (outdatedModules.indexOf(parentId) !== -1) continue;
+/******/ if (parent.hot._acceptedDependencies[moduleId]) {
+/******/ if (!outdatedDependencies[parentId])
+/******/ outdatedDependencies[parentId] = [];
+/******/ addAllToSet(outdatedDependencies[parentId], [moduleId]);
+/******/ continue;
+/******/ }
+/******/ delete outdatedDependencies[parentId];
+/******/ outdatedModules.push(parentId);
+/******/ queue.push({
+/******/ chain: chain.concat([parentId]),
+/******/ id: parentId
+/******/ });
+/******/ }
+/******/ }
+/******/
+/******/ return {
+/******/ type: "accepted",
+/******/ moduleId: updateModuleId,
+/******/ outdatedModules: outdatedModules,
+/******/ outdatedDependencies: outdatedDependencies
+/******/ };
+/******/ }
+/******/
+/******/ function addAllToSet(a, b) {
+/******/ for (var i = 0; i < b.length; i++) {
+/******/ var item = b[i];
+/******/ if (a.indexOf(item) === -1) a.push(item);
+/******/ }
+/******/ }
+/******/
+/******/ // at begin all updates modules are outdated
+/******/ // the "outdated" status can propagate to parents if they don't accept the children
+/******/ var outdatedDependencies = {};
+/******/ var outdatedModules = [];
+/******/ var appliedUpdate = {};
+/******/
+/******/ var warnUnexpectedRequire = function warnUnexpectedRequire(module) {
+/******/ console.warn(
+/******/ "[HMR] unexpected require(" + module.id + ") to disposed module"
+/******/ );
+/******/ };
+/******/
+/******/ for (var moduleId in currentUpdate) {
+/******/ if (__webpack_require__.o(currentUpdate, moduleId)) {
+/******/ var newModuleFactory = currentUpdate[moduleId];
+/******/ var result = newModuleFactory
+/******/ ? getAffectedModuleEffects(moduleId)
+/******/ : {
+/******/ type: "disposed",
+/******/ moduleId: moduleId
+/******/ };
+/******/ /** @type {Error|false} */
+/******/ var abortError = false;
+/******/ var doApply = false;
+/******/ var doDispose = false;
+/******/ var chainInfo = "";
+/******/ if (result.chain) {
+/******/ chainInfo = "\nUpdate propagation: " + result.chain.join(" -> ");
+/******/ }
+/******/ switch (result.type) {
+/******/ case "self-declined":
+/******/ if (options.onDeclined) options.onDeclined(result);
+/******/ if (!options.ignoreDeclined)
+/******/ abortError = new Error(
+/******/ "Aborted because of self decline: " +
+/******/ result.moduleId +
+/******/ chainInfo
+/******/ );
+/******/ break;
+/******/ case "declined":
+/******/ if (options.onDeclined) options.onDeclined(result);
+/******/ if (!options.ignoreDeclined)
+/******/ abortError = new Error(
+/******/ "Aborted because of declined dependency: " +
+/******/ result.moduleId +
+/******/ " in " +
+/******/ result.parentId +
+/******/ chainInfo
+/******/ );
+/******/ break;
+/******/ case "unaccepted":
+/******/ if (options.onUnaccepted) options.onUnaccepted(result);
+/******/ if (!options.ignoreUnaccepted)
+/******/ abortError = new Error(
+/******/ "Aborted because " + moduleId + " is not accepted" + chainInfo
+/******/ );
+/******/ break;
+/******/ case "accepted":
+/******/ if (options.onAccepted) options.onAccepted(result);
+/******/ doApply = true;
+/******/ break;
+/******/ case "disposed":
+/******/ if (options.onDisposed) options.onDisposed(result);
+/******/ doDispose = true;
+/******/ break;
+/******/ default:
+/******/ throw new Error("Unexception type " + result.type);
+/******/ }
+/******/ if (abortError) {
+/******/ return {
+/******/ error: abortError
+/******/ };
+/******/ }
+/******/ if (doApply) {
+/******/ appliedUpdate[moduleId] = newModuleFactory;
+/******/ addAllToSet(outdatedModules, result.outdatedModules);
+/******/ for (moduleId in result.outdatedDependencies) {
+/******/ if (__webpack_require__.o(result.outdatedDependencies, moduleId)) {
+/******/ if (!outdatedDependencies[moduleId])
+/******/ outdatedDependencies[moduleId] = [];
+/******/ addAllToSet(
+/******/ outdatedDependencies[moduleId],
+/******/ result.outdatedDependencies[moduleId]
+/******/ );
+/******/ }
+/******/ }
+/******/ }
+/******/ if (doDispose) {
+/******/ addAllToSet(outdatedModules, [result.moduleId]);
+/******/ appliedUpdate[moduleId] = warnUnexpectedRequire;
+/******/ }
+/******/ }
+/******/ }
+/******/ currentUpdate = undefined;
+/******/
+/******/ // Store self accepted outdated modules to require them later by the module system
+/******/ var outdatedSelfAcceptedModules = [];
+/******/ for (var j = 0; j < outdatedModules.length; j++) {
+/******/ var outdatedModuleId = outdatedModules[j];
+/******/ var module = __webpack_require__.c[outdatedModuleId];
+/******/ if (
+/******/ module &&
+/******/ (module.hot._selfAccepted || module.hot._main) &&
+/******/ // removed self-accepted modules should not be required
+/******/ appliedUpdate[outdatedModuleId] !== warnUnexpectedRequire &&
+/******/ // when called invalidate self-accepting is not possible
+/******/ !module.hot._selfInvalidated
+/******/ ) {
+/******/ outdatedSelfAcceptedModules.push({
+/******/ module: outdatedModuleId,
+/******/ require: module.hot._requireSelf,
+/******/ errorHandler: module.hot._selfAccepted
+/******/ });
+/******/ }
+/******/ }
+/******/
+/******/ var moduleOutdatedDependencies;
+/******/
+/******/ return {
+/******/ dispose: function () {
+/******/ currentUpdateRemovedChunks.forEach(function (chunkId) {
+/******/ delete installedChunks[chunkId];
+/******/ });
+/******/ currentUpdateRemovedChunks = undefined;
+/******/
+/******/ var idx;
+/******/ var queue = outdatedModules.slice();
+/******/ while (queue.length > 0) {
+/******/ var moduleId = queue.pop();
+/******/ var module = __webpack_require__.c[moduleId];
+/******/ if (!module) continue;
+/******/
+/******/ var data = {};
+/******/
+/******/ // Call dispose handlers
+/******/ var disposeHandlers = module.hot._disposeHandlers;
+/******/ for (j = 0; j < disposeHandlers.length; j++) {
+/******/ disposeHandlers[j].call(null, data);
+/******/ }
+/******/ __webpack_require__.hmrD[moduleId] = data;
+/******/
+/******/ // disable module (this disables requires from this module)
+/******/ module.hot.active = false;
+/******/
+/******/ // remove module from cache
+/******/ delete __webpack_require__.c[moduleId];
+/******/
+/******/ // when disposing there is no need to call dispose handler
+/******/ delete outdatedDependencies[moduleId];
+/******/
+/******/ // remove "parents" references from all children
+/******/ for (j = 0; j < module.children.length; j++) {
+/******/ var child = __webpack_require__.c[module.children[j]];
+/******/ if (!child) continue;
+/******/ idx = child.parents.indexOf(moduleId);
+/******/ if (idx >= 0) {
+/******/ child.parents.splice(idx, 1);
+/******/ }
+/******/ }
+/******/ }
+/******/
+/******/ // remove outdated dependency from module children
+/******/ var dependency;
+/******/ for (var outdatedModuleId in outdatedDependencies) {
+/******/ if (__webpack_require__.o(outdatedDependencies, outdatedModuleId)) {
+/******/ module = __webpack_require__.c[outdatedModuleId];
+/******/ if (module) {
+/******/ moduleOutdatedDependencies =
+/******/ outdatedDependencies[outdatedModuleId];
+/******/ for (j = 0; j < moduleOutdatedDependencies.length; j++) {
+/******/ dependency = moduleOutdatedDependencies[j];
+/******/ idx = module.children.indexOf(dependency);
+/******/ if (idx >= 0) module.children.splice(idx, 1);
+/******/ }
+/******/ }
+/******/ }
+/******/ }
+/******/ },
+/******/ apply: function (reportError) {
+/******/ var acceptPromises = [];
+/******/ // insert new code
+/******/ for (var updateModuleId in appliedUpdate) {
+/******/ if (__webpack_require__.o(appliedUpdate, updateModuleId)) {
+/******/ __webpack_require__.m[updateModuleId] = appliedUpdate[updateModuleId];
+/******/ }
+/******/ }
+/******/
+/******/ // run new runtime modules
+/******/ for (var i = 0; i < currentUpdateRuntime.length; i++) {
+/******/ currentUpdateRuntime[i](__webpack_require__);
+/******/ }
+/******/
+/******/ // call accept handlers
+/******/ for (var outdatedModuleId in outdatedDependencies) {
+/******/ if (__webpack_require__.o(outdatedDependencies, outdatedModuleId)) {
+/******/ var module = __webpack_require__.c[outdatedModuleId];
+/******/ if (module) {
+/******/ moduleOutdatedDependencies =
+/******/ outdatedDependencies[outdatedModuleId];
+/******/ var callbacks = [];
+/******/ var errorHandlers = [];
+/******/ var dependenciesForCallbacks = [];
+/******/ for (var j = 0; j < moduleOutdatedDependencies.length; j++) {
+/******/ var dependency = moduleOutdatedDependencies[j];
+/******/ var acceptCallback =
+/******/ module.hot._acceptedDependencies[dependency];
+/******/ var errorHandler =
+/******/ module.hot._acceptedErrorHandlers[dependency];
+/******/ if (acceptCallback) {
+/******/ if (callbacks.indexOf(acceptCallback) !== -1) continue;
+/******/ callbacks.push(acceptCallback);
+/******/ errorHandlers.push(errorHandler);
+/******/ dependenciesForCallbacks.push(dependency);
+/******/ }
+/******/ }
+/******/ for (var k = 0; k < callbacks.length; k++) {
+/******/ var result;
+/******/ try {
+/******/ result = callbacks[k].call(null, moduleOutdatedDependencies);
+/******/ } catch (err) {
+/******/ if (typeof errorHandlers[k] === "function") {
+/******/ try {
+/******/ errorHandlers[k](err, {
+/******/ moduleId: outdatedModuleId,
+/******/ dependencyId: dependenciesForCallbacks[k]
+/******/ });
+/******/ } catch (err2) {
+/******/ if (options.onErrored) {
+/******/ options.onErrored({
+/******/ type: "accept-error-handler-errored",
+/******/ moduleId: outdatedModuleId,
+/******/ dependencyId: dependenciesForCallbacks[k],
+/******/ error: err2,
+/******/ originalError: err
+/******/ });
+/******/ }
+/******/ if (!options.ignoreErrored) {
+/******/ reportError(err2);
+/******/ reportError(err);
+/******/ }
+/******/ }
+/******/ } else {
+/******/ if (options.onErrored) {
+/******/ options.onErrored({
+/******/ type: "accept-errored",
+/******/ moduleId: outdatedModuleId,
+/******/ dependencyId: dependenciesForCallbacks[k],
+/******/ error: err
+/******/ });
+/******/ }
+/******/ if (!options.ignoreErrored) {
+/******/ reportError(err);
+/******/ }
+/******/ }
+/******/ }
+/******/ if (result && typeof result.then === "function") {
+/******/ acceptPromises.push(result);
+/******/ }
+/******/ }
+/******/ }
+/******/ }
+/******/ }
+/******/
+/******/ var onAccepted = function () {
+/******/ // Load self accepted modules
+/******/ for (var o = 0; o < outdatedSelfAcceptedModules.length; o++) {
+/******/ var item = outdatedSelfAcceptedModules[o];
+/******/ var moduleId = item.module;
+/******/ try {
+/******/ item.require(moduleId);
+/******/ } catch (err) {
+/******/ if (typeof item.errorHandler === "function") {
+/******/ try {
+/******/ item.errorHandler(err, {
+/******/ moduleId: moduleId,
+/******/ module: __webpack_require__.c[moduleId]
+/******/ });
+/******/ } catch (err1) {
+/******/ if (options.onErrored) {
+/******/ options.onErrored({
+/******/ type: "self-accept-error-handler-errored",
+/******/ moduleId: moduleId,
+/******/ error: err1,
+/******/ originalError: err
+/******/ });
+/******/ }
+/******/ if (!options.ignoreErrored) {
+/******/ reportError(err1);
+/******/ reportError(err);
+/******/ }
+/******/ }
+/******/ } else {
+/******/ if (options.onErrored) {
+/******/ options.onErrored({
+/******/ type: "self-accept-errored",
+/******/ moduleId: moduleId,
+/******/ error: err
+/******/ });
+/******/ }
+/******/ if (!options.ignoreErrored) {
+/******/ reportError(err);
+/******/ }
+/******/ }
+/******/ }
+/******/ }
+/******/ };
+/******/
+/******/ return Promise.all(acceptPromises)
+/******/ .then(onAccepted)
+/******/ .then(function () {
+/******/ return outdatedModules;
+/******/ });
+/******/ }
+/******/ };
+/******/ }
+/******/ __webpack_require__.hmrI.jsonp = function (moduleId, applyHandlers) {
+/******/ if (!currentUpdate) {
+/******/ currentUpdate = {};
+/******/ currentUpdateRuntime = [];
+/******/ currentUpdateRemovedChunks = [];
+/******/ applyHandlers.push(applyHandler);
+/******/ }
+/******/ if (!__webpack_require__.o(currentUpdate, moduleId)) {
+/******/ currentUpdate[moduleId] = __webpack_require__.m[moduleId];
+/******/ }
+/******/ };
+/******/ __webpack_require__.hmrC.jsonp = function (
+/******/ chunkIds,
+/******/ removedChunks,
+/******/ removedModules,
+/******/ promises,
+/******/ applyHandlers,
+/******/ updatedModulesList
+/******/ ) {
+/******/ applyHandlers.push(applyHandler);
+/******/ currentUpdateChunks = {};
+/******/ currentUpdateRemovedChunks = removedChunks;
+/******/ currentUpdate = removedModules.reduce(function (obj, key) {
+/******/ obj[key] = false;
+/******/ return obj;
+/******/ }, {});
+/******/ currentUpdateRuntime = [];
+/******/ chunkIds.forEach(function (chunkId) {
+/******/ if (
+/******/ __webpack_require__.o(installedChunks, chunkId) &&
+/******/ installedChunks[chunkId] !== undefined
+/******/ ) {
+/******/ promises.push(loadUpdateChunk(chunkId, updatedModulesList));
+/******/ currentUpdateChunks[chunkId] = true;
+/******/ } else {
+/******/ currentUpdateChunks[chunkId] = false;
+/******/ }
+/******/ });
+/******/ if (__webpack_require__.f) {
+/******/ __webpack_require__.f.jsonpHmr = function (chunkId, promises) {
+/******/ if (
+/******/ currentUpdateChunks &&
+/******/ __webpack_require__.o(currentUpdateChunks, chunkId) &&
+/******/ !currentUpdateChunks[chunkId]
+/******/ ) {
+/******/ promises.push(loadUpdateChunk(chunkId));
+/******/ currentUpdateChunks[chunkId] = true;
+/******/ }
+/******/ };
+/******/ }
+/******/ };
+/******/
+/******/ __webpack_require__.hmrM = () => {
+/******/ if (typeof fetch === "undefined") throw new Error("No browser support: need fetch API");
+/******/ return fetch(__webpack_require__.p + __webpack_require__.hmrF()).then((response) => {
+/******/ if(response.status === 404) return; // no update available
+/******/ if(!response.ok) throw new Error("Failed to fetch update manifest " + response.statusText);
+/******/ return response.json();
+/******/ });
+/******/ };
+/******/
+/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
+/******/
+/******/ // install a JSONP callback for chunk loading
+/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
+/******/ var [chunkIds, moreModules, runtime] = data;
+/******/ // add "moreModules" to the modules object,
+/******/ // then flag all "chunkIds" as loaded and fire callback
+/******/ var moduleId, chunkId, i = 0;
+/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
+/******/ for(moduleId in moreModules) {
+/******/ if(__webpack_require__.o(moreModules, moduleId)) {
+/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
+/******/ }
+/******/ }
+/******/ if(runtime) var result = runtime(__webpack_require__);
+/******/ }
+/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
+/******/ for(;i < chunkIds.length; i++) {
+/******/ chunkId = chunkIds[i];
+/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
+/******/ installedChunks[chunkId][0]();
+/******/ }
+/******/ installedChunks[chunkId] = 0;
+/******/ }
+/******/ return __webpack_require__.O(result);
+/******/ }
+/******/
+/******/ var chunkLoadingGlobal = globalThis["webpackChunksuperset"] = globalThis["webpackChunksuperset"] || [];
+/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
+/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
+/******/ })();
+/******/
+/************************************************************************/
+/******/
+/******/ // module cache are used so entry inlining is disabled
+/******/ // startup
+/******/ // Load entry module and return exports
+/******/ __webpack_require__.O(undefined, ["vendors","vendors-node_modules_rc-component_color-picker_es_index_js-node_modules_rc-component_mutate-o-484854","webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef"], () => (__webpack_require__("./node_modules/@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js")))
+/******/ __webpack_require__.O(undefined, ["vendors","vendors-node_modules_rc-component_color-picker_es_index_js-node_modules_rc-component_mutate-o-484854","webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef"], () => (__webpack_require__("./node_modules/webpack-dev-server/client/index.js?protocol=ws%3A&hostname=0.0.0.0&port=0&pathname=%2Fws&logging=info&overlay=%7B%22errors%22%3Atrue%2C%22warnings%22%3Afalse%2C%22runtimeErrors%22%3A%22er [...]
+/******/ __webpack_require__.O(undefined, ["vendors","vendors-node_modules_rc-component_color-picker_es_index_js-node_modules_rc-component_mutate-o-484854","webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef"], () => (__webpack_require__("./node_modules/webpack/hot/only-dev-server.js")))
+/******/ __webpack_require__.O(undefined, ["vendors","vendors-node_modules_rc-component_color-picker_es_index_js-node_modules_rc-component_mutate-o-484854","webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef"], () => (__webpack_require__("./node_modules/@pmmmwh/react-refresh-webpack-plugin/client/ErrorOverlayEntry.js")))
+/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["vendors","vendors-node_modules_rc-component_color-picker_es_index_js-node_modules_rc-component_mutate-o-484854","webpack_sharing_consume_default_react-dom_react-dom-webpack_sharing_consume_default_react_rea-153fef"], () => (__webpack_require__("./src/service-worker.ts")))
+/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
+/******/
+/******/ })()
+;
\ No newline at end of file