From e1f5c49df7f59d101b679bbcce3060254c4d6756 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:11:02 -0300 Subject: [PATCH 01/14] fix: Allows configuration of Selenium Webdriver binary (#33103) --- superset/config.py | 2 +- superset/utils/webdriver.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/superset/config.py b/superset/config.py index cd5393569e2..a82c080b429 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1525,7 +1525,7 @@ WEBDRIVER_AUTH_FUNC = None # Any config options to be passed as-is to the webdriver WEBDRIVER_CONFIGURATION = { - "options": {"capabilities": {}, "preferences": {}}, + "options": {"capabilities": {}, "preferences": {}, "binary_location": ""}, "service": {"log_output": "/dev/null", "service_args": [], "port": 0, "env": {}}, } diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index f9a7899ff9c..8493a1f364d 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -275,6 +275,10 @@ class WebDriverSelenium(WebDriverProxy): # Add additional configured webdriver options webdriver_conf = dict(current_app.config["WEBDRIVER_CONFIGURATION"]) + # Set the binary location if provided + # We need to pop it from the dict due to selenium_version < 4.10.0 + options.binary_location = webdriver_conf.pop("binary_location", "") + if version.parse(selenium_version) < version.parse("4.10.0"): kwargs |= webdriver_conf else: From a5a91d5e480e9d924bd1b648912828c4e8b91150 Mon Sep 17 00:00:00 2001 From: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:19:55 -0300 Subject: [PATCH 02/14] fix(OAuth2): Update connection should not fail if connection is missing OAuth2 token (#33100) --- superset/commands/database/exceptions.py | 9 +++++++ .../commands/database/sync_permissions.py | 6 +++++ superset/commands/database/update.py | 3 ++- .../integration_tests/databases/api_tests.py | 27 +++++++++++++++++++ .../unit_tests/commands/databases/conftest.py | 2 ++ .../databases/sync_permissions_test.py | 14 ++++++++-- 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/superset/commands/database/exceptions.py b/superset/commands/database/exceptions.py index 6dfdadacbd7..10c1a96c67a 100644 --- a/superset/commands/database/exceptions.py +++ b/superset/commands/database/exceptions.py @@ -138,6 +138,15 @@ class DatabaseConnectionFailedError( # pylint: disable=too-many-ancestors message = _("Connection failed, please check your connection settings") +class MissingOAuth2TokenError(DatabaseUpdateFailedError): + """ + Exception for when the connection is missing an OAuth2 token + and it's not possible to initiate an OAuth2 dance. + """ + + message = _("Missing OAuth2 token") + + class DatabaseDeleteDatasetsExistFailedError(DeleteFailedError): message = _("Cannot delete a database that has datasets attached") diff --git a/superset/commands/database/sync_permissions.py b/superset/commands/database/sync_permissions.py index 4f041ce94cb..bceacff89b1 100644 --- a/superset/commands/database/sync_permissions.py +++ b/superset/commands/database/sync_permissions.py @@ -28,6 +28,7 @@ from superset.commands.database.exceptions import ( DatabaseConnectionFailedError, DatabaseConnectionSyncPermissionsError, DatabaseNotFoundError, + MissingOAuth2TokenError, UserNotFoundInSessionError, ) from superset.commands.database.utils import ( @@ -115,6 +116,11 @@ class SyncPermissionsCommand(BaseCommand): try: alive = ping(engine) except Exception as err: + if ( + self.db_connection.is_oauth2_enabled() + and self.db_connection.db_engine_spec.needs_oauth2(err) + ): + raise MissingOAuth2TokenError() from err raise DatabaseConnectionFailedError() from err if not alive: diff --git a/superset/commands/database/update.py b/superset/commands/database/update.py index f562b5d7802..92f91c15ec0 100644 --- a/superset/commands/database/update.py +++ b/superset/commands/database/update.py @@ -30,6 +30,7 @@ from superset.commands.database.exceptions import ( DatabaseInvalidError, DatabaseNotFoundError, DatabaseUpdateFailedError, + MissingOAuth2TokenError, ) from superset.commands.database.ssh_tunnel.create import CreateSSHTunnelCommand from superset.commands.database.ssh_tunnel.delete import DeleteSSHTunnelCommand @@ -108,7 +109,7 @@ class UpdateDatabaseCommand(BaseCommand): db_connection=database, ssh_tunnel=ssh_tunnel, ).run() - except OAuth2RedirectError: + except (OAuth2RedirectError, MissingOAuth2TokenError): pass return database diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index fc558e519fb..0c6dad0b1df 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -35,6 +35,7 @@ from sqlalchemy.exc import DBAPIError from sqlalchemy.sql import func from superset import db, security_manager +from superset.commands.database.exceptions import MissingOAuth2TokenError from superset.connectors.sqla.models import SqlaTable from superset.databases.ssh_tunnel.models import SSHTunnel from superset.databases.utils import make_url_safe # noqa: F401 @@ -1393,6 +1394,32 @@ class TestDatabaseApi(SupersetTestCase): db.session.delete(model) db.session.commit() + @mock.patch( + "superset.commands.database.sync_permissions.SyncPermissionsCommand.run", + ) + def test_update_database_missing_oauth2_token(self, mock_sync_perms): + """ + Database API: Test update DB connection that does not have + an OAuth2 token yet does not raise. + """ + example_db = get_example_database() + test_database = self.insert_database( + "test-oauth-database", example_db.sqlalchemy_uri_decrypted + ) + mock_sync_perms.side_effect = MissingOAuth2TokenError() + self.login(ADMIN_USERNAME) + database_data = { + "database_name": "test-database-updated", + "configuration_method": ConfigurationMethod.SQLALCHEMY_FORM, + } + uri = f"api/v1/database/{test_database.id}" + rv = self.client.put(uri, json=database_data) + assert rv.status_code == 200 + # Cleanup + model = db.session.query(Database).get(test_database.id) + db.session.delete(model) + db.session.commit() + def test_update_database_uniqueness(self): """ Database API: Test update uniqueness diff --git a/tests/unit_tests/commands/databases/conftest.py b/tests/unit_tests/commands/databases/conftest.py index 49da52daf91..81f489d95ed 100644 --- a/tests/unit_tests/commands/databases/conftest.py +++ b/tests/unit_tests/commands/databases/conftest.py @@ -64,6 +64,8 @@ def database_without_catalog(mocker: MockerFixture) -> MagicMock: database.db_engine_spec.__name__ = "test_engine" database.db_engine_spec.supports_catalog = False database.get_all_schema_names.return_value = ["schema1", "schema2"] + database.is_oauth2_enabled.return_value = False + database.db_engine_spec.needs_oauth2.return_value = False return database diff --git a/tests/unit_tests/commands/databases/sync_permissions_test.py b/tests/unit_tests/commands/databases/sync_permissions_test.py index e597e384db9..78dfe3d0c15 100644 --- a/tests/unit_tests/commands/databases/sync_permissions_test.py +++ b/tests/unit_tests/commands/databases/sync_permissions_test.py @@ -25,6 +25,7 @@ from superset import db from superset.commands.database.exceptions import ( DatabaseConnectionFailedError, DatabaseNotFoundError, + MissingOAuth2TokenError, UserNotFoundInSessionError, ) from superset.commands.database.sync_permissions import SyncPermissionsCommand @@ -146,14 +147,18 @@ def test_sync_permissions_command_passing_all_values( @with_config({"SYNC_DB_PERMISSIONS_IN_ASYNC_MODE": False}) -def test_sync_permissions_command_raise(mocker: MockerFixture): +def test_sync_permissions_command_raise( + mocker: MockerFixture, + database_without_catalog: MagicMock, + database_needs_oauth2: MagicMock, +): """ Test ``SyncPermissionsCommand`` when an exception is raised. """ mock_database_dao = mocker.patch( "superset.commands.database.sync_permissions.DatabaseDAO" ) - mock_database_dao.find_by_id.return_value = mocker.MagicMock() + mock_database_dao.find_by_id.return_value = database_without_catalog mock_database_dao.get_ssh_tunnel.return_value = mocker.MagicMock() mock_user = mocker.patch( "superset.commands.database.sync_permissions.security_manager.get_user_by_username" @@ -169,6 +174,11 @@ def test_sync_permissions_command_raise(mocker: MockerFixture): mock_ping.side_effect = Exception with pytest.raises(DatabaseConnectionFailedError): SyncPermissionsCommand(1, "admin").run() + # OAuth2 error + mock_database_dao.find_by_id.reset_mock() + mock_database_dao.find_by_id.return_value = database_needs_oauth2 + with pytest.raises(MissingOAuth2TokenError): + SyncPermissionsCommand(1, "admin").run() # User not found in session mock_user.reset_mock() From 7b9ebbe735cf54e5aa961cd7ea3b2110a7d159e1 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 14 Apr 2025 09:40:31 -0700 Subject: [PATCH 03/14] feat(explore): Integrate dataset panel with Folders feature (#33104) Co-authored-by: Kamil Gabryjelski --- superset-frontend/package-lock.json | 1 + .../src/components/MetricOption.tsx | 2 +- .../src/components/labelUtils.tsx | 2 +- .../src/fixtures.ts | 1 + .../test/utils/defineSavedMetrics.test.tsx | 2 + .../test/utils/mainMetric.test.ts | 15 +- .../packages/superset-ui-core/package.json | 1 + .../useCSSTextTruncation.test.tsx | 34 +++ .../useTruncation/useCSSTextTruncation.ts | 25 +- .../src/query/types/Datasource.ts | 2 + .../src/query/types/Metric.ts | 1 + .../test/query/types/Datasource.test.ts | 4 +- .../test/BigNumber/transformProps.test.ts | 2 + .../DatasourcePanel/DatasourceItems.tsx | 165 ++++++++++ .../DatasourcePanel/DatasourcePanel.test.tsx | 198 +++++++++++- .../DatasourcePanelDragOption.test.tsx | 4 +- .../DatasourcePanelItem.test.tsx | 237 +++++--------- .../DatasourcePanel/DatasourcePanelItem.tsx | 288 +++++++++--------- .../components/DatasourcePanel/fixtures.tsx | 7 +- .../components/DatasourcePanel/index.tsx | 130 +++----- .../transformDatasourceFolders.test.ts | 208 +++++++++++++ .../transformDatasourceFolders.ts | 177 +++++++++++ .../components/DatasourcePanel/types.ts | 56 ++++ .../ExploreContainer.test.tsx | 2 +- .../DndFilterSelect.test.tsx | 10 +- .../DndMetricSelect.test.tsx | 10 +- .../DndMetricSelect.tsx | 2 + .../controlUtils/controlUtils.test.tsx | 5 +- ...trolValuesCompatibleWithDatasource.test.ts | 2 +- superset-frontend/src/explore/fixtures.tsx | 5 +- .../src/features/datasets/types.ts | 2 +- 31 files changed, 1156 insertions(+), 444 deletions(-) create mode 100644 superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx create mode 100644 superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.test.ts create mode 100644 superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.ts diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 3749e68dd77..9d6ce44ff8d 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -48975,6 +48975,7 @@ "@types/react": "*", "@types/react-loadable": "*", "@types/tinycolor2": "*", + "nanoid": "^5.0.9", "react": "^17.0.2", "react-loadable": "^5.5.0", "tinycolor2": "*" diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/components/MetricOption.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/components/MetricOption.tsx index c424cde518e..59beeee4c56 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/components/MetricOption.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/MetricOption.tsx @@ -42,7 +42,7 @@ const FlexRowContainer = styled.div` `; export interface MetricOptionProps { - metric: Omit & { label?: string }; + metric: Omit & { label?: string }; openInNewWindow?: boolean; showFormula?: boolean; showType?: boolean; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/components/labelUtils.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/components/labelUtils.tsx index 4ef491f13ce..4102fb2b9f3 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/components/labelUtils.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/labelUtils.tsx @@ -97,7 +97,7 @@ export const getColumnTooltipNode = ( ); }; -type MetricType = Omit & { label?: string }; +type MetricType = Omit & { label?: string }; export const getMetricTooltipNode = ( metric: MetricType, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts index efdfef6c4f1..05b4d4df3dc 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts @@ -121,6 +121,7 @@ export const TestDataset: Dataset = { main_dttm_col: 'ds', metrics: [ { + uuid: '123', certification_details: null, certified_by: null, d3format: null, diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx index 79910895b24..682075b5c1c 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx @@ -32,6 +32,7 @@ describe('defineSavedMetrics', () => { { metric_name: 'COUNT(*) non-default-dataset-metric', expression: 'COUNT(*) non-default-dataset-metric', + uuid: '1', }, ], type: DatasourceType.Table, @@ -48,6 +49,7 @@ describe('defineSavedMetrics', () => { { metric_name: 'COUNT(*) non-default-dataset-metric', expression: 'COUNT(*) non-default-dataset-metric', + uuid: '1', }, ]); // @ts-ignore diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/mainMetric.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/mainMetric.test.ts index 9385537d292..4b009766428 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/mainMetric.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/mainMetric.test.ts @@ -24,15 +24,24 @@ describe('mainMetric', () => { expect(mainMetric(null)).toBeUndefined(); }); it('prefers the "count" metric when first', () => { - const metrics = [{ metric_name: 'count' }, { metric_name: 'foo' }]; + const metrics = [ + { metric_name: 'count', uuid: '1' }, + { metric_name: 'foo', uuid: '2' }, + ]; expect(mainMetric(metrics)).toBe('count'); }); it('prefers the "count" metric when not first', () => { - const metrics = [{ metric_name: 'foo' }, { metric_name: 'count' }]; + const metrics = [ + { metric_name: 'foo', uuid: '1' }, + { metric_name: 'count', uuid: '2' }, + ]; expect(mainMetric(metrics)).toBe('count'); }); it('selects the first metric when "count" is not an option', () => { - const metrics = [{ metric_name: 'foo' }, { metric_name: 'not_count' }]; + const metrics = [ + { metric_name: 'foo', uuid: '2' }, + { metric_name: 'not_count', uuid: '2' }, + ]; expect(mainMetric(metrics)).toBe('foo'); }); }); diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index ab9b0cbc279..158aaa15d35 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -81,6 +81,7 @@ "@types/react": "*", "@types/react-loadable": "*", "@types/tinycolor2": "*", + "nanoid": "^5.0.9", "react": "^17.0.2", "react-loadable": "^5.5.0", "tinycolor2": "*" diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.test.tsx b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.test.tsx index 02dff90621e..8239cc64c70 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.test.tsx @@ -59,3 +59,37 @@ test('should truncate', () => { expect(isTruncated).toBe(true); }); + +test('should not truncate with vertical orientation', () => { + const ref = { current: document.createElement('p') }; + Object.defineProperty(ref.current, 'offsetHeight', { get: () => 100 }); + Object.defineProperty(ref.current, 'scrollHeight', { get: () => 50 }); + jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current }); + + const { result } = renderHook(() => + useCSSTextTruncation({ + isVertical: true, + isHorizontal: false, + }), + ); + const [, isTruncated] = result.current; + + expect(isTruncated).toBe(false); +}); + +test('should truncate with vertical orientation', () => { + const ref = { current: document.createElement('p') }; + Object.defineProperty(ref.current, 'offsetHeight', { get: () => 50 }); + Object.defineProperty(ref.current, 'scrollHeight', { get: () => 100 }); + jest.spyOn(global.React, 'useRef').mockReturnValue({ current: ref.current }); + + const { result } = renderHook(() => + useCSSTextTruncation({ + isVertical: true, + isHorizontal: false, + }), + ); + const [, isTruncated] = result.current; + + expect(isTruncated).toBe(true); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.ts index 335e26a2263..8bfefc0e29e 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.ts +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useCSSTextTruncation.ts @@ -36,24 +36,37 @@ export const truncationCSS = css` * to be displayed, this hook returns a ref to attach to the text * element and a boolean for whether that element is currently truncated. */ -const useCSSTextTruncation = (): [ - RefObject, - boolean, -] => { +const useCSSTextTruncation = ( + { isVertical, isHorizontal } = { isVertical: false, isHorizontal: true }, +): [RefObject, boolean] => { const [isTruncated, setIsTruncated] = useState(true); const ref = useRef(null); const [offsetWidth, setOffsetWidth] = useState(0); const [scrollWidth, setScrollWidth] = useState(0); + const [offsetHeight, setOffsetHeight] = useState(0); + const [scrollHeight, setScrollHeight] = useState(0); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { setOffsetWidth(ref.current?.offsetWidth ?? 0); setScrollWidth(ref.current?.scrollWidth ?? 0); + setOffsetHeight(ref.current?.offsetHeight ?? 0); + setScrollHeight(ref.current?.scrollHeight ?? 0); }); useEffect(() => { - setIsTruncated(offsetWidth < scrollWidth); - }, [offsetWidth, scrollWidth]); + setIsTruncated( + (isVertical && offsetHeight < scrollHeight) || + (isHorizontal && offsetWidth < scrollWidth), + ); + }, [ + offsetWidth, + scrollWidth, + offsetHeight, + scrollHeight, + isVertical, + isHorizontal, + ]); return [ref, isTruncated]; }; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts index ab5ff950cc1..c5ce93c1e91 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Datasource.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { nanoid } from 'nanoid'; import { Column } from './Column'; import { Metric } from './Metric'; @@ -58,6 +59,7 @@ export const DEFAULT_METRICS: Metric[] = [ { metric_name: 'COUNT(*)', expression: 'COUNT(*)', + uuid: nanoid(), }, ]; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts index 229852373a8..15b59fb22ab 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Metric.ts @@ -60,6 +60,7 @@ export type SavedMetric = string; */ export interface Metric { id?: number; + uuid: string; metric_name: string; expression?: Maybe; certification_details?: Maybe; diff --git a/superset-frontend/packages/superset-ui-core/test/query/types/Datasource.test.ts b/superset-frontend/packages/superset-ui-core/test/query/types/Datasource.test.ts index c80f3d69500..aa77b74349e 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/types/Datasource.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/types/Datasource.test.ts @@ -20,10 +20,10 @@ import { DatasourceType, DEFAULT_METRICS } from '@superset-ui/core'; test('DEFAULT_METRICS', () => { expect(DEFAULT_METRICS).toEqual([ - { + expect.objectContaining({ metric_name: 'COUNT(*)', expression: 'COUNT(*)', - }, + }), ]); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts index 4ccedd1e7f2..e7ce20cc072 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts @@ -149,6 +149,7 @@ describe('BigNumberWithTrendline', () => { label: 'value', metric_name: 'value', d3format: '.2f', + uuid: '1', }, ], }, @@ -174,6 +175,7 @@ describe('BigNumberWithTrendline', () => { metric_name: 'value', d3format: '.2f', currency: { symbol: 'USD', symbolPosition: 'prefix' }, + uuid: '1', }, ], }, diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx new file mode 100644 index 00000000000..53e55b60543 --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourceItems.tsx @@ -0,0 +1,165 @@ +/** + * 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { VariableSizeList as List } from 'react-window'; +import { FlattenedItem, Folder } from './types'; +import DatasourcePanelItem from './DatasourcePanelItem'; + +const BORDER_WIDTH = 2; +const HEADER_ITEM_HEIGHT = 50; +const METRIC_OR_COLUMN_ITEM_HEIGHT = 32; +const SUBTITLE_ITEM_HEIGHT = 32; +const DIVIDER_ITEM_HEIGHT = 16; + +const flattenFolderStructure = ( + foldersToFlatten: Folder[], + collapsedFolderIds: Set, + depth = 0, + folderMap: Map = new Map(), +): { flattenedItems: FlattenedItem[]; folderMap: Map } => { + const flattenedItems: FlattenedItem[] = []; + + foldersToFlatten.forEach((folder, idx) => { + folderMap.set(folder.id, folder); + + flattenedItems.push({ + type: 'header', + folderId: folder.id, + depth, + height: HEADER_ITEM_HEIGHT, + }); + + if (!collapsedFolderIds.has(folder.id)) { + flattenedItems.push({ + type: 'subtitle', + folderId: folder.id, + depth, + height: SUBTITLE_ITEM_HEIGHT, + totalItems: folder.totalItems, + showingItems: folder.showingItems, + }); + folder.items.forEach(item => { + flattenedItems.push({ + type: 'item', + folderId: folder.id, + depth, + item, + height: METRIC_OR_COLUMN_ITEM_HEIGHT, + }); + }); + + if (folder.subFolders && folder.subFolders.length > 0) { + const { flattenedItems: subItems } = flattenFolderStructure( + folder.subFolders, + collapsedFolderIds, + depth + 1, + folderMap, + ); + + flattenedItems.push(...subItems); + } + } + if (depth === 0 && idx !== foldersToFlatten.length - 1) { + flattenedItems.push({ + type: 'divider', + folderId: folder.id, + depth, + height: DIVIDER_ITEM_HEIGHT, + }); + } + }); + + return { flattenedItems, folderMap }; +}; + +interface DatasourceItemsProps { + width: number; + height: number; + folders: Folder[]; +} +export const DatasourceItems = ({ + width, + height, + folders, +}: DatasourceItemsProps) => { + const listRef = useRef(null); + const [collapsedFolderIds, setCollapsedFolderIds] = useState>( + new Set( + folders.filter(folder => folder.isCollapsed).map(folder => folder.id), + ), + ); + + const { flattenedItems, folderMap } = useMemo( + () => flattenFolderStructure(folders, collapsedFolderIds), + [folders, collapsedFolderIds], + ); + + const handleToggleCollapse = useCallback((folderId: string) => { + setCollapsedFolderIds(prevIds => { + const newIds = new Set(prevIds); + if (newIds.has(folderId)) { + newIds.delete(folderId); + } else { + newIds.add(folderId); + } + return newIds; + }); + }, []); + + useEffect(() => { + // reset the list cache when flattenedItems length changes to recalculate the heights + listRef.current?.resetAfterIndex(0); + }, [flattenedItems]); + + const getItemSize = useCallback( + (index: number) => flattenedItems[index].height, + [flattenedItems], + ); + + const itemData = useMemo( + () => ({ + flattenedItems, + folderMap, + width, + onToggleCollapse: handleToggleCollapse, + collapsedFolderIds, + }), + [ + flattenedItems, + folderMap, + width, + handleToggleCollapse, + collapsedFolderIds, + ], + ); + + return ( + + {DatasourcePanelItem} + + ); +}; diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx index 84b40969b5b..19a91463d39 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx @@ -59,6 +59,48 @@ const datasource: IDatasource = { datasource_name: 'table1', }; +const datasourceWithFolders: IDatasource = { + ...datasource, + folders: [ + { + name: 'Test folder', + type: 'folder', + uuid: '1', + children: [ + { + name: 'Test nested folder', + type: 'folder', + uuid: '1.1', + children: [ + { + type: 'metric', + uuid: metrics[0].uuid, + name: metrics[0].metric_name, + }, + ], + }, + ], + }, + { + name: 'Second test folder', + type: 'folder', + uuid: '2', + children: [ + { + type: 'column', + uuid: columns[0].uuid, + name: columns[0].column_name, + }, + { + type: 'column', + uuid: columns[1].uuid, + name: columns[1].column_name, + }, + ], + }, + ], +}; + const mockUser = { createdOn: '2021-04-27T18:12:38.952304', email: 'admin', @@ -90,6 +132,18 @@ const props: DatasourcePanelProps = { width: 300, }; +const propsWithFolders = { + ...props, + datasource: datasourceWithFolders, + controls: { + ...props.controls, + datasource: { + ...props.controls.datasource, + datasource: datasourceWithFolders, + }, + }, +}; + const metricProps = { savedMetrics: [], columns: [], @@ -125,13 +179,9 @@ test('should render the metrics', async () => { , { useRedux: true, useDnd: true }, ); - const metricsNum = metrics.length; metrics.forEach(metric => expect(screen.getByText(metric.metric_name)).toBeInTheDocument(), ); - expect( - await screen.findByText(`Showing ${metricsNum} of ${metricsNum}`), - ).toBeInTheDocument(); }); test('should render the columns', async () => { @@ -142,13 +192,9 @@ test('should render the columns', async () => { , { useRedux: true, useDnd: true }, ); - const columnsNum = columns.length; columns.forEach(col => expect(screen.getByText(col.column_name)).toBeInTheDocument(), ); - expect( - await screen.findByText(`Showing ${columnsNum} of ${columnsNum}`), - ).toBeInTheDocument(); }); describe('DatasourcePanel', () => { @@ -310,3 +356,139 @@ test('should render only droppable metrics and columns', async () => { unmount(); }); + +test('Renders with custom folders', () => { + render( + + + + , + { + useRedux: true, + useDnd: true, + }, + ); + + expect(screen.getByText('Test folder')).toBeInTheDocument(); + expect(screen.getByText('Test nested folder')).toBeInTheDocument(); + expect(screen.getByText('Second test folder')).toBeInTheDocument(); + expect(screen.getByText('Metrics')).toBeInTheDocument(); + expect(screen.getByText('Columns')).toBeInTheDocument(); + + columns.forEach(col => { + expect(screen.getByText(col.column_name)).toBeInTheDocument(); + }); + + metrics.forEach(metric => { + expect(screen.getByText(metric.metric_name)).toBeInTheDocument(); + }); + + expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5); + expect(screen.getAllByTestId('datasource-panel-divider').length).toEqual(3); +}); + +test('Collapse folders', () => { + render( + + + + , + { + useRedux: true, + useDnd: true, + }, + ); + + userEvent.click(screen.getByText('Test folder')); + + expect(screen.getByText('Test folder')).toBeInTheDocument(); + expect(screen.queryByText('Test nested folder')).not.toBeInTheDocument(); + expect(screen.getByText('Second test folder')).toBeInTheDocument(); + expect(screen.getByText('Metrics')).toBeInTheDocument(); + expect(screen.getByText('Columns')).toBeInTheDocument(); + + expect(screen.queryByText(metrics[0].metric_name)).not.toBeInTheDocument(); + + userEvent.click(screen.getByText('Test folder')); + + expect(screen.getByText('Test folder')).toBeInTheDocument(); + expect(screen.getByText('Test nested folder')).toBeInTheDocument(); + expect(screen.getByText('Second test folder')).toBeInTheDocument(); + expect(screen.getByText('Metrics')).toBeInTheDocument(); + expect(screen.getByText('Columns')).toBeInTheDocument(); + + expect(screen.getByText(metrics[0].metric_name)).toBeInTheDocument(); +}); + +test('Default Metrics and Columns folders dont render when all metrics and columns are assigned to custom folders', () => { + const datasourceWithFullFolders: IDatasource = { + ...datasource, + folders: [ + { + name: 'Test folder', + type: 'folder', + uuid: '1', + children: [ + { + name: 'Test nested folder', + type: 'folder', + uuid: '1.1', + children: metrics.map(m => ({ + type: 'metric' as const, + uuid: m.uuid, + name: m.metric_name, + })), + }, + ], + }, + { + name: 'Second test folder', + type: 'folder', + uuid: '2', + children: columns.map(c => ({ + type: 'column', + uuid: c.uuid, + name: c.column_name, + })), + }, + ], + }; + const propsWithFullFolders = { + ...props, + datasource: datasourceWithFullFolders, + controls: { + ...props.controls, + datasource: { + ...props.controls.datasource, + datasource: datasourceWithFullFolders, + }, + }, + }; + render( + + + + , + { + useRedux: true, + useDnd: true, + }, + ); + + expect(screen.getByText('Test folder')).toBeInTheDocument(); + expect(screen.getByText('Test nested folder')).toBeInTheDocument(); + expect(screen.getByText('Second test folder')).toBeInTheDocument(); + expect(screen.queryByText('Metrics')).not.toBeInTheDocument(); + expect(screen.queryByText('Columns')).not.toBeInTheDocument(); + + columns.forEach(col => { + expect(screen.getByText(col.column_name)).toBeInTheDocument(); + }); + + metrics.forEach(metric => { + expect(screen.getByText(metric.metric_name)).toBeInTheDocument(); + }); + + expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5); + expect(screen.getAllByTestId('datasource-panel-divider').length).toEqual(1); +}); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelDragOption/DatasourcePanelDragOption.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelDragOption/DatasourcePanelDragOption.test.tsx index cced2869c94..12fd816b6db 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelDragOption/DatasourcePanelDragOption.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelDragOption/DatasourcePanelDragOption.test.tsx @@ -23,7 +23,7 @@ import DatasourcePanelDragOption from '.'; test('should render', async () => { render( , { useDnd: true }, @@ -38,7 +38,7 @@ test('should render', async () => { test('should have attribute draggable:true', async () => { render( , { useDnd: true }, diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx index 3ec64bf3266..b96905ba2eb 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx @@ -20,178 +20,89 @@ import { columns, metrics, } from 'src/explore/components/DatasourcePanel/fixtures'; -import { fireEvent, render, within } from 'spec/helpers/testing-library'; -import DatasourcePanelItem from './DatasourcePanelItem'; +import { screen, userEvent, render } from 'spec/helpers/testing-library'; +import DatasourcePanelItem, { + DatasourcePanelItemProps, +} from './DatasourcePanelItem'; -const mockData = { - metricSlice: metrics, - columnSlice: columns, - totalMetrics: Math.max(metrics.length, 10), - totalColumns: Math.max(columns.length, 13), +const mockData: DatasourcePanelItemProps['data'] = { + flattenedItems: [ + { type: 'header', depth: 0, folderId: '1', height: 50 }, + ...metrics.map((m, idx) => ({ + type: 'item' as const, + depth: 0, + folderId: '1', + height: 32, + index: idx, + item: { ...m, type: 'metric' as const }, + })), + { type: 'divider', depth: 0, folderId: '1', height: 16 }, + { type: 'header', depth: 0, folderId: '2', height: 50 }, + ...columns.map((m, idx) => ({ + type: 'item' as const, + depth: 0, + folderId: '2', + height: 32, + index: idx, + item: { ...m, type: 'column' as const }, + })), + ], + folderMap: new Map([ + [ + '1', + { + id: '1', + isCollapsed: false, + name: 'Metrics', + items: metrics.map(m => ({ ...m, type: 'metric' })), + totalItems: metrics.length, + showingItems: metrics.length, + }, + ], + [ + '2', + { + id: '2', + isCollapsed: false, + name: 'Columns', + items: columns.map(c => ({ ...c, type: 'column' })), + totalItems: columns.length, + showingItems: columns.length, + }, + ], + ]), width: 300, - showAllMetrics: false, - onShowAllMetricsChange: jest.fn(), - showAllColumns: false, - onShowAllColumnsChange: jest.fn(), - collapseMetrics: false, - onCollapseMetricsChange: jest.fn(), - collapseColumns: false, - onCollapseColumnsChange: jest.fn(), - hiddenMetricCount: 0, - hiddenColumnCount: 0, + onToggleCollapse: jest.fn(), + collapsedFolderIds: new Set(), }; -test('renders each item accordingly', () => { - const { getByText, getByTestId, rerender, container } = render( - , +const setup = (data: DatasourcePanelItemProps['data'] = mockData) => + render( + <> + {data.flattenedItems.map((_, index) => ( + + ))} + , { useDnd: true }, ); - expect(getByText('Metrics')).toBeInTheDocument(); - rerender(); - expect( - getByText( - `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`, - ), - ).toBeInTheDocument(); - mockData.metricSlice.forEach((metric, metricIndex) => { - rerender( - , - ); - expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument(); - expect( - within(getByTestId('DatasourcePanelDragOption')).getByText( - metric.metric_name, - ), - ).toBeInTheDocument(); - }); - rerender( - , - ); - expect(container).toHaveTextContent(''); +test('renders each item accordingly', () => { + setup(); + expect(screen.getByText('Metrics')).toBeInTheDocument(); + expect(screen.getByText('metric_end_certified')).toBeInTheDocument(); + expect(screen.getByText('metric_end')).toBeInTheDocument(); - const startIndexOfColumnSection = mockData.metricSlice.length + 3; - rerender( - , - ); - expect(getByText('Columns')).toBeInTheDocument(); - rerender( - , - ); - expect( - getByText( - `Showing ${mockData.columnSlice.length} of ${mockData.totalColumns}`, - ), - ).toBeInTheDocument(); - mockData.columnSlice.forEach((column, columnIndex) => { - rerender( - , - ); - expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument(); - expect( - within(getByTestId('DatasourcePanelDragOption')).getByText( - column.column_name, - ), - ).toBeInTheDocument(); - }); + expect(screen.getByText('Columns')).toBeInTheDocument(); + expect(screen.getByText('bootcamp_attend')).toBeInTheDocument(); + expect(screen.getByText('calc_first_time_dev')).toBeInTheDocument(); + expect(screen.getByText('aaaaaaaaaaa')).toBeInTheDocument(); + + expect(screen.getByTestId('datasource-panel-divider')).toBeInTheDocument(); + expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5); }); test('can collapse metrics and columns', () => { - mockData.onCollapseMetricsChange.mockClear(); - mockData.onCollapseColumnsChange.mockClear(); - const { queryByText, getByRole, rerender } = render( - , - { useDnd: true }, - ); - fireEvent.click(getByRole('button')); - expect(mockData.onCollapseMetricsChange).toHaveBeenCalled(); - expect(mockData.onCollapseColumnsChange).not.toHaveBeenCalled(); - - const startIndexOfColumnSection = mockData.metricSlice.length + 3; - rerender( - , - ); - fireEvent.click(getByRole('button')); - expect(mockData.onCollapseColumnsChange).toHaveBeenCalled(); - - rerender( - , - ); - expect( - queryByText( - `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`, - ), - ).not.toBeInTheDocument(); - - rerender( - , - ); - expect(queryByText('Columns')).toBeInTheDocument(); -}); - -test('shows ineligible items count', () => { - const hiddenColumnCount = 3; - const hiddenMetricCount = 1; - const dataWithHiddenItems = { - ...mockData, - hiddenColumnCount, - hiddenMetricCount, - }; - const { getByText, rerender } = render( - , - { useDnd: true }, - ); - expect( - getByText(`${hiddenMetricCount} ineligible item(s) are hidden`), - ).toBeInTheDocument(); - - const startIndexOfColumnSection = mockData.metricSlice.length + 3; - rerender( - , - ); - expect( - getByText(`${hiddenColumnCount} ineligible item(s) are hidden`), - ).toBeInTheDocument(); + setup(); + userEvent.click(screen.getAllByRole('button')[0]); + expect(mockData.onToggleCollapse).toHaveBeenCalled(); }); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx index 0ca8f92e161..7f7830acc05 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx @@ -16,71 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -import { CSSProperties, FC } from 'react'; +import { CSSProperties, ReactNode, useCallback } from 'react'; -import { css, Metric, styled, t, useTheme } from '@superset-ui/core'; +import { + css, + styled, + t, + useCSSTextTruncation, + useTheme, +} from '@superset-ui/core'; import { Icons } from 'src/components/Icons'; +import { Tooltip } from 'src/components/Tooltip'; import DatasourcePanelDragOption from './DatasourcePanelDragOption'; import { DndItemType } from '../DndItemType'; -import { DndItemValue } from './types'; - -export type DataSourcePanelColumn = { - is_dttm?: boolean | null; - description?: string | null; - expression?: string | null; - is_certified?: number | null; - column_name?: string | null; - name?: string | null; - type?: string; -}; - -type Props = { - index: number; - style: CSSProperties; - data: { - metricSlice: Metric[]; - columnSlice: DataSourcePanelColumn[]; - totalMetrics: number; - totalColumns: number; - width: number; - showAllMetrics: boolean; - onShowAllMetricsChange: (showAll: boolean) => void; - showAllColumns: boolean; - onShowAllColumnsChange: (showAll: boolean) => void; - collapseMetrics: boolean; - onCollapseMetricsChange: (collapse: boolean) => void; - collapseColumns: boolean; - onCollapseColumnsChange: (collapse: boolean) => void; - hiddenMetricCount: number; - hiddenColumnCount: number; - }; -}; - -export const DEFAULT_MAX_COLUMNS_LENGTH = 50; -export const DEFAULT_MAX_METRICS_LENGTH = 50; -export const ITEM_HEIGHT = 30; - -const Button = styled.button` - background: none; - border: none; - text-decoration: underline; - color: ${({ theme }) => theme.colors.primary.dark1}; -`; - -const ButtonContainer = styled.div` - text-align: center; - padding-top: 2px; -`; +import { DndItemValue, FlattenedItem, Folder } from './types'; const LabelWrapper = styled.div` ${({ theme }) => css` + color: ${theme.colors.grayscale.dark1}; overflow: hidden; text-overflow: ellipsis; font-size: ${theme.typography.sizes.s}px; background-color: ${theme.colors.grayscale.light4}; margin: ${theme.gridUnit * 2}px 0; - border-radius: 4px; + border-radius: ${theme.borderRadius}px; padding: 0 ${theme.gridUnit}px; &:first-of-type { @@ -117,98 +77,136 @@ const LabelWrapper = styled.div` `; const SectionHeaderButton = styled.button` - display: flex; - justify-content: space-between; - align-items: center; border: none; background: transparent; width: 100%; - padding-inline: 0px; + height: 100%; + padding-inline: 0; +`; + +const SectionHeaderTextContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; `; const SectionHeader = styled.span` - ${({ theme }) => ` + ${({ theme }) => css` + color: ${theme.colors.grayscale.dark1}; font-size: ${theme.typography.sizes.m}px; + font-weight: ${theme.typography.weights.medium}; line-height: 1.3; - `} -`; - -const Box = styled.div` - ${({ theme }) => ` - border: 1px ${theme.colors.grayscale.light4} solid; - border-radius: ${theme.gridUnit}px; - font-size: ${theme.typography.sizes.s}px; - padding: ${theme.gridUnit}px; - color: ${theme.colors.grayscale.light1}; - text-overflow: ellipsis; - white-space: nowrap; + text-align: left; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; overflow: hidden; + text-overflow: ellipsis; `} `; -const DatasourcePanelItem: FC = ({ index, style, data }) => { - const { - metricSlice: _metricSlice, - columnSlice, - totalMetrics, - totalColumns, - width, - showAllMetrics, - onShowAllMetricsChange, - showAllColumns, - onShowAllColumnsChange, - collapseMetrics, - onCollapseMetricsChange, - collapseColumns, - onCollapseColumnsChange, - hiddenMetricCount, - hiddenColumnCount, - } = data; - const metricSlice = collapseMetrics ? [] : _metricSlice; +const Divider = styled.div` + ${({ theme }) => css` + height: 16px; + border-bottom: 1px solid ${theme.colors.grayscale.light3}; + `} +`; - const EXTRA_LINES = collapseMetrics ? 1 : 2; - const isColumnSection = collapseMetrics - ? index >= 1 - : index > metricSlice.length + EXTRA_LINES; - const HEADER_LINE = isColumnSection - ? metricSlice.length + EXTRA_LINES + 1 - : 0; - const SUBTITLE_LINE = HEADER_LINE + 1; - const BOTTOM_LINE = - (isColumnSection ? columnSlice.length : metricSlice.length) + - (collapseMetrics ? HEADER_LINE : SUBTITLE_LINE) + - 1; - const collapsed = isColumnSection ? collapseColumns : collapseMetrics; - const setCollapse = isColumnSection - ? onCollapseColumnsChange - : onCollapseMetricsChange; - const showAll = isColumnSection ? showAllColumns : showAllMetrics; - const setShowAll = isColumnSection - ? onShowAllColumnsChange - : onShowAllMetricsChange; +export interface DatasourcePanelItemProps { + index: number; + style: CSSProperties; + data: { + flattenedItems: FlattenedItem[]; + folderMap: Map; + width: number; + onToggleCollapse: (folderId: string) => void; + collapsedFolderIds: Set; + }; +} + +const DatasourcePanelItem = ({ + index, + style, + data, +}: DatasourcePanelItemProps) => { + const { + flattenedItems, + folderMap, + width, + onToggleCollapse, + collapsedFolderIds, + } = data; + const item = flattenedItems[index]; const theme = useTheme(); - const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount; + const [labelRef, labelIsTruncated] = useCSSTextTruncation({ + isVertical: true, + isHorizontal: false, + }); + + const getTooltipNode = useCallback( + (folder: Folder) => { + let tooltipNode: ReactNode | null = null; + if (labelIsTruncated) { + tooltipNode = ( +
+ {t('Name')}: {folder.name} +
+ ); + } + if (folder.description) { + tooltipNode = ( +
+ {tooltipNode} +
+ {t('Description')}: {folder.description} +
+
+ ); + } + return tooltipNode; + }, + [labelIsTruncated], + ); + + if (!item) return null; + + const folder = folderMap.get(item.folderId); + if (!folder) return null; + + const indentation = item.depth * theme.gridUnit * 4; return (
- {index === HEADER_LINE && ( - setCollapse(!collapsed)}> - - {isColumnSection ? t('Columns') : t('Metrics')} - - {collapsed ? ( - - ) : ( - - )} + {item.type === 'header' && ( + onToggleCollapse(folder.id)}> + + + {folder.name} + {collapsedFolderIds.has(folder.id) ? ( + + ) : ( + + )} + + )} - {index === SUBTITLE_LINE && !collapsed && ( + + {item.type === 'subtitle' && (
= ({ index, style, data }) => { flex-shrink: 0; `} > - {isColumnSection - ? t(`Showing %s of %s`, columnSlice?.length, totalColumns) - : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)} + {t(`Showing %s of %s items`, item.showingItems, item.totalItems)}
- {hiddenCount > 0 && ( - {t(`%s ineligible item(s) are hidden`, hiddenCount)} - )}
)} - {index > SUBTITLE_LINE && index < BOTTOM_LINE && ( + + {item.type === 'item' && item.item && ( )} - {index === BOTTOM_LINE && - !collapsed && - (isColumnSection - ? totalColumns > DEFAULT_MAX_COLUMNS_LENGTH - : totalMetrics > DEFAULT_MAX_METRICS_LENGTH) && ( - - - - )} + + {item.type === 'divider' && ( + + )} ); }; diff --git a/superset-frontend/src/explore/components/DatasourcePanel/fixtures.tsx b/superset-frontend/src/explore/components/DatasourcePanel/fixtures.tsx index 1840ad7a67a..0e572ad002a 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/fixtures.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/fixtures.tsx @@ -26,6 +26,7 @@ export const columns = [ filterable: true, groupby: true, id: 516, + uuid: '516', is_dttm: false, python_date_format: null, type: 'DOUBLE', @@ -40,6 +41,7 @@ export const columns = [ filterable: true, groupby: true, id: 477, + uuid: '477', is_dttm: false, python_date_format: null, type: 'VARCHAR', @@ -52,7 +54,8 @@ export const columns = [ expression: null, filterable: true, groupby: true, - id: 516, + id: 517, + uuid: '517', is_dttm: false, python_date_format: null, type: 'INT', @@ -70,6 +73,7 @@ const metricsFiltered = { description: null, expression: '', id: 56, + uuid: '56', is_certified: true, metric_name: 'metric_end_certified', verbose_name: '', @@ -84,6 +88,7 @@ const metricsFiltered = { description: null, expression: '', id: 57, + uuid: '57', is_certified: false, metric_name: 'metric_end', verbose_name: '', diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx index 30c53ffd2d1..e1bb482a5e4 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx @@ -28,7 +28,6 @@ import { import { ControlConfig } from '@superset-ui/chart-controls'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList as List } from 'react-window'; import { matchSorter, rankings } from 'match-sorter'; import Alert from 'src/components/Alert'; @@ -39,22 +38,19 @@ import { FAST_DEBOUNCE } from 'src/constants'; import { ExploreActions } from 'src/explore/actions/exploreActions'; import Control from 'src/explore/components/Control'; import { useDebounceValue } from 'src/hooks/useDebounceValue'; -import DatasourcePanelItem, { - ITEM_HEIGHT, - DataSourcePanelColumn, - DEFAULT_MAX_COLUMNS_LENGTH, - DEFAULT_MAX_METRICS_LENGTH, -} from './DatasourcePanelItem'; import { DndItemType } from '../DndItemType'; -import { DndItemValue } from './types'; +import { DatasourceFolder, DatasourcePanelColumn, DndItemValue } from './types'; import { DropzoneContext } from '../ExploreContainer'; +import { DatasourceItems } from './DatasourceItems'; +import { transformDatasourceWithFolders } from './transformDatasourceFolders'; interface DatasourceControl extends Omit { datasource?: IDatasource; } export interface IDatasource { metrics: Metric[]; - columns: DataSourcePanelColumn[]; + columns: DatasourcePanelColumn[]; + folders?: DatasourceFolder[]; id: number; type: DatasourceType; database: { @@ -126,8 +122,18 @@ const StyledInfoboxWrapper = styled.div` const BORDER_WIDTH = 2; -const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) => - slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0)); +const sortColumns = (slice: DatasourcePanelColumn[]) => + [...slice] + .sort((col1, col2) => { + if (col1?.is_dttm && !col2?.is_dttm) { + return -1; + } + if (col2?.is_dttm && !col1?.is_dttm) { + return 1; + } + return 0; + }) + .sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0)); export default function DataSourcePanel({ datasource, @@ -137,7 +143,7 @@ export default function DataSourcePanel({ width, }: Props) { const [dropzones] = useContext(DropzoneContext); - const { columns: _columns, metrics } = datasource; + const { columns: _columns, metrics, folders: _folders } = datasource; const allowedColumns = useMemo(() => { const validators = Object.values(dropzones); @@ -152,21 +158,6 @@ export default function DataSourcePanel({ ); }, [dropzones, _columns]); - // display temporal column first - const columns = useMemo( - () => - [...allowedColumns].sort((col1, col2) => { - if (col1?.is_dttm && !col2?.is_dttm) { - return -1; - } - if (col2?.is_dttm && !col1?.is_dttm) { - return 1; - } - return 0; - }), - [allowedColumns], - ); - const allowedMetrics = useMemo(() => { const validators = Object.values(dropzones); return metrics.filter(metric => @@ -176,21 +167,15 @@ export default function DataSourcePanel({ ); }, [dropzones, metrics]); - const hiddenColumnCount = _columns.length - allowedColumns.length; - const hiddenMetricCount = metrics.length - allowedMetrics.length; const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); const [inputValue, setInputValue] = useState(''); - const [showAllMetrics, setShowAllMetrics] = useState(false); - const [showAllColumns, setShowAllColumns] = useState(false); - const [collapseMetrics, setCollapseMetrics] = useState(false); - const [collapseColumns, setCollapseColumns] = useState(false); const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE); const filteredColumns = useMemo(() => { if (!searchKeyword) { - return columns ?? []; + return allowedColumns ?? []; } - return matchSorter(columns, searchKeyword, { + return matchSorter(allowedColumns, searchKeyword, { keys: [ { key: 'verbose_name', @@ -211,7 +196,7 @@ export default function DataSourcePanel({ ], keepDiacritics: true, }); - }, [columns, searchKeyword]); + }, [allowedColumns, searchKeyword]); const filteredMetrics = useMemo(() => { if (!searchKeyword) { @@ -244,22 +229,21 @@ export default function DataSourcePanel({ }); }, [allowedMetrics, searchKeyword]); - const metricSlice = useMemo( - () => - showAllMetrics - ? filteredMetrics - : filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH), - [filteredMetrics, showAllMetrics], + const sortedColumns = useMemo( + () => sortColumns(filteredColumns), + [filteredColumns], ); - const columnSlice = useMemo( + const folders = useMemo( () => - showAllColumns - ? sortCertifiedFirst(filteredColumns) - : sortCertifiedFirst( - filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH), - ), - [filteredColumns, showAllColumns], + transformDatasourceWithFolders( + filteredMetrics, + sortedColumns, + _folders, + allowedMetrics, + allowedColumns, + ), + [_folders, filteredMetrics, sortedColumns], ); const showInfoboxCheck = () => { @@ -324,57 +308,17 @@ export default function DataSourcePanel({ )} {({ height }: { height: number }) => ( - - {DatasourcePanelItem} - + folders={folders} + /> )} ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - columnSlice, - inputValue, - filteredColumns.length, - filteredMetrics.length, - metricSlice, - showAllColumns, - showAllMetrics, - collapseMetrics, - collapseColumns, - datasourceIsSaveable, - width, - ], + [inputValue, datasourceIsSaveable, width, folders], ); return ( diff --git a/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.test.ts b/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.test.ts new file mode 100644 index 00000000000..6099b72c1ea --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.test.ts @@ -0,0 +1,208 @@ +/** + * 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 { Metric } from '@superset-ui/core'; +import { transformDatasourceWithFolders } from './transformDatasourceFolders'; +import { DatasourceFolder, DatasourcePanelColumn } from './types'; + +const mockMetrics: Metric[] = [ + { metric_name: 'metric1', uuid: 'metric1-uuid', expression: 'SUM(col1)' }, + { metric_name: 'metric2', uuid: 'metric2-uuid', expression: 'AVG(col2)' }, + { metric_name: 'metric3', uuid: 'metric3-uuid', expression: 'COUNT(*)' }, +]; + +const mockColumns: DatasourcePanelColumn[] = [ + { column_name: 'column1', uuid: 'column1-uuid', type: 'STRING' }, + { column_name: 'column2', uuid: 'column2-uuid', type: 'NUMERIC' }, + { column_name: 'column3', uuid: 'column3-uuid', type: 'DATETIME' }, +]; + +test('transforms data into default folders when no folder config is provided', () => { + const result = transformDatasourceWithFolders( + mockMetrics, + mockColumns, + undefined, + mockMetrics, + mockColumns, + ); + + expect(result).toHaveLength(2); + + expect(result[0].id).toBe('metrics-default'); + expect(result[0].name).toBe('Metrics'); + expect(result[0].items).toHaveLength(3); + expect(result[0].items[0].uuid).toBe('metric1-uuid'); + expect(result[0].items[0].type).toBe('metric'); + + expect(result[1].id).toBe('columns-default'); + expect(result[1].name).toBe('Columns'); + expect(result[1].items).toHaveLength(3); + expect(result[1].items[0].uuid).toBe('column1-uuid'); + expect(result[1].items[0].type).toBe('column'); +}); + +test('transforms data according to folder configuration', () => { + const folderConfig: DatasourceFolder[] = [ + { + uuid: 'folder1', + type: 'folder', + name: 'Important Metrics', + description: 'Key metrics folder', + children: [ + { type: 'metric', uuid: 'metric1-uuid', name: 'metric1' }, + { type: 'metric', uuid: 'metric2-uuid', name: 'metric2' }, + ], + }, + { + uuid: 'folder2', + type: 'folder', + name: 'Key Dimensions', + children: [{ type: 'column', uuid: 'column1-uuid', name: 'column1' }], + }, + ]; + + const result = transformDatasourceWithFolders( + mockMetrics, + mockColumns, + folderConfig, + mockMetrics, + mockColumns, + ); + + // We expect 4 folders: + // 1. Important Metrics (from config) + // 2. Key Dimensions (from config) + // 3. Metrics (default for unassigned metrics) + // 4. Columns (default for unassigned columns) + expect(result).toHaveLength(4); + + expect(result[0].id).toBe('folder1'); + expect(result[0].name).toBe('Important Metrics'); + expect(result[0].description).toBe('Key metrics folder'); + expect(result[0].items).toHaveLength(2); + expect(result[0].items[0].uuid).toBe('metric1-uuid'); + + expect(result[1].id).toBe('folder2'); + expect(result[1].name).toBe('Key Dimensions'); + expect(result[1].items).toHaveLength(1); + expect(result[1].items[0].uuid).toBe('column1-uuid'); + + expect(result[2].id).toBe('metrics-default'); + expect(result[2].items).toHaveLength(1); + expect(result[2].items[0].uuid).toBe('metric3-uuid'); + + expect(result[3].id).toBe('columns-default'); + expect(result[3].items).toHaveLength(2); +}); + +test('handles nested folder structures', () => { + const folderConfig: DatasourceFolder[] = [ + { + uuid: 'parent-folder', + type: 'folder', + name: 'Parent Folder', + children: [ + { + uuid: 'child-folder', + type: 'folder', + name: 'Child Folder', + children: [{ type: 'metric', uuid: 'metric1-uuid', name: 'metric1' }], + }, + { type: 'column', uuid: 'column1-uuid', name: 'column1' }, + ], + }, + ]; + + const result = transformDatasourceWithFolders( + mockMetrics, + mockColumns, + folderConfig, + mockMetrics, + mockColumns, + ); + + expect(result[0].id).toBe('parent-folder'); + expect(result[0].name).toBe('Parent Folder'); + expect(result[0].items).toHaveLength(1); + expect(result[0].subFolders).toHaveLength(1); + + const childFolder = result[0].subFolders![0]; + expect(childFolder.id).toBe('child-folder'); + expect(childFolder.name).toBe('Child Folder'); + expect(childFolder.items).toHaveLength(1); + expect(childFolder.parentId).toBe('parent-folder'); +}); + +test('handles empty children arrays', () => { + const folderConfig: DatasourceFolder[] = [ + { + uuid: 'empty-folder', + type: 'folder', + name: 'Empty Folder', + children: [], + }, + ]; + + const result = transformDatasourceWithFolders( + mockMetrics, + mockColumns, + folderConfig, + mockMetrics, + mockColumns, + ); + + expect(result[0].id).toBe('empty-folder'); + expect(result[0].name).toBe('Empty Folder'); + expect(result[0].items).toHaveLength(0); +}); + +test('handles non-existent metric and column UUIDs in folder config', () => { + const folderConfig: DatasourceFolder[] = [ + { + uuid: 'folder1', + type: 'folder', + name: 'Test Folder', + children: [ + { + type: 'metric', + uuid: 'non-existent-metric', + name: 'Missing Metric', + }, + { + type: 'column', + uuid: 'non-existent-column', + name: 'Missing Column', + }, + { type: 'metric', uuid: 'metric1-uuid', name: 'Existing Metric' }, + ], + }, + ]; + + const result = transformDatasourceWithFolders( + mockMetrics, + mockColumns, + folderConfig, + mockMetrics, + mockColumns, + ); + + expect(result[0].id).toBe('folder1'); + expect(result[0].items).toHaveLength(1); + expect(result[0].items[0].uuid).toBe('metric1-uuid'); +}); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.ts b/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.ts new file mode 100644 index 00000000000..dd7368b9dbd --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel/transformDatasourceFolders.ts @@ -0,0 +1,177 @@ +/** + * 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 { Metric, t } from '@superset-ui/core'; +import { + ColumnItem, + DatasourceFolder, + DatasourcePanelColumn, + Folder, + MetricItem, +} from './types'; + +const transformToFolderStructure = ( + metricsToDisplay: MetricItem[], + columnsToDisplay: ColumnItem[], + folderConfig: DatasourceFolder[] | undefined, + allMetrics: Metric[], + allColumns: DatasourcePanelColumn[], +): Folder[] => { + const metricsMap = new Map(); + const columnsMap = new Map(); + + metricsToDisplay.forEach(metric => { + metricsMap.set(metric.uuid, metric); + }); + + columnsToDisplay.forEach(column => { + columnsMap.set(column.uuid, column); + }); + + let metricsInFolders = 0; + let columnsInFolders = 0; + const processFolder = ( + datasourceFolder: DatasourceFolder, + parentId?: string, + ): Folder => { + const folder: Folder = { + id: datasourceFolder.uuid, + name: datasourceFolder.name, + description: datasourceFolder.description, + isCollapsed: false, + items: [], + totalItems: 0, + showingItems: 0, + parentId, + }; + + if (datasourceFolder.children && datasourceFolder.children.length > 0) { + if (!folder.subFolders) { + folder.subFolders = []; + } + + datasourceFolder.children.forEach(child => { + if (child.type === 'folder') { + const subFolder = processFolder(child as DatasourceFolder, folder.id); + folder.subFolders!.push(subFolder); + folder.totalItems += subFolder.totalItems; + folder.showingItems += subFolder.showingItems; + } else if (child.type === 'metric') { + folder.totalItems += 1; + metricsInFolders += 1; + const metric = metricsMap.get(child.uuid); + if (metric) { + folder.items.push(metric); + metricsMap.delete(metric.uuid); + folder.showingItems += 1; + } + } else if (child.type === 'column') { + folder.totalItems += 1; + columnsInFolders += 1; + const column = columnsMap.get(child.uuid); + if (column) { + folder.items.push(column); + columnsMap.delete(column.uuid); + folder.showingItems += 1; + } + } + }); + } + + return folder; + }; + + if (!folderConfig) { + return [ + { + id: 'metrics-default', + name: t('Metrics'), + isCollapsed: false, + items: metricsToDisplay, + totalItems: allMetrics.length, + showingItems: metricsToDisplay.length, + }, + { + id: 'columns-default', + name: t('Columns'), + isCollapsed: false, + items: columnsToDisplay, + totalItems: allColumns.length, + showingItems: columnsToDisplay.length, + }, + ]; + } + + const folders = folderConfig.map(config => processFolder(config)); + + const unassignedMetrics = metricsToDisplay.filter(metric => + metricsMap.has(metric.uuid), + ); + const unassignedColumns = columnsToDisplay.filter(column => + columnsMap.has(column.uuid), + ); + + if (unassignedMetrics.length > 0) { + folders.push({ + id: 'metrics-default', + name: t('Metrics'), + isCollapsed: false, + items: unassignedMetrics, + totalItems: allMetrics.length - metricsInFolders, + showingItems: unassignedMetrics.length, + }); + } + + if (unassignedColumns.length > 0) { + folders.push({ + id: 'columns-default', + name: t('Columns'), + isCollapsed: false, + items: unassignedColumns, + totalItems: allColumns.length - columnsInFolders, + showingItems: unassignedColumns.length, + }); + } + + return folders; +}; + +export const transformDatasourceWithFolders = ( + metricsToDisplay: Metric[], + columnsToDisplay: DatasourcePanelColumn[], + folderConfig: DatasourceFolder[] | undefined, + allMetrics: Metric[], + allColumns: DatasourcePanelColumn[], +): Folder[] => { + const metricsWithType: MetricItem[] = metricsToDisplay.map(metric => ({ + ...metric, + type: 'metric', + })); + const columnsWithType: ColumnItem[] = columnsToDisplay.map(column => ({ + ...column, + type: 'column', + })); + + return transformToFolderStructure( + metricsWithType, + columnsWithType, + folderConfig, + allMetrics, + allColumns, + ); +}; diff --git a/superset-frontend/src/explore/components/DatasourcePanel/types.ts b/superset-frontend/src/explore/components/DatasourcePanel/types.ts index 315eba93473..0639f3c8278 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/types.ts +++ b/superset-frontend/src/explore/components/DatasourcePanel/types.ts @@ -35,3 +35,59 @@ export function isDatasourcePanelDndItem( export function isSavedMetric(item: any): item is Metric { return item?.metric_name; } + +export type DatasourcePanelColumn = { + uuid: string; + id?: number; + is_dttm?: boolean | null; + description?: string | null; + expression?: string | null; + is_certified?: number | null; + column_name?: string | null; + name?: string | null; + type?: string; +}; + +export type DatasourceFolder = { + uuid: string; + type: 'folder'; + name: string; + description?: string; + children?: ( + | DatasourceFolder + | { type: 'metric'; uuid: string; name: string } + | { type: 'column'; uuid: string; name: string } + )[]; +}; + +export type MetricItem = Metric & { + type: 'metric'; +}; + +export type ColumnItem = DatasourcePanelColumn & { + type: 'column'; +}; + +export type FolderItem = MetricItem | ColumnItem; + +export interface Folder { + id: string; + name: string; + description?: string; + isCollapsed: boolean; + items: FolderItem[]; + subFolders?: Folder[]; + parentId?: string; + totalItems: number; + showingItems: number; // items shown after filtering +} + +export interface FlattenedItem { + type: 'header' | 'item' | 'divider' | 'subtitle'; + folderId: string; + depth: number; + item?: FolderItem; + height: number; + totalItems?: number; + showingItems?: number; +} diff --git a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx index 66468b4721f..c8bf92caa41 100644 --- a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx @@ -76,7 +76,7 @@ test('should only propagate dragging state when dragging the panel option', () = const { container, getByText } = render( {setup({ @@ -377,11 +381,11 @@ describe('when disallow_adhoc_metrics is set', () => { type={DndItemType.Column} /> {setup({ diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx index 91307b4dde4..4323520b8ee 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx @@ -334,7 +334,7 @@ test('cannot drop a duplicated item', () => { const { getByTestId } = render( <> @@ -362,7 +362,7 @@ test('can drop a saved metric when disallow_adhoc_metrics', () => { const { getByTestId } = render( <> { const { getByTestId, getAllByTestId } = render( <> { id: 1, type: DatasourceType.Table, columns: [{ column_name: 'a' }], - metrics: [{ metric_name: 'first' }, { metric_name: 'second' }], + metrics: [ + { metric_name: 'first', uuid: '1' }, + { metric_name: 'second', uuid: '2' }, + ], column_formats: {}, currency_formats: {}, verbose_map: {}, diff --git a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts index df1afd69a0a..b26ba8c1b4a 100644 --- a/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts +++ b/superset-frontend/src/explore/controlUtils/getControlValuesCompatibleWithDatasource.test.ts @@ -33,7 +33,7 @@ const sampleDatasource: Dataset = { { column_name: 'sample_column_3' }, { column_name: 'sample_column_4' }, ], - metrics: [{ metric_name: 'saved_metric_2' }], + metrics: [{ metric_name: 'saved_metric_2', uuid: '1' }], column_formats: {}, currency_formats: {}, verbose_map: {}, diff --git a/superset-frontend/src/explore/fixtures.tsx b/superset-frontend/src/explore/fixtures.tsx index eb1d0d6d662..211519590e9 100644 --- a/superset-frontend/src/explore/fixtures.tsx +++ b/superset-frontend/src/explore/fixtures.tsx @@ -133,7 +133,10 @@ export const exploreInitialData: ExplorePageInitialData = { id: 8, type: DatasourceType.Table, columns: [{ column_name: 'a' }], - metrics: [{ metric_name: 'first' }, { metric_name: 'second' }], + metrics: [ + { metric_name: 'first', uuid: '1' }, + { metric_name: 'second', uuid: '2' }, + ], column_formats: {}, currency_formats: {}, verbose_map: {}, diff --git a/superset-frontend/src/features/datasets/types.ts b/superset-frontend/src/features/datasets/types.ts index e0afb076706..63f1678d799 100644 --- a/superset-frontend/src/features/datasets/types.ts +++ b/superset-frontend/src/features/datasets/types.ts @@ -41,7 +41,7 @@ export type ColumnObject = { type MetricObject = { id: number; - uuid: number; + uuid: string; expression?: string; description?: string; metric_name: string; From c1eeb63d8957027562ca2934258c02da9dc74799 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Mon, 14 Apr 2025 10:53:02 -0700 Subject: [PATCH 04/14] fix: `master` builds are failing while trying to push report to cypress (#33124) --- .github/workflows/superset-e2e.yml | 4 ++-- .../Chart/DrillDetail/DrillDetailMenuItems.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index a21e46b659f..6997abf408a 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -50,8 +50,8 @@ jobs: PYTHONPATH: ${{ github.workspace }} REDIS_PORT: 16379 GITHUB_TOKEN: ${{ github.token }} - # use the dashboard feature when running manually OR merging to master - USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true'|| (github.ref == 'refs/heads/master' && 'true') || 'false' }} + # Only use dashboard when explicitly requested via workflow_dispatch + USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true' || 'false' }} services: postgres: image: postgres:16-alpine diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx index 4f661a1e463..1e6bc81df04 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx @@ -396,7 +396,7 @@ test('context menu for supported chart, dimensions, filter B', async () => { await expectDrillToDetailByDimension(filterB); }); -test('context menu for supported chart, dimensions, all filters', async () => { +test.skip('context menu for supported chart, dimensions, all filters', async () => { const filters = [filterA, filterB]; setupMenu(filters); await expectDrillToDetailByAll(filters); From 839215148ab27dbf32598ef01551091b7b5727ae Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Mon, 14 Apr 2025 20:39:09 +0200 Subject: [PATCH 05/14] feat(explore): X-axis sort by specific metric when more than 1 metric is set (#33116) --- UPDATING.md | 3 +- .../src/sections/echartsTimeSeriesQuery.tsx | 4 - .../src/shared-controls/customControls.tsx | 85 ++++--------- .../src/Timeseries/transformProps.ts | 10 +- ...f_merge_x_axis_sort_series_with_x_axis_.py | 114 ++++++++++++++++++ 5 files changed, 146 insertions(+), 70 deletions(-) create mode 100644 superset/migrations/versions/2025-04-13_22-10_378cecfdba9f_merge_x_axis_sort_series_with_x_axis_.py diff --git a/UPDATING.md b/UPDATING.md index 077f43cdd89..41f6e7d6784 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -23,7 +23,8 @@ This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. ## Next - +- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`. +There's a migration added that can potentially affect a significant number of existing charts. - [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed. - [31976](https://github.com/apache/superset/pull/31976) Removed the `DISABLE_LEGACY_DATASOURCE_EDITOR` feature flag. The previous value of the feature flag was `True` and now the feature is permanently removed. - [31959](https://github.com/apache/superset/pull/32000) Removes CSV_UPLOAD_MAX_SIZE config, use your web server to control file upload size. diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx index b293e78443b..b8ec643c0bd 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx @@ -23,8 +23,6 @@ import { xAxisForceCategoricalControl, xAxisSortAscControl, xAxisSortControl, - xAxisSortSeriesAscendingControl, - xAxisSortSeriesControl, } from '../shared-controls'; const controlsWithoutXAxis: ControlSetRow[] = [ @@ -55,8 +53,6 @@ export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = { [xAxisForceCategoricalControl], [xAxisSortControl], [xAxisSortAscControl], - [xAxisSortSeriesControl], - [xAxisSortSeriesAscendingControl], ...controlsWithoutXAxis, ], }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx index bdd6d1b82cc..971e2b39fc5 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx @@ -57,9 +57,7 @@ export const contributionModeControl = { }; const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) => - isSortable(controls) && - ensureIsArray(controls?.groupby?.value).length === 0 && - ensureIsArray(controls?.metrics?.value).length === 1; + isSortable(controls); // TODO: Expand this aggregation options list to include all backend-supported aggregations. // TODO: Migrate existing chart types (Pivot Table, etc.) to use this shared control. @@ -87,15 +85,6 @@ export const aggregationControl = { }, }; -const xAxisMultiSortVisibility = ({ - controls, -}: { - controls: ControlStateMapping; -}) => - isSortable(controls) && - (!!ensureIsArray(controls?.groupby?.value).length || - ensureIsArray(controls?.metrics?.value).length > 1); - export const xAxisSortControl = { name: 'x_axis_sort', config: { @@ -104,7 +93,7 @@ export const xAxisSortControl = { state.form_data?.orientation === 'horizontal' ? t('Y-Axis Sort By') : t('X-Axis Sort By'), - description: t('Decides which column to sort the base axis by.'), + description: t('Decides which column or measure to sort the base axis by.'), shouldMapStateToProps: () => true, mapStateToProps: (state: ControlPanelState, controlState: ControlState) => { const { controls, datasource } = state; @@ -112,23 +101,35 @@ export const xAxisSortControl = { const columns = [controls?.x_axis?.value as QueryFormColumn].filter( Boolean, ); + const isSingleSortAvailable = + ensureIsArray(controls?.groupby?.value).length === 0; + const isMultiSortAvailable = + !!ensureIsArray(controls?.groupby?.value).length || + ensureIsArray(controls?.metrics?.value).length > 1; const metrics = [ ...ensureIsArray(controls?.metrics?.value as QueryFormMetric), controls?.timeseries_limit_metric?.value as QueryFormMetric, ].filter(Boolean); const metricLabels = [...new Set(metrics.map(getMetricLabel))]; const options = [ - ...columns.map(column => { - const value = getColumnLabel(column); - return { - value, - label: dataset?.verbose_map?.[value] || value, - }; - }), - ...metricLabels.map(value => ({ - value, - label: dataset?.verbose_map?.[value] || value, - })), + ...(isSingleSortAvailable + ? [ + ...columns.map(column => { + const value = getColumnLabel(column); + return { value, label: dataset?.verbose_map?.[value] || value }; + }), + ...metricLabels.map(value => ({ + value, + label: dataset?.verbose_map?.[value] || value, + })), + ] + : []), + ...(isMultiSortAvailable + ? SORT_SERIES_CHOICES.map(choice => ({ + value: choice[0], + label: choice[1], + })) + : []), ]; const shouldReset = !( @@ -157,7 +158,7 @@ export const xAxisSortAscControl = { state.form_data?.orientation === 'horizontal' ? t('Y-Axis Sort Ascending') : t('X-Axis Sort Ascending'), - default: true, + default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_ascending, description: t('Whether to sort ascending or descending on the base Axis.'), visibility: ({ controls }: { controls: ControlStateMapping }) => controls?.x_axis_sort?.value !== undefined && @@ -184,37 +185,3 @@ export const xAxisForceCategoricalControl = { shouldMapStateToProps: () => true, }, }; - -export const xAxisSortSeriesControl = { - name: 'x_axis_sort_series', - config: { - type: 'SelectControl', - freeForm: false, - label: (state: ControlPanelState) => - state.form_data?.orientation === 'horizontal' - ? t('Y-Axis Sort By') - : t('X-Axis Sort By'), - choices: SORT_SERIES_CHOICES, - default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_type, - renderTrigger: true, - description: t('Decides which measure to sort the base axis by.'), - visibility: xAxisMultiSortVisibility, - }, -}; - -export const xAxisSortSeriesAscendingControl = { - name: 'x_axis_sort_series_ascending', - config: { - type: 'CheckboxControl', - label: (state: ControlPanelState) => - state.form_data?.orientation === 'horizontal' - ? t('Y-Axis Sort Ascending') - : t('X-Axis Sort Ascending'), - default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_ascending, - description: t('Whether to sort ascending or descending on the base Axis.'), - renderTrigger: true, - visibility: ({ controls }: { controls: ControlStateMapping }) => - controls?.x_axis_sort_series?.value !== undefined && - xAxisMultiSortVisibility({ controls }), - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 0fb392a12da..6e30306056e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -179,8 +179,8 @@ export default function transformProps( xAxisBounds, xAxisForceCategorical, xAxisLabelRotation, - xAxisSortSeries, - xAxisSortSeriesAscending, + xAxisSort, + xAxisSortAsc, xAxisTimeFormat, xAxisTitle, xAxisTitleMargin, @@ -242,10 +242,8 @@ export default function transformProps( isHorizontal, sortSeriesType, sortSeriesAscending, - xAxisSortSeries: isMultiSeries ? xAxisSortSeries : undefined, - xAxisSortSeriesAscending: isMultiSeries - ? xAxisSortSeriesAscending - : undefined, + xAxisSortSeries: isMultiSeries ? xAxisSort : undefined, + xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined, }, ); const showValueIndexes = extractShowValueIndexes(rawSeries, { diff --git a/superset/migrations/versions/2025-04-13_22-10_378cecfdba9f_merge_x_axis_sort_series_with_x_axis_.py b/superset/migrations/versions/2025-04-13_22-10_378cecfdba9f_merge_x_axis_sort_series_with_x_axis_.py new file mode 100644 index 00000000000..a8a247141a9 --- /dev/null +++ b/superset/migrations/versions/2025-04-13_22-10_378cecfdba9f_merge_x_axis_sort_series_with_x_axis_.py @@ -0,0 +1,114 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""merge_x_axis_sort_series_with_x_axis_sort + +Revision ID: 378cecfdba9f +Revises: 32bf93dfe2a4 +Create Date: 2025-04-13 22:10:10.836273 + +""" + +# revision identifiers, used by Alembic. +revision = "378cecfdba9f" +down_revision = "32bf93dfe2a4" + +from alembic import op # noqa: E402 +from sqlalchemy import Column, Integer, String, Text # noqa: E402 +from sqlalchemy.ext.declarative import declarative_base # noqa: E402 + +from superset import db # noqa: E402 +from superset.migrations.shared.utils import paginated_update # noqa: E402 +from superset.utils import json # noqa: E402 + +Base = declarative_base() + + +class Slice(Base): + __tablename__ = "slices" + + id = Column(Integer, primary_key=True) + params = Column(Text) + viz_type = Column(String(250)) + + +timeseries_charts = [ + "echarts_timeseries_bar", + "echarts_area", + "echarts_timeseries_line", + "echarts_timeseries_scatter", + "echarts_timeseries_smooth", + "echarts_timeseries_step", + "echarts_timeseries", +] + + +def upgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + for slc in paginated_update( + session.query(Slice).filter(Slice.viz_type.in_(timeseries_charts)) + ): + try: + params = json.loads(slc.params) + + # x_axis_sort_series only appears when there are multiple metrics or groupby + if not ( + ("metrics" in params and len(params["metrics"]) > 1) + or ("groupby" in params and len(params["groupby"]) > 0) + ): + continue + + if "x_axis_sort_series" in params: + params["x_axis_sort"] = params.pop("x_axis_sort_series") + if "x_axis_sort_series_ascending" in params: + params["x_axis_sort_asc"] = params.pop("x_axis_sort_series_ascending") + + slc.params = json.dumps(params, sort_keys=True) + except Exception: # noqa: S110 + pass + + session.commit() + session.close() + + +def downgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + + for slc in paginated_update( + session.query(Slice).filter(Slice.viz_type.in_(timeseries_charts)) + ): + try: + params = json.loads(slc.params) + # x_axis_sort_series only appears when there are multiple metrics or groupby + if not ( + ("metrics" in params and len(params["metrics"]) > 1) + or ("groupby" in params and len(params["groupby"]) > 0) + ): + continue + + if "x_axis_sort" in params: + params["x_axis_sort_series"] = params.pop("x_axis_sort") + if "x_axis_sort_asc" in params: + params["x_axis_sort_series_ascending"] = params.pop("x_axis_sort_asc") + + slc.params = json.dumps(params, sort_keys=True) + except Exception: # noqa: S110 + pass + + session.commit() + session.close() From 2233c02720f30e01198e2e8a1367902d29c59729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B6xtermann?= Date: Mon, 14 Apr 2025 21:20:39 +0200 Subject: [PATCH 06/14] fix(playwright): allow screenshotting empty dashboards (#33107) --- superset/utils/webdriver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index 8493a1f364d..0ad2c5eb6b7 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -191,7 +191,6 @@ class WebDriverPlaywright(WebDriverProxy): # chart containers didn't render logger.debug("Wait for chart containers to draw at url: %s", url) slice_container_locator = page.locator(".chart-container") - slice_container_locator.first.wait_for() for slice_container_elem in slice_container_locator.all(): slice_container_elem.wait_for() except PlaywrightTimeout: From 8cb71b8d3b76d46c6d143baa739f5e6858bf33e2 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Mon, 14 Apr 2025 23:08:15 +0200 Subject: [PATCH 07/14] fix(plugin-chart-table): Don't render redundant items in column config when time comparison is enabled (#33126) --- .../plugin-chart-table/src/controlPanel.tsx | 78 ++++++++++++------- .../ColumnConfigControl.tsx | 5 +- .../controls/ColumnConfigControl/types.ts | 1 + 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index c9e3a7b628b..f6798b79848 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -37,15 +37,17 @@ import { import { ensureIsArray, GenericDataType, + getMetricLabel, isAdhocColumn, isPhysicalColumn, QueryFormColumn, + QueryFormMetric, QueryMode, SMART_DATE_ID, t, } from '@superset-ui/core'; -import { isEmpty } from 'lodash'; +import { isEmpty, last } from 'lodash'; import { PAGE_SIZE_OPTIONS } from './consts'; import { ColorSchemeEnum } from './types'; @@ -486,51 +488,69 @@ const config: ControlPanelConfig = { return true; }, mapStateToProps(explore, _, chart) { - const timeComparisonStatus = !isEmpty( - explore?.controls?.time_compare?.value, - ); - + const timeComparisonValue = + explore?.controls?.time_compare?.value; const { colnames: _colnames, coltypes: _coltypes } = chart?.queriesResponse?.[0] ?? {}; let colnames: string[] = _colnames || []; let coltypes: GenericDataType[] = _coltypes || []; const childColumnMap: Record = {}; + const timeComparisonColumnMap: Record = {}; - if (timeComparisonStatus) { + if (!isEmpty(timeComparisonValue)) { /** * Replace numeric columns with sets of comparison columns. */ const updatedColnames: string[] = []; const updatedColtypes: GenericDataType[] = []; - colnames.forEach((colname, index) => { - if (coltypes[index] === GenericDataType.Numeric) { - const comparisonColumns = - generateComparisonColumns(colname); - comparisonColumns.forEach((name, idx) => { - updatedColnames.push(name); - updatedColtypes.push( - ...generateComparisonColumnTypes(4), - ); - - if (idx === 0 && name.startsWith('Main ')) { - childColumnMap[name] = false; - } else { - childColumnMap[name] = true; - } - }); - } else { - updatedColnames.push(colname); - updatedColtypes.push(coltypes[index]); - childColumnMap[colname] = false; - } - }); + colnames + .filter( + colname => + last(colname.split('__')) !== timeComparisonValue, + ) + .forEach((colname, index) => { + if ( + explore.form_data.metrics?.some( + metric => getMetricLabel(metric) === colname, + ) || + explore.form_data.percent_metrics?.some( + (metric: QueryFormMetric) => + getMetricLabel(metric) === colname, + ) + ) { + const comparisonColumns = + generateComparisonColumns(colname); + comparisonColumns.forEach((name, idx) => { + updatedColnames.push(name); + updatedColtypes.push( + ...generateComparisonColumnTypes(4), + ); + timeComparisonColumnMap[name] = true; + if (idx === 0 && name.startsWith('Main ')) { + childColumnMap[name] = false; + } else { + childColumnMap[name] = true; + } + }); + } else { + updatedColnames.push(colname); + updatedColtypes.push(coltypes[index]); + childColumnMap[colname] = false; + timeComparisonColumnMap[colname] = false; + } + }); colnames = updatedColnames; coltypes = updatedColtypes; } return { - columnsPropsObject: { colnames, coltypes, childColumnMap }, + columnsPropsObject: { + colnames, + coltypes, + childColumnMap, + timeComparisonColumnMap, + }, }; }, }, diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx index a30f68d02f0..e8d67aa018f 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx @@ -38,6 +38,7 @@ export type ColumnConfigControlProps = colnames: string[]; coltypes: GenericDataType[]; childColumnMap?: Record; + timeComparisonColumnMap?: Record; }; configFormLayout?: ColumnConfigFormLayout; appliedColumnNames?: string[]; @@ -87,6 +88,8 @@ export default function ColumnConfigControl({ type: coltypes?.[idx], config: value?.[col] || {}, isChildColumn: columnsPropsObject?.childColumnMap?.[col] ?? false, + isTimeComparisonColumn: + columnsPropsObject?.timeComparisonColumnMap?.[col] ?? false, }; }); return configs; @@ -136,7 +139,7 @@ export default function ColumnConfigControl({ column={col} onChange={config => setColumnConfig(col.name, config as T)} configFormLayout={ - col.isChildColumn + col.isTimeComparisonColumn ? ({ [col.type ?? GenericDataType.String]: [ { diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts index 5a8c4b5c393..1e2e09c9c1d 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts @@ -41,6 +41,7 @@ export type ColumnConfig = { */ export interface ColumnConfigInfo { isChildColumn: boolean; + isTimeComparisonColumn: boolean; name: string; type?: GenericDataType; config: JsonObject; From 45c77a1976d7079c7e259fc6514bde28ac1a8c9c Mon Sep 17 00:00:00 2001 From: Felipe Granado <31190972+felipegranado@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:06:54 -0300 Subject: [PATCH 08/14] chore(translations): Update PT-BR language (partial) (#29828) Co-authored-by: Evan Rusackas --- .../pt_BR/LC_MESSAGES/messages.po | 501 ++++++++++-------- 1 file changed, 269 insertions(+), 232 deletions(-) diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.po b/superset/translations/pt_BR/LC_MESSAGES/messages.po index 6aec74f7517..aa93672b7a4 100644 --- a/superset/translations/pt_BR/LC_MESSAGES/messages.po +++ b/superset/translations/pt_BR/LC_MESSAGES/messages.po @@ -75,31 +75,31 @@ msgstr "Erro: %(text)s" #, fuzzy msgid " (excluded)" -msgstr "(excluĆ­do)" +msgstr " (excluĆ­do)" #, fuzzy msgid "" " Set the opacity to 0 if you do not want to override the color specified " "in the GeoJSON" msgstr "" -"Defina opacidade a 0 se vocĆŖ nĆ£o quer sobrepor a cor especificada no " -"GeoJSON" +" Defina opacidade para 0 se vocĆŖ nĆ£o quer sobrepor a cor especificada " +"no GeoJSON" #, fuzzy msgid " a dashboard OR " -msgstr "um painel OU" +msgstr " um painel OU " #, fuzzy msgid " a new one" -msgstr "um novo" +msgstr " um novo" #, fuzzy msgid " expression which needs to adhere to the " -msgstr "expressĆ£o necessĆ”ria para aderir ao" +msgstr " expressĆ£o necessĆ”ria para aderir ao " #, fuzzy msgid " source code of Superset's sandboxed parser" -msgstr "código-fonte do analisador em Ć”rea restrita do Superset" +msgstr " código-fonte do analisador em Ć”rea restrita do Superset" #, fuzzy msgid "" @@ -131,25 +131,23 @@ msgstr "" #, fuzzy msgid " to add calculated columns" -msgstr "para adicionar colunas calculadas" +msgstr " para adicionar colunas calculadas" #, fuzzy msgid " to add metrics" -msgstr "para adicionar mĆ©tricas" +msgstr " para adicionar mĆ©tricas" #, fuzzy msgid " to edit or add columns and metrics." -msgstr "para editar ou adicionar colunas e mĆ©tricas." +msgstr " para editar ou adicionar colunas e mĆ©tricas." #, fuzzy msgid " to mark a column as a time column" -msgstr "para marcar uma coluna como uma coluna de tempo" +msgstr " para marcar uma coluna como uma coluna de tempo" #, fuzzy msgid " to open SQL Lab. From there you can save the query as a dataset." -msgstr "" -"para abrir o SQL Lab. De lĆ” vocĆŖ pode salvar a consulta como um conjunto " -"de dados." +msgstr " para abrir o SQL Lab. De lĆ” vocĆŖ pode salvar a consulta como um conjunto de dados." #, fuzzy msgid " to visualize your data." @@ -168,7 +166,7 @@ msgstr "% do pai" #, fuzzy, python-format msgid "% of total" -msgstr "% total" +msgstr "% do total" #, python-format msgid "%(dialect)s cannot be used as a data source for security reasons." @@ -200,15 +198,15 @@ msgstr "%(object)s nĆ£o existe neste banco de dados." #, python-format msgid "%(other)s charts will appear here" -msgstr "%(other)s grĆ”ficos irĆ£o aparecer aqui" +msgstr "%(other)s grĆ”ficos aparecerĆ£o aqui" #, python-format msgid "%(other)s dashboards will appear here" -msgstr "%(other)s painĆ©is irĆ£o aparecer aqui" +msgstr "%(other)s painĆ©is aparecerĆ£o aqui" #, python-format msgid "%(other)s recents will appear here" -msgstr "%(other)s recentes irĆ£o aparecer aqui" +msgstr "%(other)s recentes aparecerĆ£o aqui" #, python-format msgid "%(other)s saved queries will appear here" @@ -307,13 +305,14 @@ msgstr "%s coluna(s)" #, python-format msgid "%s ineligible item(s) are hidden" -msgstr "" +msgstr "%s Ć­tens inelegĆ­veis estĆ£o ocultos" #, python-format msgid "" "%s items could not be tagged because you don’t have edit permissions to " "all selected objects." -msgstr "" +msgstr "%s itens nĆ£o podem ser marcados porque vocĆŖ nĆ£o tem permissĆ£o de edição para " +"todos os objetos selecionados." #, python-format msgid "%s operator(s)" @@ -341,7 +340,7 @@ msgstr[1] "" #, python-format msgid "%s saved metric(s)" -msgstr "%s salvos mĆ©trica(s)" +msgstr "%s mĆ©trica(s) salva(s)" #, python-format msgid "%s updated" @@ -359,7 +358,7 @@ msgid "(Removed)" msgstr "(Removido)" msgid "(deleted or invalid type)" -msgstr "(excluĆ­do ou invĆ”lido digite)" +msgstr "(tipo excluĆ­do ou invĆ”lido)" msgid "(no description, click to see stack trace)" msgstr "(sem descrição , clique para ver rastreamento de pilha)" @@ -439,7 +438,7 @@ msgstr "frequĆŖncia de 1 minuto" #, fuzzy msgid "1 month ago" -msgstr "mĆŖs" +msgstr "1 mĆŖs atrĆ”s" msgid "1 month end frequency" msgstr "1 mĆŖs de frequĆŖncia final" @@ -514,7 +513,7 @@ msgid "2/98 percentiles" msgstr "2/98 percentis" msgid "22" -msgstr "" +msgstr "22" #, fuzzy msgid "28 days" @@ -528,11 +527,11 @@ msgstr "2D" #, fuzzy msgid "3 letter code of the country" -msgstr "todos os dias do mĆŖs" +msgstr "código de 3 letras do paĆ­s" #, fuzzy msgid "3 years" -msgstr "2 anos" +msgstr "3 anos" msgid "3 years ago" msgstr "3 anos atrĆ”s" @@ -542,7 +541,7 @@ msgstr "30 dias" #, fuzzy msgid "30 days ago" -msgstr "28 dias atrĆ”s" +msgstr "30 dias atrĆ”s" #, fuzzy msgid "30 minute" @@ -610,10 +609,10 @@ msgid ":" msgstr ":" msgid "< (Smaller than)" -msgstr "< (menor que)" +msgstr "< (Menor que)" msgid "<= (Smaller or equal)" -msgstr "<= (menor ou equal)" +msgstr "<= (Menor ou igual)" msgid "" msgstr "" @@ -637,11 +636,14 @@ msgid "> (Larger than)" msgstr "> (Maior que)" msgid ">= (Larger or equal)" -msgstr ">= (Maior ou equal)" +msgstr ">= (Maior ou igual)" msgid "A Big Number" msgstr "Um grande nĆŗmero" +msgid "A comma separated list of columns that should be parsed as dates" +msgstr "Uma lista separada por vĆ­rgulas de colunas que devem ser analisadas como datas" + msgid "Select column names from a dropdown list that should be parsed as dates." msgstr "" "Selecione os nomes das colunas a serem analisadas como datas na lista " @@ -653,7 +655,7 @@ msgstr "" " permissĆ£o para fazer upload." msgid "A database port is required when connecting via SSH Tunnel." -msgstr "" +msgstr "Uma porta de banco de dados Ć© necessĆ”ria ao conectar via tĆŗnel SSH." msgid "A database with the same name already exists." msgstr "JĆ” existe um banco de dados com o mesmo nome." @@ -689,8 +691,7 @@ msgid "A list of tags that have been applied to this chart." msgstr "Uma lista de tags que foram aplicadas a esse grĆ”fico." msgid "A list of users who can alter the chart. Searchable by name or username." -msgstr "" -"Uma lista de UsuĆ”rios que podem alterar o grĆ”fico. PesquisĆ”vel por nome " +msgstr "Uma lista de usuĆ”rios que podem alterar o grĆ”fico. PesquisĆ”vel por nome " "ou nome de usuĆ”rio." msgid "A map of the world, that can indicate values in different countries." @@ -792,10 +793,10 @@ msgid "AUG" msgstr "AGO" msgid "AXIS TITLE MARGIN" -msgstr "MARGEM DO TƍTULO DO EIXO" +msgstr "MARGEM DO EIXO DO TƍTULO " msgid "AXIS TITLE POSITION" -msgstr "POSIƇƃO DO TƍTULO DO EIXO" +msgstr "POSIƇƃO DO EIXO DO TƍTULO" msgid "About" msgstr "Sobre" @@ -845,7 +846,7 @@ msgstr "Adicionar alerta" #, fuzzy msgid "Add BCC Recipients" -msgstr "recentes" +msgstr "Adicionar Recipientes BCC" #, fuzzy msgid "Add CC Recipients" @@ -855,16 +856,16 @@ msgid "Add CSS template" msgstr "Adicionar modelo CSS" msgid "Add Chart" -msgstr "Adicionar grĆ”fico" +msgstr "Adicionar GrĆ”fico" msgid "Add Column" -msgstr "Adicionar coluna" +msgstr "Adicionar Coluna" msgid "Add Dashboard" -msgstr "Adicionar painel" +msgstr "Adicionar Painel" msgid "Add Database" -msgstr "Adicionar Banco de dados" +msgstr "Adicionar Banco de Dados" msgid "Add Log" msgstr "Adicionar Log" @@ -873,21 +874,21 @@ msgid "Add Metric" msgstr "Adicionar MĆ©trica" msgid "Add Report" -msgstr "Adicionar relatório" +msgstr "Adicionar Relatório" #, fuzzy msgid "Add Rule" -msgstr "Fórmula ruim." +msgstr "Adicionar Regra" #, fuzzy msgid "Add Tag" -msgstr "marca" +msgstr "Adicionar Marca" msgid "Add a Plugin" msgstr "Adicionar um Plugin" msgid "Add a dataset" -msgstr "Adicionar um conjunto de dados" +msgstr "Adicionar um Conjunto de Dados" msgid "Add a new tab" msgstr "Adicionar uma nova aba" @@ -930,17 +931,17 @@ msgstr "" "datasource\"modal" msgid "Add color for positive/negative change" -msgstr "" +msgstr "Adicionar cor para alteração positivo/negativo" msgid "Add cross-filter" msgstr "Adicionar filtro cruzado" msgid "Add custom scoping" -msgstr "" +msgstr "Adicionar escopo personalizado" #, fuzzy msgid "Add dataset columns here to group the pivot table columns." -msgstr "Colunas para agrupar nas colunas" +msgstr "Adicionar colunas aqui para agrupar as colunas dinĆ¢micas." msgid "Add delivery method" msgstr "Adicionar mĆ©todo de entrega" @@ -987,7 +988,7 @@ msgstr "Adicionar mĆ©trica" #, fuzzy msgid "Add metrics to dataset in \"Edit datasource\" modal" -msgstr "Adicionar MĆ©tricas para conjunto de dados em \"Edit datasource\"modal" +msgstr "Adicionar mĆ©tricas para conjunto de dados em \"Edit datasource\"modal" msgid "Add new color formatter" msgstr "Adicionar novo formatador de cores" @@ -1005,7 +1006,7 @@ msgid "Add sheet" msgstr "Adicionar planilha" msgid "Add tag to entities" -msgstr "" +msgstr "Adicionar tag Ć s entidades" msgid "Add the name of the chart" msgstr "Adicione o nome do grĆ”fico" @@ -1017,7 +1018,7 @@ msgid "Add to dashboard" msgstr "Adicionar ao painel" msgid "Add/Edit Filters" -msgstr "Adicionar/Editar filtros" +msgstr "Adicionar/Editar Filtros" msgid "Added" msgstr "Adicionado" @@ -1029,16 +1030,16 @@ msgstr[0] "Adicionado a 1 painel" msgstr[1] "" msgid "Additional Parameters" -msgstr "ParĆ¢metros adicionais" +msgstr "ParĆ¢metros Adicionais" msgid "Additional fields may be required" -msgstr "Adicional campos que podem ser necessĆ”rios" +msgstr "Campos adicionais podem ser necessĆ”rios" msgid "Additional information" msgstr "Informação adicional" msgid "Additional metadata" -msgstr "Metadados adicionais" +msgstr "Metadados Adicionais" msgid "Additional padding for legend." msgstr "Preenchimento adicional da legenda." @@ -1061,16 +1062,22 @@ msgid "" "Adds color to the chart symbols based on the positive or negative change " "from the comparison value." msgstr "" +"Adiciona cor aos sĆ­mbolos do grĆ”fico com base na mudanƧa positiva ou negativa " +"do valor de comparação." msgid "" "Adjust column settings such as specifying the columns to read, how " "duplicates are handled, column data types, and more." msgstr "" +"Ajuste as configuraƧƵes das colunas, como especificar as colunas a serem lidas, como " +"duplicatas sĆ£o tratadas, tipos de dados de coluna e muito mais." msgid "" "Adjust how spaces, blank lines, null values are handled and other file " "wide settings." msgstr "" +"Ajuste como espaƧos, linhas em branco, valores nulos e outros arquivos sĆ£o tratados " +"configuraƧƵes amplas." msgid "Adjust how this database will interact with SQL Lab." msgstr "Ajustar como esse banco de dados vai interagir com SQL Lab." @@ -1082,13 +1089,13 @@ msgid "Advanced" msgstr "AvanƧado" msgid "Advanced Analytics" -msgstr "AnĆ”lise avanƧada" +msgstr "AnĆ”lise AvanƧada" msgid "Advanced Data type" -msgstr "Tipo de dados avanƧado" +msgstr "Tipo de Dados AvanƧado" msgid "Advanced analytics" -msgstr "Analytics avanƧado" +msgstr "AnĆ”lise avanƧada" msgid "Advanced analytics Query A" msgstr "AnĆ”lise avanƧada Consulta A" @@ -1098,13 +1105,13 @@ msgstr "AnĆ”lise avanƧada Consulta B" #, fuzzy msgid "Advanced analytics post processing" -msgstr "AnĆ”lise avanƧada" +msgstr "Pós processamento da anĆ”lise avanƧada" msgid "Advanced data type" msgstr "Tipo de dados avanƧado" msgid "Advanced-Analytics" -msgstr "AnĆ”lise avanƧada" +msgstr "AnĆ”lise AvanƧada" msgid "After" msgstr "Depois de" @@ -1113,17 +1120,17 @@ msgid "Aggregate" msgstr "Agregado" msgid "Aggregate Mean" -msgstr "MĆ©dia agregada" +msgstr "MĆ©dia Agregada" msgid "Aggregate Sum" -msgstr "Soma agregada" +msgstr "Soma Agregada" msgid "" "Aggregate function applied to the list of points in each cluster to " "produce the cluster label." msgstr "" -"Função agregada aplicada Ć  lista de pontos em cada cluster para produzir " -"o rótulo do cluster." +"Função agregada aplicada Ć  lista de pontos em cada cluster para " +"produzir o rótulo do cluster." msgid "" "Aggregate function to apply when pivoting and computing the total rows " @@ -1141,7 +1148,7 @@ msgstr "" #, fuzzy msgid "Aggregation" -msgstr "agregar" +msgstr "Agregar" msgid "Aggregation function" msgstr "Função de agregação" @@ -1157,7 +1164,7 @@ msgstr "Condição de alerta" #, fuzzy msgid "Alert contents" -msgstr "ConteĆŗdo da cĆ©lula" +msgstr "ConteĆŗdo do alerta" msgid "Alert ended grace period." msgstr "O alerta terminou o perĆ­odo de carĆŖncia." @@ -1173,7 +1180,7 @@ msgstr "O alerta encontrou um erro durante a execução de uma consulta." #, fuzzy msgid "Alert is active" -msgstr "Relatórios por e-mail ativo" +msgstr "Alerta estĆ” ativo" msgid "Alert name" msgstr "Nome do alerta" @@ -1223,13 +1230,13 @@ msgid "All" msgstr "Todos" msgid "All Text" -msgstr "Todos os Textos" +msgstr "Todos o Texto" msgid "All charts" msgstr "Todos os grĆ”ficos" msgid "All charts/global scoping" -msgstr "" +msgstr "Todos os escopos grĆ”ficos/global" msgid "All filters" msgstr "Todos os filtros" @@ -1259,12 +1266,14 @@ msgid "Allow DML" msgstr "Permitir DML" msgid "Allow changing catalogs" -msgstr "" +msgstr "Permitir alteração de catĆ”logos" msgid "" "Allow column names to be changed to case insensitive format, if supported" " (e.g. Oracle, Snowflake)." msgstr "" +"Permitir que os nomes das colunas sejam alterados para um formato que nĆ£o diferencia maiĆŗsculas de minĆŗsculas, se houver suporte" +"(ex.: Oracle, Snowflake)." msgid "Allow columns to be rearranged" msgstr "Permitir que as colunas sejam reorganizadas" @@ -1366,7 +1375,7 @@ msgid "An error has occurred" msgstr "Ocorreu um erro" msgid "An error occurred" -msgstr "Ocorreu um erro" +msgstr "Um erro ocorreu" msgid "An error occurred saving dataset" msgstr "Ocorreu um erro ao salvar conjunto de dados" @@ -1564,13 +1573,13 @@ msgstr "Anotação" #, python-format msgid "Annotation Layer %s" -msgstr "Camada de anotação %s" +msgstr "Camada de Anotação %s" msgid "Annotation Layers" -msgstr "Camadas de anotação" +msgstr "Camadas de Anotação" msgid "Annotation Slice Configuration" -msgstr "Configuração de fatia de anotação" +msgstr "Configuração de Fatia de Anotação" msgid "Annotation could not be created." msgstr "NĆ£o foi possĆ­vel criar uma anotação." @@ -1662,7 +1671,7 @@ msgstr "AnotaƧƵes nĆ£o foram excluĆ­das." #, fuzzy msgid "Any" -msgstr "dia" +msgstr "Qualquer" msgid "Any additional detail to show in the certification tooltip." msgstr "" @@ -1733,7 +1742,7 @@ msgstr "Aplicar filtros" #, fuzzy msgid "Apply metrics on" -msgstr "Minha mĆ©trica" +msgstr "Aplicar mĆ©trica em" msgid "Apply to all panels" msgstr "Aplicar para todos painĆ©is" @@ -1751,7 +1760,7 @@ msgid "Are you sure you intend to overwrite the following values?" msgstr "Tem certeza de que pretende substituir os valores a seguir?" msgid "Are you sure you want to cancel?" -msgstr "Tem certeza que deseja cancelar ?" +msgstr "Tem certeza que deseja cancelar?" msgid "Are you sure you want to delete" msgstr "Tem certeza que deseja remover" @@ -1803,10 +1812,10 @@ msgstr "Tem certeza que deseja salvar e aplicar mudanƧas ?" #, fuzzy msgid "Area" -msgstr "Ć”rea de texto" +msgstr "Area" msgid "Area Chart" -msgstr "GrĆ”fico de Ć”rea" +msgstr "GrĆ”fico de Ɓrea" msgid "Area chart" msgstr "GrĆ”fico de Ć”rea" @@ -1846,13 +1855,13 @@ msgid "August" msgstr "Agosto" msgid "Authorization needed" -msgstr "" +msgstr "Autorização necessĆ”ria" msgid "Auto" -msgstr "Auto" +msgstr "AutomĆ”tico" msgid "Auto Zoom" -msgstr "Zoom automĆ”tico" +msgstr "Zoom AutomĆ”tico" msgid "Autocomplete" msgstr "Autocompletar" @@ -1922,18 +1931,16 @@ msgid "Bar" msgstr "Barra" msgid "Bar Chart" -msgstr "GrĆ”fico de barras" +msgstr "GrĆ”fico de Barras" msgid "Bar Chart (legacy)" -msgstr "GrĆ”fico de barras (legado)" +msgstr "GrĆ”fico de Barras (legado)" msgid "Bar Charts are used to show metrics as a series of bars." -msgstr "" -"Os grĆ”ficos de barras sĆ£o usados para mostrar as mĆ©tricas como uma sĆ©rie " -"de barras." +msgstr "Os grĆ”ficos de barras sĆ£o usados para mostrar as mĆ©tricas como uma sĆ©rie de barras" msgid "Bar Values" -msgstr "Valores de barra" +msgstr "Valores da Barra" msgid "Bar orientation" msgstr "Orientação da barra" @@ -1947,10 +1954,10 @@ msgid "Base layer map style. See Mapbox documentation: %s" msgstr "" msgid "Based on a metric" -msgstr "Com base em uma mĆ©trica" +msgstr "Baseado em uma mĆ©trica" msgid "Based on granularity, number of time periods to compare against" -msgstr "Com base na granularidade, nĆŗmero de perĆ­odos de tempo para comparação" +msgstr "Baseado na granularidade, nĆŗmero de perĆ­odos de tempo para comparação" msgid "Based on what should series be ordered on the chart and legend" msgstr "Com base no que as sĆ©ries devem ser ordenadas no grĆ”fico e na legenda" @@ -1981,7 +1988,7 @@ msgid "Big Number Font Size" msgstr "Tamanho da Fonte do NĆŗmero Grande" msgid "Big Number with Time Period Comparison" -msgstr "" +msgstr "NĆŗmero Grande com Comparação de PerĆ­odo de Tempo" msgid "Big Number with Trendline" msgstr "NĆŗmero grande com Trendline" @@ -1991,19 +1998,19 @@ msgid "Bins" msgstr "em" msgid "Bottom" -msgstr "Parte inferior" +msgstr "Inferior" msgid "Bottom Margin" msgstr "Margem Inferior" msgid "Bottom left" -msgstr "Parte inferior esquerda" +msgstr "Inferior esquerda" msgid "Bottom margin, in pixels, allowing for more room for axis labels" -msgstr "Margem Inferior, em pixels, permitindo mais espaƧo para o rótulo dos eixos" +msgstr "Margem inferior, em pixels, permitindo mais espaƧo para o rótulo dos eixos" msgid "Bottom right" -msgstr "Parte inferior direita" +msgstr "Inferior direita" msgid "Bottom to Top" msgstr "De baixo para cima" @@ -2083,17 +2090,17 @@ msgstr "GrĆ”fico de bolhas" #, fuzzy msgid "Bubble Chart (legacy)" -msgstr "GrĆ”fico de linhas (herdado)" +msgstr "GrĆ”fico de Linhas (herdado)" msgid "Bubble Color" -msgstr "Cor da bolha" +msgstr "Cor da Bolha" #, fuzzy msgid "Bubble Opacity" -msgstr "GrĆ”fico de bolhas" +msgstr "Opacidade das Bolhas" msgid "Bubble Size" -msgstr "Tamanho da bolha" +msgstr "Tamanho da Bolha" msgid "Bubble size" msgstr "Tamanho da bolha" @@ -2112,16 +2119,16 @@ msgid "Bulk select" msgstr "Seleção em bloco" msgid "Bulk tag" -msgstr "" +msgstr "Tag do Bloco" msgid "Bullet Chart" -msgstr "GrĆ”fico de marcadores" +msgstr "GrĆ”fico de Marcadores" msgid "Business" msgstr "Negócios" msgid "Business Data Type" -msgstr "Tipo de dados comerciais" +msgstr "Tipo de Dados Comerciais" msgid "" "By default, each filter loads at most 1000 choices at the initial page " @@ -2152,13 +2159,13 @@ msgid "CC recipients" msgstr "recentes" msgid "CREATE DATASET" -msgstr "CREATE DATASET" +msgstr "CRIAR DATASET" msgid "CREATE TABLE AS" -msgstr "CREATE TABLE AS" +msgstr "CRIAR TABELA COMO" msgid "CREATE VIEW AS" -msgstr "CREATE VIEW AS" +msgstr "CRIAR VIEW COMO" msgid "CREATE VIEW statement" msgstr "Declaração CREATE VIEW" @@ -2252,10 +2259,10 @@ msgid "Calculate contribution per series or row" msgstr "Calcular a contribuição por sĆ©rie ou linha" msgid "Calculate from first step" -msgstr "" +msgstr "Calcule desde o primeiro passo" msgid "Calculate from previous step" -msgstr "" +msgstr "Calcule desde passo anterior" #, python-format msgid "Calculated column [%s] requires an expression" @@ -2305,13 +2312,13 @@ msgstr "NĆ£o Ć© possĆ­vel analisar a string de tempo [%(human_readable)s]" #, fuzzy msgid "Catalog" -msgstr "marca" +msgstr "Marca" msgid "Categorical" msgstr "Categórico" msgid "Categorical Color" -msgstr "Cor categórica" +msgstr "Cor Categórica" msgid "Categories to group by on the x-axis." msgstr "Categorias para grupo por sobre o eixo x." @@ -2320,7 +2327,7 @@ msgid "Category" msgstr "Categoria" msgid "Category Name" -msgstr "Nome da categoria" +msgstr "Nome da Categoria" msgid "Category and Percentage" msgstr "Categoria e Porcentagem" @@ -2338,13 +2345,13 @@ msgid "Category, Value and Percentage" msgstr "Categoria, Valor e Porcentagem" msgid "Cell Padding" -msgstr "Preenchimento de cĆ©lula" +msgstr "Preenchimento de CĆ©lula" msgid "Cell Radius" msgstr "Raio da CĆ©lula" msgid "Cell Size" -msgstr "Tamanho da cĆ©lula" +msgstr "Tamanho da CĆ©lula" msgid "Cell content" msgstr "ConteĆŗdo da cĆ©lula" @@ -2354,7 +2361,7 @@ msgstr "Limite de cĆ©lula" #, fuzzy msgid "Centroid (Longitude and Latitude): " -msgstr "Centroide (Longitude e Latitude):" +msgstr "Centroide (Longitude e Latitude): " msgid "Certification" msgstr "Certificação" @@ -2382,7 +2389,7 @@ msgid "Change order of rows." msgstr "Mudar ordem das linhas." msgid "Changed By" -msgstr "Alterado por" +msgstr "Alterado Por" #, fuzzy msgid "Changed by" @@ -2447,16 +2454,16 @@ msgstr "Tempo limite da cache do grĆ”fico" #, python-format msgid "Chart Data: %s" -msgstr "Dados do grĆ”fico: %s" +msgstr "Dados do GrĆ”fico: %s" msgid "Chart ID" -msgstr "ID do grĆ”fico" +msgstr "ID do GrĆ”fico" msgid "Chart Options" -msgstr "OpƧƵes do grĆ”fico" +msgstr "OpƧƵes do GrĆ”fico" msgid "Chart Orientation" -msgstr "Orientação do grĆ”fico" +msgstr "Orientação do GrĆ”fico" #, fuzzy, python-format msgid "Chart Owner: %s" @@ -2465,10 +2472,10 @@ msgstr[0] "ProprietĆ”rio do grĆ”fico: %s" msgstr[1] "" msgid "Chart Source" -msgstr "Fonte do grĆ”fico" +msgstr "Fonte do GrĆ”fico" msgid "Chart Title" -msgstr "TĆ­tulo do grĆ”fico" +msgstr "TĆ­tulo do GrĆ”fico" #, python-format msgid "Chart [%s] has been overwritten" @@ -2572,11 +2579,11 @@ msgstr "Veja este grĆ”fico no painel:" #, fuzzy msgid "Check out this chart: " -msgstr "DĆŖ uma olhada neste grĆ”fico:" +msgstr "DĆŖ uma olhada neste grĆ”fico: " #, fuzzy msgid "Check out this dashboard: " -msgstr "Confira este painel:" +msgstr "Confira este painel: " msgid "Check to force date partitions to have the same height" msgstr "Marcar para forƧar as partiƧƵes de data a terem a mesma altura" @@ -2764,7 +2771,7 @@ msgstr "" "dados." msgid "Click to add a contour" -msgstr "" +msgstr "Clique para adicionar o contorno" msgid "Click to cancel sorting" msgstr "Clique para cancelar a ordenação" @@ -2888,14 +2895,14 @@ msgstr "" "consulta." msgid "Column Configuration" -msgstr "Configuração da coluna" +msgstr "Configuração da Coluna" #, fuzzy msgid "Column Data Types" -msgstr "Tipo de dados avanƧado" +msgstr "Tipo de dados AvanƧado" msgid "Column Formatting" -msgstr "Formatação de colunas" +msgstr "Formatação de Colunas" msgid "" "Column containing ISO 3166-2 codes of region/province/department in your " @@ -2948,7 +2955,7 @@ msgstr "" #, fuzzy msgid "Columnar Upload" -msgstr "Arquivo colunar" +msgstr "Upload Colunar" msgid "Columns" msgstr "Colunas" @@ -2957,7 +2964,7 @@ msgid "Columns To Be Parsed as Dates" msgstr "Colunas a serem analisadas como datas" msgid "Columns To Read" -msgstr "Colunas a serem lidas" +msgstr "Colunas a Serem Lidas" #, python-format msgid "Columns missing in dataset: %(invalid_columns)s" @@ -2986,7 +2993,7 @@ msgid "Columns to group by on the rows" msgstr "Colunas para agrupar nas linhas" msgid "Combine metrics" -msgstr "Combinar MĆ©tricas" +msgstr "Combinar mĆ©tricas" msgid "" "Comma-separated color picks for the intervals, e.g. 1,2,4. Integers " @@ -3017,7 +3024,7 @@ msgstr "" " e mĆ©tricas relacionadas." msgid "Compare results with other time periods." -msgstr "" +msgstr "Compare resultados com outros perĆ­odos de tempo." msgid "Compare the same summarized metric across multiple groups." msgstr "Comparar a mesma mĆ©trica resumida em vĆ”rios grupos." @@ -3051,7 +3058,7 @@ msgid "Comparison" msgstr "Comparação" msgid "Comparison Period Lag" -msgstr "Lag do PerĆ­odo de comparação" +msgstr "Atraso do PerĆ­odo de Comparação" #, fuzzy msgid "Comparison font size" @@ -3087,11 +3094,11 @@ msgstr "Configuração" #, fuzzy msgid "Configure Advanced Time Range " -msgstr "Configurar intervalo de tempo avanƧado" +msgstr "Configurar intervalo de tempo avanƧado " #, fuzzy msgid "Configure Time Range: Current..." -msgstr "Configurar Intervalo de Tempo: Último..." +msgstr "Configurar Intervalo de Tempo: Atual..." msgid "Configure Time Range: Last..." msgstr "Configurar Intervalo de Tempo: Último..." @@ -3106,7 +3113,7 @@ msgid "Configure filter scopes" msgstr "Configurar os Ć¢mbitos de filtragem" msgid "Configure the basics of your Annotation Layer." -msgstr "Configurar o fundamentos da sua camada de anotação." +msgstr "Configurar o fundamentos da sua Camada de Anotação." msgid "Configure this dashboard to embed it into an external web application." msgstr "Configurar este painel para incorporĆ”-lo em um aplicativo web externo." @@ -9634,11 +9641,17 @@ msgid "" "data within the row limit. You can use an aggregation function on a " "column or write custom SQL to create a percentage metric." msgstr "" +"Selecione uma ou mais mĆ©tricas para exibir, que serĆ£o exibidas nas " +"porcentagens do total. As mĆ©tricas percentuais serĆ£o calculadas apenas a partir de " +"dados dentro do limite de linhas. VocĆŖ pode usar uma função de agregação em uma " +"coluna ou escreva um SQL personalizado para criar uma mĆ©trica percentual." msgid "" "Select one or many metrics to display. You can use an aggregation " "function on a column or write custom SQL to create a metric." msgstr "" +"Selecione uma ou mais mĆ©tricas para exibir. VocĆŖ pode usar uma função de " +"agregação em uma coluna ou escreva um SQL personalizado para criar uma mĆ©trica." msgid "Select operator" msgstr "Selecionar operador" @@ -9689,6 +9702,11 @@ msgid "" "\"All charts\" to apply cross-filters to all charts that use the same " "dataset or contain the same column name in the dashboard." msgstr "" +"Selecione os grĆ”ficos aos quais deseja aplicar filtros cruzados neste " +"painel. Desmarcar um grĆ”fico irĆ” excluĆ­-lo de ser filtrado quando " +"aplicar filtros cruzados de qualquer grĆ”fico no painel. VocĆŖ pode selecionar " +"\"Todos os grĆ”ficos\" para aplicar filtros cruzados a todos os grĆ”ficos que usam o mesmo " +"conjunto de dados ou que contĆ©m o mesmo nome de coluna no painel." msgid "" "Select the charts to which you want to apply cross-filters when " @@ -9696,6 +9714,10 @@ msgid "" "filters to all charts that use the same dataset or contain the same " "column name in the dashboard." msgstr "" +"Selecione os grĆ”ficos aos quais deseja aplicar filtros cruzados quando " +"interagir com este grĆ”fico. VocĆŖ pode selecionar \"Todos os grĆ”ficos\" para aplicar " +"filtros em todos os grĆ”ficos que usam o mesmo conjunto de dados ou contĆŖm o mesmo " +"nome da coluna no painel." msgid "Select the geojson column" msgstr "Selecione a coluna geojson" @@ -9787,13 +9809,13 @@ msgid "Set filter mapping" msgstr "Definir o mapeamento de filtros" msgid "Set header rows and the number of rows to read or skip." -msgstr "" +msgstr "Defina linhas de cabeƧalho e o nĆŗmero de linhas a serem lidas ou ignoradas." msgid "Set up an email report" msgstr "Configurar um relatório de e-mail" msgid "Set up basic details, such as name and description." -msgstr "" +msgstr "Configure detalhes bĆ”sicos, como nome e descrição." msgid "" "Sets the hierarchy levels of the chart. Each level is\n" @@ -9801,7 +9823,7 @@ msgid "" "the hierarchy." msgstr "" "Define os nĆ­veis hierĆ”rquicos do grĆ”fico. Cada nĆ­vel Ć©\n" -" representado por um anel, sendo o cĆ­rculo mais interno o topo da " +" representado por um anel, sendo o cĆ­rculo mais interno o topo da " "hierarquia." msgid "Settings" @@ -10229,7 +10251,7 @@ msgid "Sorry, your browser does not support copying. Use Ctrl / Cmd + C!" msgstr "Desculpe, seu navegador nĆ£o suporta cópias. Use Ctrl / Cmd + C!" msgid "Sort" -msgstr "Classificar" +msgstr "Ordenar" msgid "Sort Bars" msgstr "Barras de classificação" @@ -10238,7 +10260,7 @@ msgid "Sort Descending" msgstr "Ordenação decrescente" msgid "Sort Metric" -msgstr "Classificar mĆ©trica" +msgstr "Ordenar MĆ©trica" msgid "Sort Series Ascending" msgstr "Ordenar sĆ©ries em ordem crescente" @@ -10266,31 +10288,31 @@ msgid "Sort by %s" msgstr "Ordenar por %s" msgid "Sort by metric" -msgstr "Classificar por mĆ©trica" +msgstr "Ordenar por mĆ©trica" msgid "Sort columns alphabetically" msgstr "Ordenar colunas alfabeticamente" msgid "Sort columns by" -msgstr "Classificar colunas por" +msgstr "Ordenar colunas por" msgid "Sort descending" msgstr "Ordenação decrescente" msgid "Sort filter values" -msgstr "Valores do filtro de classificação" +msgstr "Ordenar valores do filtro" msgid "Sort metric" msgstr "Ordenar mĆ©trica" msgid "Sort rows by" -msgstr "Ordenar as linhas por" +msgstr "Ordenar linhas por" msgid "Sort series in ascending order" -msgstr "Ordenar as sĆ©ries por ordem crescente" +msgstr "Ordenar sĆ©ries por ordem crescente" msgid "Sort type" -msgstr "Tipo de classificação" +msgstr "Tipo de ordenação" msgid "Source" msgstr "Fonte" @@ -10305,7 +10327,7 @@ msgid "Source category" msgstr "Categoria de origem" msgid "Sparkline" -msgstr "Sparkline" +msgstr "MinigrĆ”fico" msgid "Spatial" msgstr "Espacial" @@ -10508,7 +10530,7 @@ msgid "Successfully changed dataset!" msgstr "Conjunto de dados alterado com sucesso!" msgid "Suffix" -msgstr "" +msgstr "Sufixo" msgid "Suffix to apply after the percentage display" msgstr "Sufixo para aplicar após a apresentação da percentagem" @@ -10533,7 +10555,7 @@ msgstr "Valores da soma" #, fuzzy msgid "Summary" -msgstr "Domingo" +msgstr "SumĆ”rio" msgid "Sunburst Chart" msgstr "GrĆ”fico Sunburst" @@ -10599,7 +10621,7 @@ msgstr "Sintaxe" #, python-format msgid "Syntax Error: %(qualifier)s input \"%(input)s\" expecting \"%(expected)s" -msgstr "" +msgstr "Erro de Sintaxe: %(qualifier)s entrada \"%(input)s\" espera \"%(expected)s" msgid "TABLES" msgstr "TABELAS" @@ -10648,6 +10670,8 @@ msgid "" "Table already exists. You can change your 'if table already exists' " "strategy to append or replace or provide a different Table Name to use." msgstr "" +"A tabela jĆ” existe. VocĆŖ pode alterar seu 'se a tabela jĆ” existir'" +"estratĆ©gia para anexar ou substituir ou fornecer um nome de tabela diferente para usar." msgid "Table cache timeout" msgstr "Tempo limite do cache da tabela" @@ -10716,7 +10740,7 @@ msgstr "Lista atualizada" #, python-format msgid "Tagged %s %ss" -msgstr "" +msgstr "Marcado %s %ss" msgid "Tagged Object could not be deleted." msgstr "O objeto marcado nĆ£o pĆ“de ser excluĆ­do." @@ -10776,7 +10800,7 @@ msgid "Text" msgstr "Texto" msgid "Text / Markdown" -msgstr "" +msgstr "Texto / Markdown" msgid "Text align" msgstr "Alinhamento Texto" @@ -10907,10 +10931,10 @@ msgid "The column header label" msgstr "O rótulo do cabeƧalho da coluna" msgid "The column to be used as the source of the edge." -msgstr "" +msgstr "A coluna a ser usada como origem da aresta." msgid "The column to be used as the target of the edge." -msgstr "" +msgstr "A coluna a ser usada como alvo da ponte." msgid "The column was deleted or renamed in the database." msgstr "A coluna foi excluĆ­da ou renomeada no banco de dados." @@ -10996,10 +11020,10 @@ msgid "The dataset associated with this chart no longer exists" msgstr "O conjunto de dados associado a este grĆ”fico jĆ” nĆ£o existe" msgid "The dataset column/metric that returns the values on your chart's x-axis." -msgstr "" +msgstr "A coluna/mĆ©trica do conjunto de dados que retorna os valores no eixo x do grĆ”fico." msgid "The dataset column/metric that returns the values on your chart's y-axis." -msgstr "" +msgstr "A coluna/mĆ©trica do conjunto de dados que retorna os valores no eixo y do grĆ”fico." msgid "" "The dataset configuration exposed here\n" @@ -11027,10 +11051,10 @@ msgid "The datasource is too large to query." msgstr "A fonte de dados Ć© muito grande para ser consultada." msgid "The default catalog that should be used for the connection." -msgstr "" +msgstr "O catĆ”logo padrĆ£o que deve ser usado para a conexĆ£o." msgid "The default schema that should be used for the connection." -msgstr "" +msgstr "O esquema padrĆ£o que deve ser usado para a conexĆ£o." msgid "" "The description can be displayed as widget headers in the dashboard view." @@ -11127,7 +11151,7 @@ msgstr "" "substitua o grĆ”fico da 'visĆ£o de exploração'" msgid "The lower limit of the threshold range of the Isoband" -msgstr "" +msgstr "O limite inferior da faixa de limite da Isoband" msgid "The maximum number of events to return, equivalent to the number of rows" msgstr "O nĆŗmero mĆ”ximo de eventos retornados, equivalente ao nĆŗmero de linhas" @@ -11426,6 +11450,8 @@ msgid "" "interpretation e.g. 1, 1.0, or \"1\" (compatible with Python's float() " "function)." msgstr "" +"O resultado desta consulta deve ser um valor capaz de ser numĆ©rico " +"ex. 1, 1.0 ou \"1\" (compatĆ­vel com a função float() do Python)." msgid "The results backend no longer has the data from the query." msgstr "O backend de resultados nĆ£o tem mais os dados da consulta." @@ -11446,6 +11472,8 @@ msgid "" "The row limit set for the chart was reached. The chart may show partial " "data." msgstr "" +"O limite de linhas definido para o grĆ”fico foi atingido. O grĆ”fico pode mostrar dados " +"parciais." #, python-format msgid "" @@ -11464,16 +11492,17 @@ msgstr "" "usado para executar essa consulta." msgid "The schema of the submitted payload is invalid." -msgstr "" +msgstr "O esquema da carga enviada Ć© invĆ”lido." msgid "The schema was deleted or renamed in the database." msgstr "O esquema foi excluĆ­do ou renomeado no banco de dados." msgid "The screenshot could not be downloaded. Please, try again later." -msgstr "" +msgstr "A captura de tela nĆ£o pĆ“de ser baixada. Por favor, tente novamente mais tarde." msgid "The screenshot is being generated. Please, do not leave the page." -msgstr "" +msgstr "A captura de tela estĆ” sendo gerada. Por favor, nĆ£o saia da pĆ”gina." + msgid "The screenshot is now being downloaded." msgstr "" @@ -11593,13 +11622,13 @@ msgid "The unit of measure for the specified point radius" msgstr "A unidade de medida do raio do ponto especificado" msgid "The upper limit of the threshold range of the Isoband" -msgstr "" +msgstr "O limite superior da faixa de limite da Isoband" msgid "The user seems to have been deleted" msgstr "O usuĆ”rio parece ter sido excluĆ­do" msgid "The user/password combination is not valid (Incorrect password for user)." -msgstr "" +msgstr "A combinação usuĆ”rio/senha nĆ£o Ć© vĆ”lida (senha incorreta para o usuĆ”rio)." #, python-format msgid "The username \"%(username)s\" does not exist." @@ -11975,11 +12004,15 @@ msgid "" "This field is used as a unique identifier to attach the calculated " "dimension to charts. It is also used as the alias in the SQL query." msgstr "" +"Este campo Ć© usado como um identificador exclusivo para anexar o cĆ”lculo " +"dimensĆ£o para grĆ”ficos. TambĆ©m Ć© usado como alias na consulta SQL." msgid "" "This field is used as a unique identifier to attach the metric to charts." " It is also used as the alias in the SQL query." msgstr "" +"Este campo Ć© usado como um identificador exclusivo para anexar a mĆ©trica aos grĆ”ficos." +" TambĆ©m Ć© usado como alias na consulta SQL." msgid "" "This fields acts a Superset view, meaning that Superset will run a query " @@ -12042,12 +12075,14 @@ msgid "This metric might be incompatible with current dataset" msgstr "Essa mĆ©trica pode ser incompatĆ­vel com o conjunto de dados atual" msgid "This option has been disabled by the administrator." -msgstr "" +msgstr "Esta opção foi desabilitada pelo administrador." msgid "" "This page is intended to be embedded in an iframe, but it looks like that" " is not the case." msgstr "" +"Esta pĆ”gina foi projetada para ser incorporada em um iframe, mas Ć© assim" +" nĆ£o Ć© o caso." msgid "" "This section allows you to configure how to use the slice\n" @@ -12109,6 +12144,9 @@ msgid "" "to main columns for increase and decrease. Basic conditional formatting " "can be overwritten by conditional formatting below." msgstr "" +"Isso serĆ” aplicado a toda a tabela. SerĆ£o adicionadas setas (↑ e ↓) " +"para colunas principais para aumentar e diminuir. Formatação condicional bĆ”sica " +"pode ​​ser substituĆ­do pela formatação condicional abaixo." msgid "This will remove your current embed configuration." msgstr "Isso removerĆ” sua configuração de incorporação atual." @@ -12389,7 +12427,7 @@ msgid "Transpose pivot" msgstr "Transpor pivĆ“" msgid "Treat values as categorical." -msgstr "" +msgstr "Tratar valores como categórico" msgid "Tree Chart" msgstr "GrĆ”fico de Ć”rvore" @@ -12505,13 +12543,11 @@ msgid "URL slug" msgstr "Slug de URL" msgid "Unable to calculate such a date delta" -msgstr "" +msgstr "NĆ£o Ć© possĆ­vel calcular esse delta de data" #, python-format msgid "Unable to connect to catalog named \"%(catalog_name)s\"." -msgstr "" -"NĆ£o foi possĆ­vel conectar-se ao catĆ”logo chamado \"%(catalog_name)s " -"\"." +msgstr "NĆ£o foi possĆ­vel conectar-se ao catĆ”logo chamado \"%(catalog_name)s\"." #, python-format msgid "Unable to connect to database \"%(database)s\"." @@ -12533,10 +12569,10 @@ msgid "Unable to create chart without a query id." msgstr "NĆ£o Ć© possĆ­vel criar um grĆ”fico sem um ID de consulta." msgid "Unable to decode value" -msgstr "" +msgstr "NĆ£o foi possĆ­vel decodificar o valor" msgid "Unable to encode value" -msgstr "" +msgstr "NĆ£o foi possĆ­vel codificar o valor" #, python-format msgid "Unable to find such a holiday: [%(holiday)s]" @@ -12736,7 +12772,7 @@ msgstr "Carregar arquivo no banco de dados" #, python-format msgid "Upload a file with a valid extension. Valid: [%s]" -msgstr "" +msgstr "Suba um arquivo com extensĆ£o vĆ”lida. VĆ”lido: [%s]" msgid "Upload file to database" msgstr "Carregar arquivo no banco de dados" @@ -12914,10 +12950,10 @@ msgstr "Limites de valor" #, python-format msgid "Value cannot exceed %s" -msgstr "" +msgstr "Valor nĆ£o pode exceder %s" msgid "Value difference between the time periods" -msgstr "" +msgstr "DiferenƧa de valor entre os perĆ­odos de tempo" msgid "Value format" msgstr "Formato do valor" @@ -13358,7 +13394,8 @@ msgstr "" msgid "" "When the secondary temporal columns are filtered, apply the same filter " "to the main datetime column." -msgstr "" +msgstr "Quando as colunas temporais secundĆ”rias forem filtradas, aplique o mesmo filtro " +"para a coluna principal de data e hora." msgid "" "When using \"Autocomplete filters\", this can be used to improve " @@ -13596,7 +13633,7 @@ msgid "Which relatives to highlight on hover" msgstr "Qual parentes para destaque sobre passe o mouse" msgid "Whisker/outlier options" -msgstr "OpƧƵes de Whisker/outlier" +msgstr "OpƧƵes de whisker/outlier" msgid "White" msgstr "Branco" @@ -13608,7 +13645,7 @@ msgid "Width of the confidence interval. Should be between 0 and 1" msgstr "Largura do intervalo de confianƧa. Deve estar entre 0 e 1" msgid "Width of the sparkline" -msgstr "Largura do brilho" +msgstr "Largura do minigrĆ”fico" msgid "Window must be > 0" msgstr "A janela deve ser > 0" @@ -13641,7 +13678,7 @@ msgid "X AXIS TITLE BOTTOM MARGIN" msgstr "MARGEM INFERIOR DO TƍTULO DO EIXO X" msgid "X AXIS TITLE MARGIN" -msgstr "" +msgstr "MARGEM DO TƍTULO DO EIXO X" msgid "X Axis" msgstr "Eixo X" @@ -13705,11 +13742,11 @@ msgid "Y Axis Title" msgstr "TĆ­tulo do Eixo Y" msgid "Y Axis Title Margin" -msgstr "" +msgstr "Margem do TĆ­tulo do Eixo Y" #, fuzzy msgid "Y Axis Title Position" -msgstr "Posição do subtotal das linhas" +msgstr "Posição do TĆ­tulo do Eixo Y" msgid "Y Log Scale" msgstr "Escala logarĆ­tmica Y" @@ -13722,10 +13759,10 @@ msgid "Y-Axis" msgstr "Eixo Y" msgid "Y-Axis Sort Ascending" -msgstr "Classificação do eixo Y em ordem crescente" +msgstr "Classificar Eixo Y em ordem crescente" msgid "Y-Axis Sort By" -msgstr "Classificação do Eixo Y Por" +msgstr "Classificar Eixo Y Por" msgid "Y-axis" msgstr "Eixo Y" @@ -13760,7 +13797,7 @@ msgstr "Sim, sobrescrever mudanƧas" #, python-format msgid "You are adding tags to %s %ss" -msgstr "" +msgstr "VocĆŖ estĆ” adicionando marcas para %s %ss" msgid "" "You are importing one or more charts that already exist. Overwriting " @@ -13936,7 +13973,7 @@ msgstr "" "atual para redefinir o histórico." msgid "You may have an error in your SQL statement. {message}" -msgstr "" +msgstr "VocĆŖ pode ter um erro na sua instrução SQL. {mensagem}" msgid "" "You must be a dataset owner in order to edit. Please reach out to a " @@ -14010,7 +14047,7 @@ msgid "Your report could not be deleted" msgstr "NĆ£o foi possĆ­vel excluir seu relatório" msgid "ZIP file contains multiple file types" -msgstr "" +msgstr "Arquivo ZIP contĆ©m mĆŗltiplos tipos de arquivo" msgid "Zero imputation" msgstr "Imputação zero" @@ -14100,7 +14137,7 @@ msgid "`width` must be greater or equal to 0" msgstr "`largura` deve ser maior ou igual a 0" msgid "add colors to cell bars for +/-" -msgstr "" +msgstr "adicionar cores para as barras para +/-" msgid "aggregate" msgstr "agregar" @@ -14110,11 +14147,11 @@ msgstr "alerta" #, fuzzy msgid "alert condition" -msgstr "Condição de alerta" +msgstr "condição de alerta" #, fuzzy msgid "alert dark" -msgstr "alerta" +msgstr "alerta dark" msgid "alerts" msgstr "alertas" @@ -14132,7 +14169,7 @@ msgid "annotation" msgstr "anotação" msgid "annotation_layer" -msgstr "camada de anotação" +msgstr "camada_de_anotação" msgid "asfreq" msgstr "asfreq" @@ -14144,14 +14181,14 @@ msgid "auto" msgstr "automĆ”tico" msgid "auto (Smooth)" -msgstr "auto (Suave)" +msgstr "automĆ”tico (Suave)" msgid "background" msgstr "fundo" #, fuzzy msgid "basic conditional formatting" -msgstr "Formatação condicional" +msgstr "formatação condicional bĆ”sica" msgid "basis" msgstr "base" @@ -14172,10 +14209,10 @@ msgid "boolean type icon" msgstr "Ć­cone do tipo booleano" msgid "bottom" -msgstr "fundo" +msgstr "inferior" msgid "button (cmd + z) until you save your changes." -msgstr "(cmd + z) atĆ© vocĆŖ salvar suas mudanƧas." +msgstr "botĆ£o (cmd + z) atĆ© vocĆŖ salvar suas mudanƧas." msgid "by using" msgstr "usando" @@ -14211,15 +14248,15 @@ msgid "code ISO 3166-1 alpha-3 (cca3)" msgstr "código ISO 3166-1 alpha-3 (cca3)" msgid "code International Olympic Committee (cioc)" -msgstr "código ComitĆŖ OlĆ­mpico Internacional (cioc)" +msgstr "código do ComitĆŖ OlĆ­mpico Internacional (cioc)" #, fuzzy msgid "color scheme for comparison" -msgstr "Comparação de tempo" +msgstr "esquema de cor para comparação" #, fuzzy msgid "color type" -msgstr "Cor por" +msgstr "tipo de cor" msgid "column" msgstr "coluna" @@ -14230,7 +14267,7 @@ msgstr "conectando ao %(dbModelName)s." #, fuzzy msgid "content type" -msgstr "Tipo de etapa" +msgstr "tipo de conteĆŗdo" msgid "count" msgstr "contagem" @@ -14246,7 +14283,7 @@ msgstr "criar um conjunto de dados a partir de uma consulta SQL" #, fuzzy msgid "crontab" -msgstr "contagem" +msgstr "crontab" msgid "css" msgstr "css" @@ -14255,7 +14292,7 @@ msgid "css_template" msgstr "css_template" msgid "cumsum" -msgstr "cumsum" +msgstr "soma acumulada" msgid "cumulative" msgstr "cumulativo" @@ -14270,7 +14307,7 @@ msgid "database" msgstr "banco de dados" msgid "dataset" -msgstr "dataset" +msgstr "conjunto de dados" msgid "dataset name" msgstr "nome do conjunto de dados" @@ -14308,10 +14345,10 @@ msgid "deck.gl Heatmap" msgstr "grĆ”ficos do deck.gl" msgid "deck.gl Multiple Layers" -msgstr "deck.gl MĆŗltiplas camadas" +msgstr "deck.gl MĆŗltiplas Camadas" msgid "deck.gl Path" -msgstr "deck.gl Path" +msgstr "deck.gl Caminho" msgid "deck.gl Polygon" msgstr "deck.gl PolĆ­gono" @@ -14336,7 +14373,7 @@ msgstr "excluir" #, fuzzy msgid "descendant" -msgstr "Ordenação decrescente" +msgstr descendente" msgid "description" msgstr "descrição" @@ -14354,62 +14391,62 @@ msgid "dttm" msgstr "dttm" msgid "e.g. ********" -msgstr "por exemplo ********" +msgstr "ex. ********" msgid "e.g. 127.0.0.1" -msgstr "por exemplo, 127.0.0.1" +msgstr "ex. 127.0.0.1" msgid "e.g. 5432" -msgstr "por exemplo, 5432" +msgstr "ex. 5432" msgid "e.g. AccountAdmin" -msgstr "por exemplo , AccountAdmin" +msgstr "ex. AccountAdmin" #, fuzzy msgid "e.g. Analytics" -msgstr "Analytics avanƧado" +msgstr "ex. Analytics" msgid "e.g. compute_wh" -msgstr "por exemplo , compute_wh" +msgstr "ex. compute_wh" #, fuzzy msgid "e.g. default" -msgstr "padrĆ£o" +msgstr "ex. padrĆ£o" msgid "e.g. hive_metastore" -msgstr "" +msgstr "ex. hive_metastore" msgid "e.g. param1=value1¶m2=value2" -msgstr "por exemplo, param1=value1¶m2=value2" +msgstr "ex. param1=value1¶m2=value2" msgid "e.g. sql/protocolv1/o/12345" -msgstr "por exemplo , sql/protocolv1/o/12345" +msgstr "ex. sql/protocolv1/o/12345" msgid "e.g. world_population" -msgstr "por exemplo, world_population" +msgstr "ex. world_population" msgid "e.g. xy12345.us-east-2.aws" -msgstr "por exemplo, xy12345.us-east-2.aws" +msgstr "ex. xy12345.us-east-2.aws" msgid "e.g., a \"user id\" column" -msgstr "por exemplo, uma coluna \"user id\"" +msgstr "ex., uma coluna \"user id\"" msgid "edit mode" msgstr "modo de edição" #, fuzzy msgid "email subject" -msgstr "Selecionar assunto" +msgstr "assunto do email" msgid "entries" msgstr "entradas" #, fuzzy msgid "error" -msgstr "Erro" +msgstr "erro" msgid "error dark" -msgstr "" +msgstr "erro dark" msgid "error_message" msgstr "mensagem de erro" @@ -14531,7 +14568,7 @@ msgstr "" " que o percentil superior." msgid "max" -msgstr "mĆ”ximo" +msgstr "mĆ”x" msgid "mean" msgstr "mĆ©dia" @@ -14541,7 +14578,7 @@ msgstr "mediana" #, fuzzy msgid "meters" -msgstr "ParĆ¢metros" +msgstr "metros" msgid "metric" msgstr "mĆ©trica" @@ -14569,7 +14606,7 @@ msgstr "deve ter um valor" #, fuzzy msgid "name" -msgstr "Nome" +msgstr "nome" msgid "no SQL validator is configured" msgstr "nenhum validador SQL estĆ” configurado" @@ -14586,7 +14623,7 @@ msgstr "nvd3" #, fuzzy msgid "offline" -msgstr "Offline" +msgstr "offline" msgid "on" msgstr "em" @@ -14648,11 +14685,11 @@ msgid "permalink state not found" msgstr "estado do permalink nĆ£o encontrado" msgid "pixelated (Sharp)" -msgstr "pixelado (nĆ­tido)" +msgstr "pixelado (NĆ­tido)" #, fuzzy msgid "pixels" -msgstr "Pixels" +msgstr "pixels" msgid "previous calendar month" msgstr "mĆŖs anterior do calendĆ”rio" @@ -14661,7 +14698,7 @@ msgid "previous calendar week" msgstr "semana anterior do calendĆ”rio" msgid "previous calendar year" -msgstr "ano-calendĆ”rio anterior" +msgstr "ano anterior do calendĆ”rio" msgid "published" msgstr "publicado" @@ -14724,16 +14761,16 @@ msgid "" " scale; change: Show changes compared to the first data point in each " "series" msgstr "" -"sĆ©ries: Tratar cada sĆ©rie de forma independente; geral: Todas as sĆ©ries " -"usam a mesma escala; alteração: Mostrar alteraƧƵes em comparação com o " +"sĆ©ries: Tratar cada sĆ©rie de forma independente; geral: Todas as sĆ©ries" +" usam a mesma escala; alteração: Mostrar alteraƧƵes em comparação com o " "primeiro ponto de dados em cada sĆ©rie" #, fuzzy msgid "shift start date" -msgstr "Data de inĆ­cio" +msgstr "trocar data de inĆ­cio" msgid "sql" -msgstr "" +msgstr "sql" msgid "square" msgstr "quadrado" @@ -14748,10 +14785,10 @@ msgid "std" msgstr "std" msgid "step-after" -msgstr "etapa seguinte" +msgstr "passo seguinte" msgid "step-before" -msgstr "passo-anteerior" +msgstr "passo anteerior" msgid "stopped" msgstr "interrompido" @@ -14767,7 +14804,7 @@ msgstr "sucesso" #, fuzzy msgid "success dark" -msgstr "sucesso" +msgstr "sucesso dark" msgid "sum" msgstr "soma" @@ -14829,7 +14866,7 @@ msgid "virtual" msgstr "virtual" msgid "viz type" -msgstr "tipo de visualização" +msgstr "tipo de vis" msgid "was created" msgstr "foi criado" From ff8605b7233bd5bd8015dcfc48840d23b3eb1a11 Mon Sep 17 00:00:00 2001 From: WLCFaro Date: Tue, 15 Apr 2025 00:16:08 +0200 Subject: [PATCH 09/14] feat(lang): update Italian language (#29827) --- superset/translations/it/LC_MESSAGES/messages.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/translations/it/LC_MESSAGES/messages.po b/superset/translations/it/LC_MESSAGES/messages.po index 001dc63c114..52f9356bb04 100644 --- a/superset/translations/it/LC_MESSAGES/messages.po +++ b/superset/translations/it/LC_MESSAGES/messages.po @@ -8421,7 +8421,7 @@ msgid "Refresh" msgstr "" msgid "Refresh dashboard" -msgstr "Rimuovi il grafico dalla dashboard" +msgstr "Ricarica la dashboard" msgid "Refresh frequency" msgstr "" From 5f62deaa366896db97c211951e3dbd323433156c Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 14 Apr 2025 19:01:11 -0700 Subject: [PATCH 10/14] chore: use create table util (#33072) --- superset/migrations/migration_utils.py | 13 ++++++++ superset/migrations/shared/utils.py | 13 ++++---- .../2015-09-21_17-30_4e6a06bad7a8_init.py | 30 ++++++++++--------- ...-04_11-16_315b3f4da9b0_adding_log_model.py | 10 ++++--- .../2016-01-13_20-24_8e80a26a31db_.py | 10 ++++--- ...-02-03_17-41_d827694c7555_css_templates.py | 10 ++++--- ...09-56_a2d606a761d9_adding_favstar_model.py | 10 ++++--- ..._17-58_4fa88fe24e94_owners_many_to_many.py | 12 ++++---- ...7-25_17-48_ad82a75afd82_add_query_model.py | 10 ++++--- ...9_5e4a03ef0bf0_add_request_access_model.py | 4 ++- ...7-01-10_11-47_bcf3126872fc_add_keyvalue.py | 10 ++++--- ...17-09-13_16-36_ddd6ebdd853b_annotations.py | 6 ++-- ...8_6c7537a6004a_models_for_email_reports.py | 12 ++++---- ...cd94a4_change_owner_to_m2m_relation_on_.py | 5 ++-- ...4b49eb0782_add_tables_for_sql_lab_state.py | 12 ++++---- ...07_0a6f12f60c73_add_role_level_security.py | 12 ++++---- ...a813e_add_tables_relation_to_row_level_.py | 13 ++++---- ...020-05-26_23-21_2f1d15e8a6af_add_alerts.py | 14 +++++---- ...0-08-28_17-16_175ea3592453_cache_lookup.py | 10 ++++--- ...1_20-30_2e5a0ee25ed4_refractor_alerting.py | 16 +++++----- ...collapse_alerting_models_into_a_single_.py | 9 +++--- ...11-06_49b5a32daba5_add_report_schedules.py | 18 ++++++----- ...658_add_roles_relationship_to_dashboard.py | 9 ++++-- ...3-29_11-15_3ebe0993c770_filterset_table.py | 10 ++++--- ...fbb1a5849b_add_embedded_dahshoard_table.py | 16 +++++----- ..._09-59_6766938c6065_add_key_value_store.py | 16 +++++----- ...c8595_create_ssh_tunnel_credentials_tbl.py | 21 ++++++------- ..._13-13_83e1abbe777f_drop_access_request.py | 10 ++++--- ...e0f6f91c2055_create_user_favorite_table.py | 10 ++++--- ...-02_678eefb4ab44_add_access_token_table.py | 17 ++++++----- ...7811799_remove_sl_dataset_columns_table.py | 4 +-- ...49add7bfc_remove_sl_table_columns_table.py | 4 +-- ...3_38f4144e8558_remove_sl_dataset_tables.py | 4 +-- ...27_e53fd48cc078_remove_sl_dataset_users.py | 4 +-- ...13_15-29_a6b32d2d07b1_remove_sl_columns.py | 4 +-- ...-13_15-31_007a1abffe7e_remove_sl_tables.py | 4 +-- ...3_15-33_48cbb571fa3a_remove_sl_datasets.py | 4 +-- 37 files changed, 232 insertions(+), 164 deletions(-) diff --git a/superset/migrations/migration_utils.py b/superset/migrations/migration_utils.py index 99229a10299..1dc0e08383d 100644 --- a/superset/migrations/migration_utils.py +++ b/superset/migrations/migration_utils.py @@ -16,6 +16,7 @@ # under the License. from alembic.operations import Operations +from sqlalchemy.engine.reflection import Inspector naming_convention = { "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", @@ -26,6 +27,18 @@ naming_convention = { def create_unique_constraint( op: Operations, index_id: str, table_name: str, uix_columns: list[str] ) -> None: + # Get the database connection and inspector + bind = op.get_bind() + inspector = Inspector.from_engine(bind) + + # Check if the unique constraint already exists + existing_constraints = inspector.get_unique_constraints(table_name) + for constraint in existing_constraints: + if constraint["name"] == index_id: + # Constraint already exists, no need to create it + return + + # Create the unique constraint if it doesn't exist with op.batch_alter_table( table_name, naming_convention=naming_convention ) as batch_op: diff --git a/superset/migrations/shared/utils.py b/superset/migrations/shared/utils.py index 32e7710729d..6ee4137af60 100644 --- a/superset/migrations/shared/utils.py +++ b/superset/migrations/shared/utils.py @@ -226,24 +226,25 @@ def drop_fks_for_table( op.drop_constraint(fk_name, table_name, type_="foreignkey") -def create_table(table_name: str, *columns: SchemaItem) -> None: +def create_table(table_name: str, *columns: SchemaItem, **kwargs: Any) -> None: """ Creates a database table with the specified name and columns. This function checks if a table with the given name already exists in the database. If the table already exists, it logs an informational. - Otherwise, it proceeds to create a new table using the provided name and schema columns. + Otherwise, it proceeds to create a new table using the provided name + and schema columns. :param table_name: The name of the table to be created. - :param columns: A variable number of arguments representing the schema just like when calling alembic's method create_table() - """ # noqa: E501 - + :param columns: A variable number of arguments representing the schema + just like when calling alembic's method create_table() + """ if has_table(table_name=table_name): logger.info(f"Table {LRED}{table_name}{RESET} already exists. Skipping...") return logger.info(f"Creating table {GREEN}{table_name}{RESET}...") - op.create_table(table_name, *columns) + op.create_table(table_name, *columns, **kwargs) logger.info(f"Table {GREEN}{table_name}{RESET} created.") diff --git a/superset/migrations/versions/2015-09-21_17-30_4e6a06bad7a8_init.py b/superset/migrations/versions/2015-09-21_17-30_4e6a06bad7a8_init.py index cd0f74eaffd..ab34ab9e550 100644 --- a/superset/migrations/versions/2015-09-21_17-30_4e6a06bad7a8_init.py +++ b/superset/migrations/versions/2015-09-21_17-30_4e6a06bad7a8_init.py @@ -22,17 +22,19 @@ Create Date: 2015-09-21 17:30:38.442998 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "4e6a06bad7a8" down_revision = None -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "clusters", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -54,7 +56,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("cluster_name"), ) - op.create_table( + create_table( "dashboards", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -69,7 +71,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "dbs", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -85,7 +87,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("database_name"), ) - op.create_table( + create_table( "datasources", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -111,7 +113,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("datasource_name"), ) - op.create_table( + create_table( "tables", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -129,7 +131,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("table_name"), ) - op.create_table( + create_table( "columns", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -153,7 +155,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "metrics", sa.Column("id", sa.Integer(), nullable=False), sa.Column("metric_name", sa.String(length=512), nullable=True), @@ -170,7 +172,7 @@ def upgrade(): sa.ForeignKeyConstraint(["datasource_name"], ["datasources.datasource_name"]), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "slices", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -195,7 +197,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "sql_metrics", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -214,7 +216,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "table_columns", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), @@ -239,7 +241,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "dashboard_slices", sa.Column("id", sa.Integer(), nullable=False), sa.Column( diff --git a/superset/migrations/versions/2015-12-04_11-16_315b3f4da9b0_adding_log_model.py b/superset/migrations/versions/2015-12-04_11-16_315b3f4da9b0_adding_log_model.py index 5f12821399b..5a0121c2427 100644 --- a/superset/migrations/versions/2015-12-04_11-16_315b3f4da9b0_adding_log_model.py +++ b/superset/migrations/versions/2015-12-04_11-16_315b3f4da9b0_adding_log_model.py @@ -22,16 +22,18 @@ Create Date: 2015-12-04 11:16:58.226984 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "315b3f4da9b0" down_revision = "1a48a5411020" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "logs", sa.Column("id", sa.Integer(), nullable=False), sa.Column("action", sa.String(length=512), nullable=True), diff --git a/superset/migrations/versions/2016-01-13_20-24_8e80a26a31db_.py b/superset/migrations/versions/2016-01-13_20-24_8e80a26a31db_.py index e0f85c3a5b4..6f44f7a0a97 100644 --- a/superset/migrations/versions/2016-01-13_20-24_8e80a26a31db_.py +++ b/superset/migrations/versions/2016-01-13_20-24_8e80a26a31db_.py @@ -22,16 +22,18 @@ Create Date: 2016-01-13 20:24:45.256437 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "8e80a26a31db" down_revision = "2591d77e9831" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "url", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), diff --git a/superset/migrations/versions/2016-02-03_17-41_d827694c7555_css_templates.py b/superset/migrations/versions/2016-02-03_17-41_d827694c7555_css_templates.py index f65582563c2..40d2c4f1364 100644 --- a/superset/migrations/versions/2016-02-03_17-41_d827694c7555_css_templates.py +++ b/superset/migrations/versions/2016-02-03_17-41_d827694c7555_css_templates.py @@ -22,16 +22,18 @@ Create Date: 2016-02-03 17:41:10.944019 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "d827694c7555" down_revision = "43df8de3a5f4" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "css_templates", sa.Column("created_on", sa.DateTime(), nullable=False), sa.Column("changed_on", sa.DateTime(), nullable=False), diff --git a/superset/migrations/versions/2016-03-13_09-56_a2d606a761d9_adding_favstar_model.py b/superset/migrations/versions/2016-03-13_09-56_a2d606a761d9_adding_favstar_model.py index b2ba0674958..f8e4aacabc3 100644 --- a/superset/migrations/versions/2016-03-13_09-56_a2d606a761d9_adding_favstar_model.py +++ b/superset/migrations/versions/2016-03-13_09-56_a2d606a761d9_adding_favstar_model.py @@ -22,16 +22,18 @@ Create Date: 2016-03-13 09:56:58.329512 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "a2d606a761d9" down_revision = "18e88e1cc004" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "favstar", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), diff --git a/superset/migrations/versions/2016-04-15_17-58_4fa88fe24e94_owners_many_to_many.py b/superset/migrations/versions/2016-04-15_17-58_4fa88fe24e94_owners_many_to_many.py index bbb015775e8..bdb141fe830 100644 --- a/superset/migrations/versions/2016-04-15_17-58_4fa88fe24e94_owners_many_to_many.py +++ b/superset/migrations/versions/2016-04-15_17-58_4fa88fe24e94_owners_many_to_many.py @@ -22,16 +22,18 @@ Create Date: 2016-04-15 17:58:33.842012 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "4fa88fe24e94" down_revision = "b4456560d4f3" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "dashboard_user", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), @@ -40,7 +42,7 @@ def upgrade(): sa.ForeignKeyConstraint(["user_id"], ["ab_user.id"]), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "slice_user", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), diff --git a/superset/migrations/versions/2016-07-25_17-48_ad82a75afd82_add_query_model.py b/superset/migrations/versions/2016-07-25_17-48_ad82a75afd82_add_query_model.py index c62ac0c41a5..2260d627c6f 100644 --- a/superset/migrations/versions/2016-07-25_17-48_ad82a75afd82_add_query_model.py +++ b/superset/migrations/versions/2016-07-25_17-48_ad82a75afd82_add_query_model.py @@ -22,16 +22,18 @@ Create Date: 2016-07-25 17:48:12.771103 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "ad82a75afd82" down_revision = "f162a1dea4c4" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "query", sa.Column("id", sa.Integer(), nullable=False), sa.Column("client_id", sa.String(length=11), nullable=False), diff --git a/superset/migrations/versions/2016-09-09_17-39_5e4a03ef0bf0_add_request_access_model.py b/superset/migrations/versions/2016-09-09_17-39_5e4a03ef0bf0_add_request_access_model.py index cfba54e8be9..fcd739d68a4 100644 --- a/superset/migrations/versions/2016-09-09_17-39_5e4a03ef0bf0_add_request_access_model.py +++ b/superset/migrations/versions/2016-09-09_17-39_5e4a03ef0bf0_add_request_access_model.py @@ -25,13 +25,15 @@ Create Date: 2016-09-09 17:39:57.846309 import sqlalchemy as sa from alembic import op +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "5e4a03ef0bf0" down_revision = "b347b202819b" def upgrade(): - op.create_table( + create_table( "access_request", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2017-01-10_11-47_bcf3126872fc_add_keyvalue.py b/superset/migrations/versions/2017-01-10_11-47_bcf3126872fc_add_keyvalue.py index e4097389077..4c13d968d8a 100644 --- a/superset/migrations/versions/2017-01-10_11-47_bcf3126872fc_add_keyvalue.py +++ b/superset/migrations/versions/2017-01-10_11-47_bcf3126872fc_add_keyvalue.py @@ -22,17 +22,19 @@ Create Date: 2017-01-10 11:47:56.306938 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "bcf3126872fc" down_revision = "f18570e03440" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "keyvalue", sa.Column("id", sa.Integer(), nullable=False), sa.Column("value", sa.Text(), nullable=False), diff --git a/superset/migrations/versions/2017-09-13_16-36_ddd6ebdd853b_annotations.py b/superset/migrations/versions/2017-09-13_16-36_ddd6ebdd853b_annotations.py index eb06712333b..f999b69fc1e 100644 --- a/superset/migrations/versions/2017-09-13_16-36_ddd6ebdd853b_annotations.py +++ b/superset/migrations/versions/2017-09-13_16-36_ddd6ebdd853b_annotations.py @@ -25,6 +25,8 @@ Create Date: 2017-09-13 16:36:39.144489 import sqlalchemy as sa from alembic import op +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "ddd6ebdd853b" down_revision = "ca69c70ec99b" @@ -32,7 +34,7 @@ down_revision = "ca69c70ec99b" def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "annotation_layer", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -45,7 +47,7 @@ def upgrade(): sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "annotation", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2018-05-15_20-28_6c7537a6004a_models_for_email_reports.py b/superset/migrations/versions/2018-05-15_20-28_6c7537a6004a_models_for_email_reports.py index 3b53fc5991b..92e5471c26d 100644 --- a/superset/migrations/versions/2018-05-15_20-28_6c7537a6004a_models_for_email_reports.py +++ b/superset/migrations/versions/2018-05-15_20-28_6c7537a6004a_models_for_email_reports.py @@ -22,17 +22,19 @@ Create Date: 2018-05-15 20:28:51.977572 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "6c7537a6004a" down_revision = "a61b40f9f57f" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "dashboard_email_schedules", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -62,7 +64,7 @@ def upgrade(): ["active"], unique=False, ) - op.create_table( + create_table( "slice_email_schedules", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py b/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py index 332fe88e535..7a1b43331bc 100644 --- a/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py +++ b/superset/migrations/versions/2018-12-15_12-34_3e1b21cd94a4_change_owner_to_m2m_relation_on_.py @@ -27,6 +27,7 @@ from alembic import op # revision identifiers, used by Alembic. from superset import db +from superset.migrations.shared.utils import create_table from superset.utils.core import generic_find_fk_constraint_name revision = "3e1b21cd94a4" @@ -65,7 +66,7 @@ DruidDatasource = sa.Table( def upgrade(): - op.create_table( + create_table( "sqlatable_user", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), @@ -74,7 +75,7 @@ def upgrade(): sa.ForeignKeyConstraint(["user_id"], ["ab_user.id"]), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "druiddatasource_user", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), diff --git a/superset/migrations/versions/2019-11-13_11-05_db4b49eb0782_add_tables_for_sql_lab_state.py b/superset/migrations/versions/2019-11-13_11-05_db4b49eb0782_add_tables_for_sql_lab_state.py index 44efe9c8ade..3172679fd74 100644 --- a/superset/migrations/versions/2019-11-13_11-05_db4b49eb0782_add_tables_for_sql_lab_state.py +++ b/superset/migrations/versions/2019-11-13_11-05_db4b49eb0782_add_tables_for_sql_lab_state.py @@ -22,17 +22,19 @@ Create Date: 2019-11-13 11:05:30.122167 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "db4b49eb0782" down_revision = "78ee127d0d1d" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "tab_state", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -59,7 +61,7 @@ def upgrade(): sqlite_autoincrement=True, ) op.create_index(op.f("ix_tab_state_id"), "tab_state", ["id"], unique=True) - op.create_table( + create_table( "table_schema", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2019-12-04_17-07_0a6f12f60c73_add_role_level_security.py b/superset/migrations/versions/2019-12-04_17-07_0a6f12f60c73_add_role_level_security.py index d2485787078..35fbfe12183 100644 --- a/superset/migrations/versions/2019-12-04_17-07_0a6f12f60c73_add_role_level_security.py +++ b/superset/migrations/versions/2019-12-04_17-07_0a6f12f60c73_add_role_level_security.py @@ -22,16 +22,18 @@ Create Date: 2019-12-04 17:07:54.390805 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "0a6f12f60c73" down_revision = "3325d4caccc8" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "row_level_security_filters", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -45,7 +47,7 @@ def upgrade(): sa.ForeignKeyConstraint(["table_id"], ["tables.id"]), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "rls_filter_roles", sa.Column("id", sa.Integer(), nullable=False), sa.Column("role_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2020-04-24_10-46_e557699a813e_add_tables_relation_to_row_level_.py b/superset/migrations/versions/2020-04-24_10-46_e557699a813e_add_tables_relation_to_row_level_.py index 1efa321b605..1ca6edd7f32 100644 --- a/superset/migrations/versions/2020-04-24_10-46_e557699a813e_add_tables_relation_to_row_level_.py +++ b/superset/migrations/versions/2020-04-24_10-46_e557699a813e_add_tables_relation_to_row_level_.py @@ -22,22 +22,23 @@ Create Date: 2020-04-24 10:46:24.119363 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table +from superset.utils.core import generic_find_fk_constraint_name + # revision identifiers, used by Alembic. revision = "e557699a813e" down_revision = "743a117f0d98" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - -from superset.utils.core import generic_find_fk_constraint_name # noqa: E402 - def upgrade(): bind = op.get_bind() metadata = sa.MetaData(bind=bind) insp = sa.engine.reflection.Inspector.from_engine(bind) - rls_filter_tables = op.create_table( + rls_filter_tables = create_table( "rls_filter_tables", sa.Column("id", sa.Integer(), nullable=False), sa.Column("table_id", sa.Integer(), nullable=True), diff --git a/superset/migrations/versions/2020-05-26_23-21_2f1d15e8a6af_add_alerts.py b/superset/migrations/versions/2020-05-26_23-21_2f1d15e8a6af_add_alerts.py index c0243717f42..24e7483304d 100644 --- a/superset/migrations/versions/2020-05-26_23-21_2f1d15e8a6af_add_alerts.py +++ b/superset/migrations/versions/2020-05-26_23-21_2f1d15e8a6af_add_alerts.py @@ -22,17 +22,19 @@ Create Date: 2020-05-26 23:21:50.059635 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "2f1d15e8a6af" down_revision = "a72cb0ebeb22" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "alerts", sa.Column("id", sa.Integer(), nullable=False), sa.Column("label", sa.String(length=150), nullable=False), @@ -59,7 +61,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), ) op.create_index(op.f("ix_alerts_active"), "alerts", ["active"], unique=False) - op.create_table( + create_table( "alert_logs", sa.Column("id", sa.Integer(), nullable=False), sa.Column("scheduled_dttm", sa.DateTime(), nullable=True), @@ -73,7 +75,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "alert_owner", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=True), diff --git a/superset/migrations/versions/2020-08-28_17-16_175ea3592453_cache_lookup.py b/superset/migrations/versions/2020-08-28_17-16_175ea3592453_cache_lookup.py index da3cd807db8..9856c369ef3 100644 --- a/superset/migrations/versions/2020-08-28_17-16_175ea3592453_cache_lookup.py +++ b/superset/migrations/versions/2020-08-28_17-16_175ea3592453_cache_lookup.py @@ -22,16 +22,18 @@ Create Date: 2020-08-28 17:16:57.379425 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "175ea3592453" down_revision = "f80a3b88324b" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "cache_keys", sa.Column("id", sa.Integer(), nullable=False), sa.Column("cache_key", sa.String(256), nullable=False), diff --git a/superset/migrations/versions/2020-08-31_20-30_2e5a0ee25ed4_refractor_alerting.py b/superset/migrations/versions/2020-08-31_20-30_2e5a0ee25ed4_refractor_alerting.py index d6c2b4a5314..3dbaac5ee3e 100644 --- a/superset/migrations/versions/2020-08-31_20-30_2e5a0ee25ed4_refractor_alerting.py +++ b/superset/migrations/versions/2020-08-31_20-30_2e5a0ee25ed4_refractor_alerting.py @@ -22,18 +22,20 @@ Create Date: 2020-08-31 20:30:30.781478 """ +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import mysql + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "2e5a0ee25ed4" down_revision = "f80a3b88324b" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy.dialects import mysql # noqa: E402 - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "alert_validators", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -57,7 +59,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "sql_observers", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -85,7 +87,7 @@ def upgrade(): ), sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "sql_observations", sa.Column("id", sa.Integer(), nullable=False), sa.Column("dttm", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py b/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py index 9502a66f4db..3ba58ecd771 100644 --- a/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py +++ b/superset/migrations/versions/2020-10-05_18-10_af30ca79208f_collapse_alerting_models_into_a_single_.py @@ -29,6 +29,7 @@ from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.orm import backref, relationship, RelationshipProperty from superset import db +from superset.migrations.shared.utils import create_table from superset.utils.core import generic_find_fk_constraint_name revision = "af30ca79208f" @@ -153,7 +154,7 @@ def upgrade(): # sqlite does not support column and fk deletion if isinstance(bind.dialect, SQLiteDialect): op.drop_table("sql_observations") - op.create_table( + create_table( "sql_observations", sa.Column("id", sa.Integer(), nullable=False), sa.Column("dttm", sa.DateTime(), nullable=True), @@ -174,7 +175,7 @@ def downgrade(): bind = op.get_bind() insp = sa.engine.reflection.Inspector.from_engine(bind) - op.create_table( + create_table( "sql_observers", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -191,7 +192,7 @@ def downgrade(): sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "alert_validators", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), @@ -248,7 +249,7 @@ def downgrade(): ), ) op.drop_table("alerts") - op.create_table( + create_table( "alerts", sa.Column("id", sa.Integer(), nullable=False), sa.Column("label", sa.String(length=150), nullable=False), diff --git a/superset/migrations/versions/2020-11-04_11-06_49b5a32daba5_add_report_schedules.py b/superset/migrations/versions/2020-11-04_11-06_49b5a32daba5_add_report_schedules.py index 8e28c299562..311362d68cf 100644 --- a/superset/migrations/versions/2020-11-04_11-06_49b5a32daba5_add_report_schedules.py +++ b/superset/migrations/versions/2020-11-04_11-06_49b5a32daba5_add_report_schedules.py @@ -22,17 +22,19 @@ Create Date: 2020-11-04 11:06:59.249758 """ +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "49b5a32daba5" down_revision = "96e99fb176a0" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy.engine.reflection import Inspector # noqa: E402 - def upgrade(): - op.create_table( + create_table( "report_schedule", sa.Column("id", sa.Integer(), nullable=False), sa.Column("type", sa.String(length=50), nullable=False), @@ -76,7 +78,7 @@ def upgrade(): op.f("ix_report_schedule_active"), "report_schedule", ["active"], unique=False ) - op.create_table( + create_table( "report_execution_log", sa.Column("id", sa.Integer(), nullable=False), sa.Column("scheduled_dttm", sa.DateTime(), nullable=False), @@ -91,7 +93,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "report_recipient", sa.Column("id", sa.Integer(), nullable=False), sa.Column("type", sa.String(length=50), nullable=False), @@ -108,7 +110,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), ) - op.create_table( + create_table( "report_schedule_user", sa.Column("id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2021-01-14_19-12_e11ccdd12658_add_roles_relationship_to_dashboard.py b/superset/migrations/versions/2021-01-14_19-12_e11ccdd12658_add_roles_relationship_to_dashboard.py index 78eaa98d581..03ee04bfb06 100644 --- a/superset/migrations/versions/2021-01-14_19-12_e11ccdd12658_add_roles_relationship_to_dashboard.py +++ b/superset/migrations/versions/2021-01-14_19-12_e11ccdd12658_add_roles_relationship_to_dashboard.py @@ -21,15 +21,18 @@ Revises: 260bf0649a77 Create Date: 2021-01-14 19:12:43.406230 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "e11ccdd12658" down_revision = "260bf0649a77" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 def upgrade(): - op.create_table( + create_table( "dashboard_roles", sa.Column("id", sa.Integer(), nullable=False), sa.Column("role_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2021-03-29_11-15_3ebe0993c770_filterset_table.py b/superset/migrations/versions/2021-03-29_11-15_3ebe0993c770_filterset_table.py index 8e092112dce..eb767339835 100644 --- a/superset/migrations/versions/2021-03-29_11-15_3ebe0993c770_filterset_table.py +++ b/superset/migrations/versions/2021-03-29_11-15_3ebe0993c770_filterset_table.py @@ -22,16 +22,18 @@ Create Date: 2021-03-29 11:15:48.831225 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "3ebe0993c770" down_revision = "181091c0ef16" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): - op.create_table( + create_table( "filter_sets", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2022-01-28_16-03_5afbb1a5849b_add_embedded_dahshoard_table.py b/superset/migrations/versions/2022-01-28_16-03_5afbb1a5849b_add_embedded_dahshoard_table.py index c784595735a..d47e41cac3e 100644 --- a/superset/migrations/versions/2022-01-28_16-03_5afbb1a5849b_add_embedded_dahshoard_table.py +++ b/superset/migrations/versions/2022-01-28_16-03_5afbb1a5849b_add_embedded_dahshoard_table.py @@ -22,19 +22,21 @@ Create Date: 2022-01-28 16:03:02.944080 """ +from uuid import uuid4 + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "5afbb1a5849b" down_revision = "5fd49410a97a" -from uuid import uuid4 # noqa: E402 - -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy_utils import UUIDType # noqa: E402 - def upgrade(): - op.create_table( + create_table( "embedded_dashboards", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2022-03-04_09-59_6766938c6065_add_key_value_store.py b/superset/migrations/versions/2022-03-04_09-59_6766938c6065_add_key_value_store.py index f22ac167ef7..7dcae88f539 100644 --- a/superset/migrations/versions/2022-03-04_09-59_6766938c6065_add_key_value_store.py +++ b/superset/migrations/versions/2022-03-04_09-59_6766938c6065_add_key_value_store.py @@ -22,19 +22,21 @@ Create Date: 2022-03-04 09:59:26.922329 """ +from uuid import uuid4 + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "6766938c6065" down_revision = "7293b0ca7944" -from uuid import uuid4 # noqa: E402 - -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy_utils import UUIDType # noqa: E402 - def upgrade(): - op.create_table( + create_table( "key_value", sa.Column("id", sa.Integer(), nullable=False), sa.Column("resource", sa.String(32), nullable=False), diff --git a/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py b/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py index 147ad8d372d..c2d05093b9b 100644 --- a/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py +++ b/superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_create_ssh_tunnel_credentials_tbl.py @@ -22,24 +22,25 @@ Create Date: 2022-10-20 10:48:08.722861 """ +from uuid import uuid4 + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset import app +from superset.extensions import encrypted_field_factory +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "f3c2d8ec8595" down_revision = "4ce1d9b25135" -from uuid import uuid4 # noqa: E402 - -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy_utils import UUIDType # noqa: E402 - -from superset import app # noqa: E402 -from superset.extensions import encrypted_field_factory # noqa: E402 - app_config = app.config def upgrade(): - op.create_table( + create_table( "ssh_tunnels", # AuditMixinNullable sa.Column("created_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2023-06-01_13-13_83e1abbe777f_drop_access_request.py b/superset/migrations/versions/2023-06-01_13-13_83e1abbe777f_drop_access_request.py index 4e08a1b1253..dbcf7454e8c 100644 --- a/superset/migrations/versions/2023-06-01_13-13_83e1abbe777f_drop_access_request.py +++ b/superset/migrations/versions/2023-06-01_13-13_83e1abbe777f_drop_access_request.py @@ -22,20 +22,22 @@ Create Date: 2023-06-01 13:13:18.147362 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "83e1abbe777f" down_revision = "ae58e1e58e5c" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): op.drop_table("access_request") def downgrade(): - op.create_table( + create_table( "access_request", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py b/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py index 2cf0e02ce8f..0ca937f10ed 100644 --- a/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py +++ b/superset/migrations/versions/2023-07-12_20-34_e0f6f91c2055_create_user_favorite_table.py @@ -22,17 +22,19 @@ Create Date: 2023-07-12 20:34:57.553981 """ +import sqlalchemy as sa +from alembic import op + +from superset.migrations.shared.utils import create_table + # revision identifiers, used by Alembic. revision = "e0f6f91c2055" down_revision = "bf646a0c1501" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 - def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table( + create_table( "user_favorite_tag", sa.Column("user_id", sa.Integer(), nullable=False), sa.Column("tag_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2024-03-20_16-02_678eefb4ab44_add_access_token_table.py b/superset/migrations/versions/2024-03-20_16-02_678eefb4ab44_add_access_token_table.py index 59ccd7a0852..7d9c851285c 100644 --- a/superset/migrations/versions/2024-03-20_16-02_678eefb4ab44_add_access_token_table.py +++ b/superset/migrations/versions/2024-03-20_16-02_678eefb4ab44_add_access_token_table.py @@ -22,19 +22,22 @@ Create Date: 2024-03-20 16:02:58.515915 """ +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import EncryptedType + +from superset.migrations.shared.utils import ( + create_table, + drop_fks_for_table, +) + # revision identifiers, used by Alembic. revision = "678eefb4ab44" down_revision = "be1b217cd8cd" -import sqlalchemy as sa # noqa: E402 -from alembic import op # noqa: E402 -from sqlalchemy_utils import EncryptedType # noqa: E402 - -from superset.migrations.shared.utils import drop_fks_for_table # noqa: E402 - def upgrade(): - op.create_table( + create_table( "database_user_oauth2_tokens", sa.Column("created_on", sa.DateTime(), nullable=True), sa.Column("changed_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2024-05-24_11-31_02f4f7811799_remove_sl_dataset_columns_table.py b/superset/migrations/versions/2024-05-24_11-31_02f4f7811799_remove_sl_dataset_columns_table.py index 70913d2ecb9..affe9259cd7 100644 --- a/superset/migrations/versions/2024-05-24_11-31_02f4f7811799_remove_sl_dataset_columns_table.py +++ b/superset/migrations/versions/2024-05-24_11-31_02f4f7811799_remove_sl_dataset_columns_table.py @@ -25,7 +25,7 @@ Create Date: 2024-05-24 11:31:57.115586 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "02f4f7811799" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("dataset_id", sa.Integer(), nullable=False), sa.Column("column_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2024-08-13_15-17_39549add7bfc_remove_sl_table_columns_table.py b/superset/migrations/versions/2024-08-13_15-17_39549add7bfc_remove_sl_table_columns_table.py index bb7a2d14dbd..2c8ec719807 100644 --- a/superset/migrations/versions/2024-08-13_15-17_39549add7bfc_remove_sl_table_columns_table.py +++ b/superset/migrations/versions/2024-08-13_15-17_39549add7bfc_remove_sl_table_columns_table.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:17:23.273168 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "39549add7bfc" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("table_id", sa.Integer(), nullable=False), sa.Column("column_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2024-08-13_15-23_38f4144e8558_remove_sl_dataset_tables.py b/superset/migrations/versions/2024-08-13_15-23_38f4144e8558_remove_sl_dataset_tables.py index d8565f89b6f..95a2951b31a 100644 --- a/superset/migrations/versions/2024-08-13_15-23_38f4144e8558_remove_sl_dataset_tables.py +++ b/superset/migrations/versions/2024-08-13_15-23_38f4144e8558_remove_sl_dataset_tables.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:23:28.768963 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "38f4144e8558" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("dataset_id", sa.Integer(), nullable=False), sa.Column("table_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2024-08-13_15-27_e53fd48cc078_remove_sl_dataset_users.py b/superset/migrations/versions/2024-08-13_15-27_e53fd48cc078_remove_sl_dataset_users.py index e9122dc47af..f6d0af07a62 100644 --- a/superset/migrations/versions/2024-08-13_15-27_e53fd48cc078_remove_sl_dataset_users.py +++ b/superset/migrations/versions/2024-08-13_15-27_e53fd48cc078_remove_sl_dataset_users.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:27:11.589886 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "e53fd48cc078" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("dataset_id", sa.Integer(), nullable=False), sa.Column("user_id", sa.Integer(), nullable=False), diff --git a/superset/migrations/versions/2024-08-13_15-29_a6b32d2d07b1_remove_sl_columns.py b/superset/migrations/versions/2024-08-13_15-29_a6b32d2d07b1_remove_sl_columns.py index d10862fb8c7..80b5b5ca055 100644 --- a/superset/migrations/versions/2024-08-13_15-29_a6b32d2d07b1_remove_sl_columns.py +++ b/superset/migrations/versions/2024-08-13_15-29_a6b32d2d07b1_remove_sl_columns.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:29:33.135672 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "a6b32d2d07b1" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("uuid", sa.Numeric(precision=16), nullable=True), sa.Column("created_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2024-08-13_15-31_007a1abffe7e_remove_sl_tables.py b/superset/migrations/versions/2024-08-13_15-31_007a1abffe7e_remove_sl_tables.py index 8b0f2861018..12ff3d73e01 100644 --- a/superset/migrations/versions/2024-08-13_15-31_007a1abffe7e_remove_sl_tables.py +++ b/superset/migrations/versions/2024-08-13_15-31_007a1abffe7e_remove_sl_tables.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:31:31.478017 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "007a1abffe7e" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("uuid", sa.Numeric(precision=16), nullable=True), sa.Column("created_on", sa.DateTime(), nullable=True), diff --git a/superset/migrations/versions/2024-08-13_15-33_48cbb571fa3a_remove_sl_datasets.py b/superset/migrations/versions/2024-08-13_15-33_48cbb571fa3a_remove_sl_datasets.py index 7b35ebec261..84ac681a373 100644 --- a/superset/migrations/versions/2024-08-13_15-33_48cbb571fa3a_remove_sl_datasets.py +++ b/superset/migrations/versions/2024-08-13_15-33_48cbb571fa3a_remove_sl_datasets.py @@ -25,7 +25,7 @@ Create Date: 2024-08-13 15:33:14.551012 import sqlalchemy as sa from alembic import op -from superset.migrations.shared.utils import drop_fks_for_table, has_table +from superset.migrations.shared.utils import create_table, drop_fks_for_table, has_table # revision identifiers, used by Alembic. revision = "48cbb571fa3a" @@ -41,7 +41,7 @@ def upgrade(): def downgrade(): - op.create_table( + create_table( table_name, sa.Column("uuid", sa.Numeric(precision=16), nullable=True), sa.Column("created_on", sa.DateTime(), nullable=True), From bc0ffe0d10de77778a41355ba8a3b921c2d03cac Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Tue, 15 Apr 2025 08:25:09 -0300 Subject: [PATCH 11/14] fix: Viz migration error handling (#33037) --- superset/cli/viz_migrations.py | 22 +++++-- .../migrations/shared/migrate_viz/base.py | 59 +++++++++++-------- superset/migrations/shared/utils.py | 6 +- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py index 34550ac2da4..df62a598584 100644 --- a/superset/cli/viz_migrations.py +++ b/superset/cli/viz_migrations.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import logging from enum import Enum from typing import Type @@ -107,9 +108,10 @@ def migrate_viz() -> None: ) def upgrade(viz_type: str, ids: tuple[int, ...] | None = None) -> None: """Upgrade a viz to the latest version.""" - if ids is None: + setup_logger() + if viz_type: migrate_by_viz_type(VizType(viz_type)) - else: + elif ids: migrate_by_id(ids) @@ -133,9 +135,10 @@ def upgrade(viz_type: str, ids: tuple[int, ...] | None = None) -> None: ) def downgrade(viz_type: str, ids: tuple[int, ...] | None = None) -> None: """Downgrade a viz to the previous version.""" - if ids is None: + setup_logger() + if viz_type: migrate_by_viz_type(VizType(viz_type), is_downgrade=True) - else: + elif ids: migrate_by_id(ids, is_downgrade=True) @@ -163,7 +166,7 @@ def migrate_by_id(ids: tuple[int, ...], is_downgrade: bool = False) -> None: slices = db.session.query(Slice).filter(Slice.id.in_(ids)) for slc in paginated_update( slices, - lambda current, total: print( + lambda current, total: click.echo( f"{('Downgraded' if is_downgrade else 'Upgraded')} {current}/{total} charts" ), ): @@ -171,3 +174,12 @@ def migrate_by_id(ids: tuple[int, ...], is_downgrade: bool = False) -> None: PREVIOUS_VERSION[slc.viz_type].downgrade_slice(slc) elif slc.viz_type in MIGRATIONS: MIGRATIONS[slc.viz_type].upgrade_slice(slc) + + +def setup_logger() -> None: + """ + Configure the logger for the CLI commands. + """ + console_handler = logging.StreamHandler() + logger = logging.getLogger("alembic") + logger.addHandler(console_handler) diff --git a/superset/migrations/shared/migrate_viz/base.py b/superset/migrations/shared/migrate_viz/base.py index ee5372e3a8f..ba10de1ec22 100644 --- a/superset/migrations/shared/migrate_viz/base.py +++ b/superset/migrations/shared/migrate_viz/base.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. import copy +import logging from typing import Any from sqlalchemy import and_, Column, Integer, String, Text @@ -26,6 +27,8 @@ from superset.constants import TimeGrain from superset.migrations.shared.utils import paginated_update, try_load_json from superset.utils import json +logger = logging.getLogger("alembic") + Base = declarative_base() @@ -121,41 +124,51 @@ class MigrateViz: @classmethod def upgrade_slice(cls, slc: Slice) -> None: - clz = cls(slc.params) - form_data_bak = copy.deepcopy(clz.data) + try: + clz = cls(slc.params) + form_data_bak = copy.deepcopy(clz.data) - clz._pre_action() - clz._migrate() - clz._post_action() + clz._pre_action() + clz._migrate() + clz._post_action() - # viz_type depends on the migration and should be set after its execution - # because a source viz can be mapped to different target viz types - slc.viz_type = clz.target_viz_type + # viz_type depends on the migration and should be set after its execution + # because a source viz can be mapped to different target viz types + slc.viz_type = clz.target_viz_type - # only backup params - slc.params = json.dumps({**clz.data, FORM_DATA_BAK_FIELD_NAME: form_data_bak}) + # only backup params + slc.params = json.dumps( + {**clz.data, FORM_DATA_BAK_FIELD_NAME: form_data_bak} + ) - if "form_data" in (query_context := try_load_json(slc.query_context)): - query_context["form_data"] = clz.data - slc.query_context = json.dumps(query_context) + if "form_data" in (query_context := try_load_json(slc.query_context)): + query_context["form_data"] = clz.data + slc.query_context = json.dumps(query_context) + except Exception as e: + logger.warning(f"Failed to migrate slice {slc.id}: {e}") @classmethod def downgrade_slice(cls, slc: Slice) -> None: - form_data = try_load_json(slc.params) - if "viz_type" in (form_data_bak := form_data.get(FORM_DATA_BAK_FIELD_NAME, {})): - slc.params = json.dumps(form_data_bak) - slc.viz_type = form_data_bak.get("viz_type") - query_context = try_load_json(slc.query_context) - if "form_data" in query_context: - query_context["form_data"] = form_data_bak - slc.query_context = json.dumps(query_context) + try: + form_data = try_load_json(slc.params) + if "viz_type" in ( + form_data_bak := form_data.get(FORM_DATA_BAK_FIELD_NAME, {}) + ): + slc.params = json.dumps(form_data_bak) + slc.viz_type = form_data_bak.get("viz_type") + query_context = try_load_json(slc.query_context) + if "form_data" in query_context: + query_context["form_data"] = form_data_bak + slc.query_context = json.dumps(query_context) + except Exception as e: + logger.warning(f"Failed to downgrade slice {slc.id}: {e}") @classmethod def upgrade(cls, session: Session) -> None: slices = session.query(Slice).filter(Slice.viz_type == cls.source_viz_type) for slc in paginated_update( slices, - lambda current, total: print(f"Upgraded {current}/{total} charts"), + lambda current, total: logger.info(f"Upgraded {current}/{total} charts"), ): cls.upgrade_slice(slc) @@ -169,6 +182,6 @@ class MigrateViz: ) for slc in paginated_update( slices, - lambda current, total: print(f"Downgraded {current}/{total} charts"), + lambda current, total: logger.info(f"Downgraded {current}/{total} charts"), ): cls.downgrade_slice(slc) diff --git a/superset/migrations/shared/utils.py b/superset/migrations/shared/utils.py index 6ee4137af60..db97383a564 100644 --- a/superset/migrations/shared/utils.py +++ b/superset/migrations/shared/utils.py @@ -172,11 +172,7 @@ def paginated_update( def try_load_json(data: Optional[str]) -> dict[str, Any]: - try: - return data and json.loads(data) or {} - except json.JSONDecodeError: - print(f"Failed to parse: {data}") - return {} + return data and json.loads(data) or {} def has_table(table_name: str) -> bool: From 013379eb86276636d19f6dea159607a5c967b9b9 Mon Sep 17 00:00:00 2001 From: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:04:28 +0200 Subject: [PATCH 12/14] feat(List Users): Migrate List Users FAB to React (#32882) --- .../src/components/Checkbox/Checkbox.tsx | 4 +- .../components/ListView/Filters/DateRange.tsx | 25 +- .../ListView/Filters/NumericalRange.tsx | 134 ++++ .../src/components/ListView/Filters/index.tsx | 19 + .../src/components/ListView/types.ts | 9 +- .../src/features/users/UserListModal.tsx | 250 ++++++++ superset-frontend/src/features/users/types.ts | 27 + superset-frontend/src/features/users/utils.ts | 43 ++ .../src/pages/UsersList/UsersList.test.tsx | 192 ++++++ .../src/pages/UsersList/index.tsx | 598 ++++++++++++++++++ superset-frontend/src/views/CRUD/hooks.ts | 4 +- superset-frontend/src/views/routes.tsx | 25 +- superset/initialization/__init__.py | 9 + superset/security/manager.py | 48 +- superset/views/users_list.py | 34 + tests/unit_tests/security/api_test.py | 2 +- 16 files changed, 1390 insertions(+), 33 deletions(-) create mode 100644 superset-frontend/src/components/ListView/Filters/NumericalRange.tsx create mode 100644 superset-frontend/src/features/users/UserListModal.tsx create mode 100644 superset-frontend/src/features/users/types.ts create mode 100644 superset-frontend/src/features/users/utils.ts create mode 100644 superset-frontend/src/pages/UsersList/UsersList.test.tsx create mode 100644 superset-frontend/src/pages/UsersList/index.tsx create mode 100644 superset/views/users_list.py diff --git a/superset-frontend/src/components/Checkbox/Checkbox.tsx b/superset-frontend/src/components/Checkbox/Checkbox.tsx index 941b97aa538..931a45fed30 100644 --- a/superset-frontend/src/components/Checkbox/Checkbox.tsx +++ b/superset-frontend/src/components/Checkbox/Checkbox.tsx @@ -21,7 +21,7 @@ import { styled } from '@superset-ui/core'; import { CheckboxChecked, CheckboxUnchecked } from 'src/components/Checkbox'; export interface CheckboxProps { - checked: boolean; + checked?: boolean; onChange: (val?: boolean) => void; style?: CSSProperties; className?: string; @@ -35,7 +35,7 @@ const Styles = styled.span` `; export default function Checkbox({ - checked, + checked = false, onChange, style, className, diff --git a/superset-frontend/src/components/ListView/Filters/DateRange.tsx b/superset-frontend/src/components/ListView/Filters/DateRange.tsx index bf03012eedf..030d462f8ef 100644 --- a/superset-frontend/src/components/ListView/Filters/DateRange.tsx +++ b/superset-frontend/src/components/ListView/Filters/DateRange.tsx @@ -35,11 +35,12 @@ import { useLocale } from 'src/hooks/useLocale'; import { BaseFilter, FilterHandler } from './Base'; interface DateRangeFilterProps extends BaseFilter { - onSubmit: (val: number[]) => void; + onSubmit: (val: number[] | string[]) => void; name: string; + dateFilterValueType?: 'unix' | 'iso'; } -type ValueState = [number, number]; +type ValueState = [number, number] | [string, string] | null; const RangeFilterContainer = styled.div` display: inline-flex; @@ -50,7 +51,12 @@ const RangeFilterContainer = styled.div` `; function DateRangeFilter( - { Header, initialValue, onSubmit }: DateRangeFilterProps, + { + Header, + initialValue, + onSubmit, + dateFilterValueType = 'unix', + }: DateRangeFilterProps, ref: RefObject, ) { const [value, setValue] = useState(initialValue ?? null); @@ -85,11 +91,14 @@ function DateRangeFilter( onSubmit([]); return; } - const changeValue = [ - dayjsRange[0]?.valueOf() ?? 0, - dayjsRange[1]?.valueOf() ?? 0, - ] as ValueState; - setValue(changeValue); + const changeValue = + dateFilterValueType === 'iso' + ? [dayjsRange[0].toISOString(), dayjsRange[1].toISOString()] + : [ + dayjsRange[0]?.valueOf() ?? 0, + dayjsRange[1]?.valueOf() ?? 0, + ]; + setValue(changeValue as ValueState); onSubmit(changeValue); }} /> diff --git a/superset-frontend/src/components/ListView/Filters/NumericalRange.tsx b/superset-frontend/src/components/ListView/Filters/NumericalRange.tsx new file mode 100644 index 00000000000..04c4f29fec8 --- /dev/null +++ b/superset-frontend/src/components/ListView/Filters/NumericalRange.tsx @@ -0,0 +1,134 @@ +/** + * 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, forwardRef, useImperativeHandle, RefObject } from 'react'; +import { styled, t } from '@superset-ui/core'; +import { InputNumber } from 'src/components/Input'; +import { FormLabel } from 'src/components/Form'; +import { BaseFilter, FilterHandler } from './Base'; + +const RangeFilterContainer = styled.div` + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 360px; +`; + +const InputContainer = styled.div` + display: flex; + align-items: center; + width: 100%; + position: relative; +`; + +const StyledDivider = styled.span` + margin: 0 ${({ theme }) => theme.gridUnit * 2}px; + color: ${({ theme }) => theme.colors.grayscale.base}; + font-weight: ${({ theme }) => theme.typography.weights.bold}; + font-size: ${({ theme }) => theme.typography.sizes.m}px; +`; + +const ErrorMessage = styled.div` + color: ${({ theme }) => theme.colors.error.base}; + font-size: ${({ theme }) => theme.typography.sizes.s}px; + font-weight: ${({ theme }) => theme.typography.weights.bold}; + position: absolute; + bottom: -50%; + left: 0; +`; + +interface NumericalRangeFilterProps extends BaseFilter { + onSubmit: (val: [number | null, number | null]) => void; + name: string; + min?: number; + max?: number; +} + +function NumericalRangeFilter( + { Header, initialValue, onSubmit }: NumericalRangeFilterProps, + ref: RefObject, +) { + const [value, setValue] = useState<[number | null, number | null]>( + initialValue ?? [null, null], + ); + const [hasError, setHasError] = useState(false); + + const handleMinChange = (newMin: number | null) => { + const newValue: [number | null, number | null] = [newMin, value[1]]; + setValue(newValue); + + if (newMin !== null && value[1] !== null && newMin >= value[1]) { + setHasError(true); + return; + } + + setHasError(false); + onSubmit(newValue); + }; + const handleMaxChange = (newMax: number | null) => { + const newValue: [number | null, number | null] = [value[0], newMax]; + setValue(newValue); + + if (value[0] !== null && newMax !== null && value[0] >= newMax) { + setHasError(true); + return; + } + + setHasError(false); + onSubmit(newValue); + }; + + useImperativeHandle(ref, () => ({ + clearFilter: () => { + setValue([null, null]); + setHasError(false); + onSubmit([null, null]); + }, + })); + + return ( + + {Header} + + + - + + {hasError && ( + + {t('Minimum must be strictly less than maximum')} + + )} + + + ); +} + +export default forwardRef(NumericalRangeFilter); diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx b/superset-frontend/src/components/ListView/Filters/index.tsx index 8f64b67d051..5c794f15df0 100644 --- a/superset-frontend/src/components/ListView/Filters/index.tsx +++ b/superset-frontend/src/components/ListView/Filters/index.tsx @@ -35,6 +35,7 @@ import { import SearchFilter from './Search'; import SelectFilter from './Select'; import DateRangeFilter from './DateRange'; +import NumericalRangeFilter from './NumericalRange'; import { FilterHandler } from './Base'; interface UIFiltersProps { @@ -76,6 +77,9 @@ function UIFilters( toolTipDescription, onFilterUpdate, loading, + dateFilterValueType, + min, + max, }, index, ) => { @@ -136,6 +140,21 @@ function UIFilters( key={key} name={id} onSubmit={value => updateFilterValue(index, value)} + dateFilterValueType={dateFilterValueType || 'unix'} + /> + ); + } + if (input === 'numerical_range') { + return ( + updateFilterValue(index, value)} /> ); } diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 5b498070acf..6e0ff40743c 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -48,7 +48,8 @@ export interface Filter { | 'select' | 'checkbox' | 'search' - | 'datetime_range'; + | 'datetime_range' + | 'numerical_range'; unfilteredLabel?: string; selects?: SelectOption[]; onFilterOpen?: () => void; @@ -60,6 +61,9 @@ export interface Filter { ) => Promise<{ data: SelectOption[]; totalCount: number }>; paginate?: boolean; loading?: boolean; + dateFilterValueType?: 'unix' | 'iso'; + min?: number; + max?: number; } export type Filters = Filter[]; @@ -74,7 +78,8 @@ export type InnerFilterValue = | undefined | string[] | number[] - | { label: string; value: string | number }; + | { label: string; value: string | number } + | [number | null, number | null]; export interface FilterValue { id: string; diff --git a/superset-frontend/src/features/users/UserListModal.tsx b/superset-frontend/src/features/users/UserListModal.tsx new file mode 100644 index 00000000000..90ea5de749c --- /dev/null +++ b/superset-frontend/src/features/users/UserListModal.tsx @@ -0,0 +1,250 @@ +/** + * 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 { t } from '@superset-ui/core'; +import { useToasts } from 'src/components/MessageToasts/withToasts'; +import FormModal from 'src/components/Modal/FormModal'; +import { FormItem } from 'src/components/Form'; +import { Input } from 'src/components/Input'; +import Checkbox from 'src/components/Checkbox'; +import Select from 'src/components/Select/Select'; +import { Role, UserObject } from 'src/pages/UsersList'; +import { FormInstance } from 'src/components'; +import { BaseUserListModalProps, FormValues } from './types'; +import { createUser, updateUser } from './utils'; + +export interface UserModalProps extends BaseUserListModalProps { + roles: Role[]; + isEditMode?: boolean; + user?: UserObject; +} + +function UserListModal({ + show, + onHide, + onSave, + roles, + isEditMode = false, + user, +}: UserModalProps) { + const { addDangerToast, addSuccessToast } = useToasts(); + const handleFormSubmit = async (values: FormValues) => { + const handleError = async (err: any, action: 'create' | 'update') => { + let errorMessage = + action === 'create' + ? t('Error while adding user!') + : t('Error while updating user!'); + + if (err.status === 422) { + const errorData = await err.json(); + const detail = errorData?.message || ''; + + if (detail.includes('duplicate key value')) { + if (detail.includes('ab_user_username_key')) { + errorMessage = t( + 'This username is already taken. Please choose another one.', + ); + } else if (detail.includes('ab_user_email_key')) { + errorMessage = t( + 'This email is already associated with an account.', + ); + } + } + } + + addDangerToast(errorMessage); + throw err; + }; + + if (isEditMode) { + if (!user) { + throw new Error('User is required in edit mode'); + } + try { + await updateUser(user.id, values); + addSuccessToast(t('User was successfully updated!')); + } catch (err) { + await handleError(err, 'update'); + } + } else { + try { + await createUser(values); + addSuccessToast(t('User was successfully created!')); + } catch (err) { + await handleError(err, 'create'); + } + } + }; + + const requiredFields = isEditMode + ? ['first_name', 'last_name', 'username', 'email', 'roles'] + : [ + 'first_name', + 'last_name', + 'username', + 'email', + 'password', + 'roles', + 'confirmPassword', + ]; + + const initialValues = { + ...user, + roles: user?.roles.map(role => role.id) || [], + }; + + return ( + + {(form: FormInstance) => ( + <> + + + + + + + + + + + { + form.setFieldsValue({ isActive: checked }); + }} + /> + + + + + +