diff --git a/README.md b/README.md
index e0bb2915399..c23fd3fdbe3 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ under the License.
# Superset
[](https://opensource.org/license/apache-2-0)
-[](https://github.com/apache/superset/tree/latest)
+[](https://github.com/apache/superset/releases/latest)
[](https://github.com/apache/superset/actions)
[](https://badge.fury.io/py/apache-superset)
[](https://codecov.io/github/apache/superset)
diff --git a/null_byte.csv b/null_byte.csv
deleted file mode 100644
index 55132aaa639..00000000000
Binary files a/null_byte.csv and /dev/null differ
diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json
index 64da86afdcc..01763f3078a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/package.json
+++ b/superset-frontend/plugins/plugin-chart-echarts/package.json
@@ -24,10 +24,10 @@
"lib"
],
"dependencies": {
+ "@types/react-redux": "^7.1.10",
"d3-array": "^1.2.0",
- "lodash": "^4.17.21",
"dayjs": "^1.11.13",
- "@types/react-redux": "^7.1.10"
+ "lodash": "^4.17.21"
},
"peerDependencies": {
"@superset-ui/chart-controls": "*",
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index ea9c63c7f25..3015010d05f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -156,9 +156,15 @@ export function sortAndFilterSeries(
case SortSeriesType.Avg:
aggregator = name => ({ name, value: meanBy(rows, name) });
break;
- default:
- aggregator = name => ({ name, value: name.toLowerCase() });
- break;
+ default: {
+ const collator = new Intl.Collator(undefined, {
+ numeric: true,
+ sensitivity: 'base',
+ });
+ return seriesNames.sort((a, b) =>
+ sortSeriesAscending ? collator.compare(a, b) : collator.compare(b, a),
+ );
+ }
}
const sortedValues = seriesNames.map(aggregator);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
index 323f576fd09..5f54c63f94d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts
@@ -68,6 +68,39 @@ const sortData: DataRecord[] = [
{ my_x_axis: null, x: 4, y: 3, z: 7 },
];
+const sortDataWithNumbers: DataRecord[] = [
+ {
+ my_x_axis: 'my_axis',
+ '9. September': 6,
+ 6: 1,
+ '11. November': 8,
+ 8: 2,
+ '10. October': 1,
+ 10: 4,
+ '3. March': 2,
+ '8. August': 6,
+ 2: 1,
+ 12: 3,
+ 9: 1,
+ '1. January': 1,
+ '4. April': 12,
+ '2. February': 9,
+ 5: 4,
+ 3: 1,
+ 11: 2,
+ '12. December': 4,
+ 1: 7,
+ '6. June': 1,
+ 4: 5,
+ 7: 2,
+ c: 0,
+ '7. July': 2,
+ d: 0,
+ '5. May': 4,
+ a: 1,
+ },
+];
+
const totalStackedValues = [3, 15, 14];
test('sortRows by name ascending', () => {
@@ -289,6 +322,84 @@ test('sortAndFilterSeries by name descending', () => {
sortAndFilterSeries(sortData, 'my_x_axis', [], SortSeriesType.Name, false),
).toEqual(['z', 'y', 'x']);
});
+test('sortAndFilterSeries by name with numbers asc', () => {
+ expect(
+ sortAndFilterSeries(
+ sortDataWithNumbers,
+ 'my_x_axis',
+ [],
+ SortSeriesType.Name,
+ true,
+ ),
+ ).toEqual([
+ '1',
+ '1. January',
+ '2',
+ '2. February',
+ '3',
+ '3. March',
+ '4',
+ '4. April',
+ '5',
+ '5. May',
+ '6',
+ '6. June',
+ '7',
+ '7. July',
+ '8',
+ '8. August',
+ '9',
+ '9. September',
+ '10',
+ '10. October',
+ '11',
+ '11. November',
+ '12',
+ '12. December',
+ 'a',
+ 'c',
+ 'd',
+ ]);
+});
+test('sortAndFilterSeries by name with numbers desc', () => {
+ expect(
+ sortAndFilterSeries(
+ sortDataWithNumbers,
+ 'my_x_axis',
+ [],
+ SortSeriesType.Name,
+ false,
+ ),
+ ).toEqual([
+ 'd',
+ 'c',
+ 'a',
+ '12. December',
+ '12',
+ '11. November',
+ '11',
+ '10. October',
+ '10',
+ '9. September',
+ '9',
+ '8. August',
+ '8',
+ '7. July',
+ '7',
+ '6. June',
+ '6',
+ '5. May',
+ '5',
+ '4. April',
+ '4',
+ '3. March',
+ '3',
+ '2. February',
+ '2',
+ '1. January',
+ '1',
+ ]);
+});
describe('extractSeries', () => {
it('should generate a valid ECharts timeseries series object', () => {
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index 933ee6a0c1d..ef3a8e700c2 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -486,8 +486,9 @@ const config: ControlPanelConfig = {
return true;
},
mapStateToProps(explore, _, chart) {
- const timeComparisonStatus =
- !!explore?.controls?.time_compare?.value;
+ const timeComparisonStatus = !isEmpty(
+ explore?.controls?.time_compare?.value,
+ );
const { colnames: _colnames, coltypes: _coltypes } =
chart?.queriesResponse?.[0] ?? {};
diff --git a/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx b/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx
index 61b3a7f0011..4cbf930ec21 100644
--- a/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx
+++ b/superset-frontend/src/explore/components/controls/ColorPickerControl.jsx
@@ -78,7 +78,7 @@ export default class ColorPickerControl extends Component {
renderPopover() {
const presetColors = getCategoricalSchemeRegistry()
.get()
- .colors.filter((s, i) => i < 7);
+ .colors.filter((s, i) => i < 9);
return (
None:
+ """
+ Update the catalog of the datasets that are associated with database.
+ """
+ from superset.connectors.sqla.models import SqlaTable
+ from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState
+
+ for model in [
+ SqlaTable,
+ Query,
+ SavedQuery,
+ TabState,
+ TableSchema,
+ ]:
+ fk = "db_id" if model == SavedQuery else "database_id"
+ predicate = {fk: database_id}
+ update = {"catalog": new_catalog}
+ db.session.query(model).filter_by(**predicate).update(update)
+
def validate(self) -> None:
if database_name := self._properties.get("database_name"):
if not DatabaseDAO.validate_update_uniqueness(
diff --git a/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py b/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py
index 2366d3bf261..9cc3735f28c 100644
--- a/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py
+++ b/superset/migrations/versions/2024-09-25_17-59_7b17aa722e30_uuidmixin.py
@@ -24,7 +24,8 @@ Create Date: 2024-09-25 17:59:21.028426
import sqlalchemy as sa
import sqlalchemy_utils
-from alembic import op
+
+from superset.migrations.shared.utils import add_columns, drop_columns
# revision identifiers, used by Alembic.
revision = "7b17aa722e30"
@@ -32,16 +33,16 @@ down_revision = "48cbb571fa3a"
def upgrade():
- op.add_column(
+ add_columns(
"css_templates",
sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=True),
)
- op.add_column(
+ add_columns(
"favstar",
sa.Column("uuid", sqlalchemy_utils.types.uuid.UUIDType(), nullable=True),
)
def downgrade():
- op.drop_column("css_templates", "uuid")
- op.drop_column("favstar", "uuid")
+ drop_columns("css_templates", "uuid")
+ drop_columns("favstar", "uuid")
diff --git a/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py b/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py
index 3e767f36a6f..e9f7d387c51 100644
--- a/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py
+++ b/superset/migrations/versions/2025-03-03_20-52_94e7a3499973_add_folders_column_to_dataset.py
@@ -23,20 +23,21 @@ Create Date: 2025-03-03 20:52:24.585143
"""
import sqlalchemy as sa
-from alembic import op
from sqlalchemy.types import JSON
+from superset.migrations.shared.utils import add_columns, drop_columns
+
# revision identifiers, used by Alembic.
revision = "94e7a3499973"
down_revision = "74ad1125881c"
def upgrade():
- op.add_column(
+ add_columns(
"tables",
sa.Column("folders", JSON, nullable=True),
)
def downgrade():
- op.drop_column("tables", "folders")
+ drop_columns("tables", "folders")
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 80a1fb957dd..d11e0f3e41c 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -1126,7 +1126,9 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"""
Find a List of models by a list of ids, if defined applies `base_filter`
"""
- query = self.get_session.query(Role).filter(Role.id.in_(role_ids))
+ query = self.get_session.query(self.role_model).filter(
+ self.role_model.id.in_(role_ids)
+ )
return query.all()
def copy_role(
diff --git a/superset/translations/nl/LC_MESSAGES/messages.po b/superset/translations/nl/LC_MESSAGES/messages.po
index e652b9b74f4..ffdc05c88ba 100644
--- a/superset/translations/nl/LC_MESSAGES/messages.po
+++ b/superset/translations/nl/LC_MESSAGES/messages.po
@@ -3056,9 +3056,8 @@ msgstr "Configuratie"
msgid "Configure Advanced Time Range "
msgstr "Configureer geavanceerd tijdbereik "
-#, fuzzy
msgid "Configure Time Range: Current..."
-msgstr "Configureer Tijdbereik: Laatste..."
+msgstr "Configureer Tijdbereik: Huidige..."
msgid "Configure Time Range: Last..."
msgstr "Configureer Tijdbereik: Laatste..."
@@ -3366,29 +3365,23 @@ msgstr "Valuta voorvoegsel of achtervoegsel"
msgid "Currency symbol"
msgstr "Valuta symbool"
-#, fuzzy
msgid "Current"
-msgstr "Valuta"
+msgstr "Huidig"
-#, fuzzy
msgid "Current day"
-msgstr "Valuta"
+msgstr "Huidige dag"
-#, fuzzy
msgid "Current month"
-msgstr "Valuta symbool"
+msgstr "Huidige maand"
-#, fuzzy
msgid "Current quarter"
-msgstr "Voer huidige query uit"
+msgstr "Huidig kwartaal"
-#, fuzzy
msgid "Current week"
-msgstr "Voer huidige query uit"
+msgstr "Huidige week"
-#, fuzzy
msgid "Current year"
-msgstr "Valuta"
+msgstr "Huidig jaar"
#, python-format
msgid "Currently rendered: %s"
diff --git a/tests/unit_tests/commands/dashboard/__init__.py b/tests/unit_tests/commands/dashboard/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/tests/unit_tests/commands/dashboard/__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/dashboard/create_test.py b/tests/unit_tests/commands/dashboard/create_test.py
new file mode 100644
index 00000000000..9bb0cff9ab9
--- /dev/null
+++ b/tests/unit_tests/commands/dashboard/create_test.py
@@ -0,0 +1,98 @@
+# 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 pytest
+from flask_appbuilder.security.sqla.models import Role, User
+from pytest_mock import MockerFixture
+
+from superset.commands.dashboard.create import CreateDashboardCommand
+from superset.extensions import security_manager
+from tests.unit_tests.fixtures.common import admin_user, after_each # noqa: F401
+
+
+def test_validate_custom_role_class(
+ mocker: MockerFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ admin_user: User, # noqa: F811
+ after_each: None, # noqa: F811
+) -> None:
+ class CustomRoleModel(Role):
+ __tablename__ = "ab_role"
+
+ monkeypatch.setattr(security_manager, "role_model", CustomRoleModel)
+ mocker.patch.object(security_manager, "is_admin", return_value=True)
+
+ owner_ids = [admin_user.id]
+ role_ids = [role.id for role in admin_user.roles]
+
+ command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids})
+ command.validate()
+
+ for role in command._properties["roles"]:
+ assert isinstance(role, CustomRoleModel)
+
+
+def test_validate_custom_user_class(
+ mocker: MockerFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ admin_user: User, # noqa: F811
+ after_each: None, # noqa: F811
+) -> None:
+ class CustomUserModel(User):
+ __tablename__ = "ab_user"
+
+ monkeypatch.setattr(security_manager, "user_model", CustomUserModel)
+ mocker.patch.object(security_manager, "is_admin", return_value=True)
+
+ owner_ids = [admin_user.id]
+ role_ids = [role.id for role in admin_user.roles]
+
+ command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids})
+ command.validate()
+
+ for owner in command._properties["owners"]:
+ assert isinstance(owner, CustomUserModel)
+
+
+def test_validate_custom_role_and_user_class(
+ mocker: MockerFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ admin_user: User, # noqa: F811
+ after_each: None, # noqa: F811
+) -> None:
+ class CustomRoleModel(Role):
+ __tablename__ = "ab_role"
+
+ class CustomUserModel(User):
+ __tablename__ = "ab_user"
+
+ monkeypatch.setattr(security_manager, "role_model", CustomRoleModel)
+ monkeypatch.setattr(security_manager, "user_model", CustomUserModel)
+ mocker.patch.object(security_manager, "is_admin", return_value=True)
+
+ owner_ids = [admin_user.id]
+ role_ids = [role.id for role in admin_user.roles]
+
+ command = CreateDashboardCommand(data={"owners": owner_ids, "roles": role_ids})
+ command.validate()
+
+ for role in command._properties["roles"]:
+ assert isinstance(role, CustomRoleModel)
+
+ for owner in command._properties["owners"]:
+ assert isinstance(owner, CustomUserModel)
diff --git a/tests/unit_tests/commands/databases/update_test.py b/tests/unit_tests/commands/databases/update_test.py
index a74ff3c5c06..5e6b0869d72 100644
--- a/tests/unit_tests/commands/databases/update_test.py
+++ b/tests/unit_tests/commands/databases/update_test.py
@@ -580,3 +580,65 @@ def test_update_other_fields_dont_affect_oauth(
add_pvm.assert_not_called()
database_needs_oauth2.purge_oauth2_tokens.assert_not_called()
+
+
+def test_update_with_catalog_change(mocker: MockerFixture) -> None:
+ """
+ Test that assets are updated when the main catalog changes.
+ """
+ old_database = mocker.MagicMock(allow_multi_catalog=False)
+ old_database.get_default_catalog.return_value = "project-A"
+ old_database.id = 1
+
+ new_database = mocker.MagicMock(allow_multi_catalog=False)
+ new_database.get_default_catalog.return_value = "project-B"
+
+ database_dao = mocker.patch("superset.commands.database.update.DatabaseDAO")
+ database_dao.find_by_id.return_value = old_database
+ database_dao.update.return_value = new_database
+
+ mocker.patch("superset.commands.database.update.SyncPermissionsCommand")
+ mocker.patch.object(
+ UpdateDatabaseCommand,
+ "validate",
+ )
+ update_catalog_attribute = mocker.patch.object(
+ UpdateDatabaseCommand,
+ "_update_catalog_attribute",
+ )
+
+ UpdateDatabaseCommand(1, {}).run()
+
+ update_catalog_attribute.assert_called_once_with(1, "project-B")
+
+
+def test_update_without_catalog_change(mocker: MockerFixture) -> None:
+ """
+ Test that assets are not updated when the main catalog doesn't change.
+ """
+ old_database = mocker.MagicMock(allow_multi_catalog=False)
+ old_database.database_name = "Ye Old DB"
+ old_database.get_default_catalog.return_value = "project-A"
+ old_database.id = 1
+
+ new_database = mocker.MagicMock(allow_multi_catalog=False)
+ new_database.database_name = "Fancy new DB"
+ new_database.get_default_catalog.return_value = "project-A"
+
+ database_dao = mocker.patch("superset.commands.database.update.DatabaseDAO")
+ database_dao.find_by_id.return_value = old_database
+ database_dao.update.return_value = new_database
+
+ mocker.patch("superset.commands.database.update.SyncPermissionsCommand")
+ mocker.patch.object(
+ UpdateDatabaseCommand,
+ "validate",
+ )
+ update_catalog_attribute = mocker.patch.object(
+ UpdateDatabaseCommand,
+ "_update_catalog_attribute",
+ )
+
+ UpdateDatabaseCommand(1, {}).run()
+
+ update_catalog_attribute.assert_not_called()