From 02ef9ca4cd60d0d05f0afd2cebc1cabb83dfd4df Mon Sep 17 00:00:00 2001 From: cccs-Dustin <96579982+cccs-Dustin@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:22:57 -0400 Subject: [PATCH 01/29] feat(sqllab): Add a configuration option to disable data preview (#19104) --- docs/static/resources/openapi.json | 14 ++++-- .../src/SqlLab/actions/sqlLab.js | 45 ++++++++++++------- .../src/SqlLab/actions/sqlLab.test.js | 41 ++++++++++++++--- .../components/AceEditorWrapper/index.tsx | 9 +++- .../src/SqlLab/components/SqlEditor/index.jsx | 1 + .../components/SqlEditorLeftBar/index.tsx | 4 +- .../DatabaseSelector.test.tsx | 4 ++ .../database/DatabaseModal/ExtraOptions.tsx | 20 ++++++++- .../database/DatabaseModal/index.test.jsx | 17 ++++++- .../src/views/CRUD/data/database/types.ts | 1 + superset/dashboards/schemas.py | 1 + superset/databases/api.py | 1 + superset/databases/schemas.py | 6 ++- superset/models/core.py | 8 ++++ superset/views/core.py | 1 + superset/views/database/mixins.py | 5 ++- tests/integration_tests/core_tests.py | 23 ++++++++++ .../integration_tests/databases/api_tests.py | 2 + 18 files changed, 168 insertions(+), 35 deletions(-) diff --git a/docs/static/resources/openapi.json b/docs/static/resources/openapi.json index 86d07ad8264c5..1e8c6129f1d44 100644 --- a/docs/static/resources/openapi.json +++ b/docs/static/resources/openapi.json @@ -2248,6 +2248,9 @@ "allows_virtual_table_explore": { "type": "boolean" }, + "disable_data_preview": { + "type": "boolean" + }, "backend": { "type": "string" }, @@ -2472,6 +2475,9 @@ "allows_virtual_table_explore": { "readOnly": true }, + "disable_data_preview": { + "readOnly": true + }, "backend": { "readOnly": true }, @@ -2571,7 +2577,7 @@ "type": "boolean" }, "extra": { - "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.

", + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.

", "type": "string" }, "force_ctas_schema": { @@ -2663,7 +2669,7 @@ "type": "boolean" }, "extra": { - "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.

", + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.

", "type": "string" }, "force_ctas_schema": { @@ -2720,7 +2726,7 @@ "type": "string" }, "extra": { - "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.

", + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.

", "type": "string" }, "impersonate_user": { @@ -2768,7 +2774,7 @@ "type": "string" }, "extra": { - "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown.

", + "description": "

JSON string containing extra configuration elements.
1. The engine_params object gets unpacked into the sqlalchemy.create_engine call, while the metadata_params gets unpacked into the sqlalchemy.MetaData call.
2. The metadata_cache_timeout is a cache timeout setting in seconds for metadata fetch of this database. Specify it as \"metadata_cache_timeout\": {\"schema_cache_timeout\": 600, \"table_cache_timeout\": 600}. If unset, cache will not be enabled for the functionality. A timeout of 0 indicates that the cache never expires.
3. The schemas_allowed_for_csv_upload is a comma separated list of schemas that CSVs are allowed to upload to. Specify it as \"schemas_allowed_for_csv_upload\": [\"public\", \"csv_upload\"]. If database flavor does not support schema or any schema is allowed to be accessed, just leave the list empty
4. the version field is a string specifying the this db's version. This should be used with Presto DBs so that the syntax is correct
5. The allows_virtual_table_explore field is a boolean specifying whether or not the Explore button in SQL Lab results is shown
6. The disable_data_preview field is a boolean specifying whether or not data preview queries will be run when fetching table metadata in SQL Lab.

", "type": "string" }, "impersonate_user": { diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index f89a6a8535df7..e13e4263a36cd 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -1018,28 +1018,13 @@ function getTableMetadata(table, query, dispatch) { ), }) .then(({ json }) => { - const dataPreviewQuery = { - id: shortid.generate(), - dbId: query.dbId, - sql: json.selectStar, - tableName: table.name, - sqlEditorId: null, - tab: '', - runAsync: false, - ctas: false, - isDataPreview: true, - }; const newTable = { ...table, ...json, expanded: true, isMetadataLoading: false, - dataPreviewQueryId: dataPreviewQuery.id, }; - Promise.all([ - dispatch(mergeTable(newTable, dataPreviewQuery)), // Merge table to tables in state - dispatch(runQuery(dataPreviewQuery)), // Run query to get preview data for table - ]); + dispatch(mergeTable(newTable)); // Merge table to tables in state return newTable; }) .catch(() => @@ -1082,7 +1067,7 @@ function getTableExtendedMetadata(table, query, dispatch) { ); } -export function addTable(query, tableName, schemaName) { +export function addTable(query, database, tableName, schemaName) { return function (dispatch) { const table = { dbId: query.dbId, @@ -1110,6 +1095,32 @@ export function addTable(query, tableName, schemaName) { }) : Promise.resolve({ json: { id: shortid.generate() } }); + if (!database.disable_data_preview && database.id === query.dbId) { + const dataPreviewQuery = { + id: shortid.generate(), + dbId: query.dbId, + sql: newTable.selectStar, + tableName: table.name, + sqlEditorId: null, + tab: '', + runAsync: database.allow_run_async, + ctas: false, + isDataPreview: true, + }; + Promise.all([ + dispatch( + mergeTable( + { + ...newTable, + dataPreviewQueryId: dataPreviewQuery.id, + }, + dataPreviewQuery, + ), + ), + dispatch(runQuery(dataPreviewQuery)), + ]); + } + return sync .then(({ json: resultJson }) => dispatch(mergeTable({ ...table, id: resultJson.id })), diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index d04d8b90ab1a8..789ae986bfbe7 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -727,28 +727,60 @@ describe('async actions', () => { it('updates the table schema state in the backend', () => { expect.assertions(5); + const database = { disable_data_preview: true }; + const tableName = 'table'; + const schemaName = 'schema'; + const store = mockStore({}); + const expectedActionTypes = [ + actions.MERGE_TABLE, // addTable + actions.MERGE_TABLE, // getTableMetadata + actions.MERGE_TABLE, // getTableExtendedMetadata + actions.MERGE_TABLE, // addTable + ]; + return store + .dispatch(actions.addTable(query, database, tableName, schemaName)) + .then(() => { + expect(store.getActions().map(a => a.type)).toEqual( + expectedActionTypes, + ); + expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); + expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1); + expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength( + 1, + ); + + // tab state is not updated, since no query was run + expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); + }); + }); + + it('updates and runs data preview query when configured', () => { + expect.assertions(5); + const results = { data: mockBigNumber, - query: { sqlEditorId: 'null' }, + query: { sqlEditorId: 'null', dbId: 1 }, query_id: 'efgh', }; fetchMock.post(runQueryEndpoint, JSON.stringify(results), { overwriteRoutes: true, }); + const database = { disable_data_preview: false, id: 1 }; const tableName = 'table'; const schemaName = 'schema'; const store = mockStore({}); const expectedActionTypes = [ actions.MERGE_TABLE, // addTable actions.MERGE_TABLE, // getTableMetadata - actions.START_QUERY, // runQuery (data preview) actions.MERGE_TABLE, // getTableExtendedMetadata - actions.QUERY_SUCCESS, // querySuccess + actions.MERGE_TABLE, // addTable (data preview) + actions.START_QUERY, // runQuery (data preview) actions.MERGE_TABLE, // addTable + actions.QUERY_SUCCESS, // querySuccess ]; return store - .dispatch(actions.addTable(query, tableName, schemaName)) + .dispatch(actions.addTable(query, database, tableName, schemaName)) .then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, @@ -758,7 +790,6 @@ describe('async actions', () => { expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength( 1, ); - // tab state is not updated, since the query is a data preview expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0); }); diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx index 8807ccdacc7f4..25da49137ad60 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx @@ -43,11 +43,17 @@ interface Props { actions: { queryEditorSetSelectedText: (edit: any, text: null | string) => void; queryEditorSetFunctionNames: (queryEditor: object, dbId: number) => void; - addTable: (queryEditor: any, value: any, schema: any) => void; + addTable: ( + queryEditor: any, + database: any, + value: any, + schema: any, + ) => void; }; autocomplete: boolean; onBlur: (sql: string) => void; sql: string; + database: any; schemas: any[]; tables: any[]; functionNames: string[]; @@ -210,6 +216,7 @@ class AceEditorWrapper extends React.PureComponent { if (data.meta === 'table') { this.props.actions.addTable( this.props.queryEditor, + this.props.database, data.value, this.props.queryEditor.schema, ); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index 168a53d52b1a0..7899cbf71908a 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -514,6 +514,7 @@ class SqlEditor extends React.PureComponent { onChange={this.onSqlChanged} queryEditor={this.props.queryEditor} sql={this.props.queryEditor.sql} + database={this.props.database} schemas={this.props.queryEditor.schemaOptions} tables={this.props.queryEditor.tableOptions} functionNames={this.props.queryEditor.functionNames} diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx index 0988fd9680371..f9e8c2da9f98f 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx @@ -36,7 +36,7 @@ interface actionsTypes { queryEditorSetFunctionNames: (queryEditor: QueryEditor, dbId: number) => void; collapseTable: (table: Table) => void; expandTable: (table: Table) => void; - addTable: (queryEditor: any, value: any, schema: any) => void; + addTable: (queryEditor: any, database: any, value: any, schema: any) => void; setDatabases: (arg0: any) => {}; addDangerToast: (msg: string) => void; queryEditorSetSchema: (queryEditor: QueryEditor, schema?: string) => void; @@ -103,7 +103,7 @@ export default function SqlEditorLeftBar({ const onTableChange = (tableName: string, schemaName: string) => { if (tableName && schemaName) { - actions.addTable(queryEditor, tableName, schemaName); + actions.addTable(queryEditor, database, tableName, schemaName); } }; diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index d8d4e23eb1651..2387c2e2517fe 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -76,6 +76,7 @@ beforeEach(() => { allows_cost_estimate: 'Allows Cost Estimate', allows_subquery: 'Allows Subquery', allows_virtual_table_explore: 'Allows Virtual Table Explore', + disable_data_preview: 'Disables SQL Lab Data Preview', backend: 'Backend', changed_on: 'Changed On', changed_on_delta_humanized: 'Changed On Delta Humanized', @@ -97,6 +98,7 @@ beforeEach(() => { 'allows_cost_estimate', 'allows_subquery', 'allows_virtual_table_explore', + 'disable_data_preview', 'backend', 'changed_on', 'changed_on_delta_humanized', @@ -130,6 +132,7 @@ beforeEach(() => { allows_cost_estimate: null, allows_subquery: true, allows_virtual_table_explore: true, + disable_data_preview: false, backend: 'postgresql', changed_on: '2021-03-09T19:02:07.141095', changed_on_delta_humanized: 'a day ago', @@ -150,6 +153,7 @@ beforeEach(() => { allows_cost_estimate: null, allows_subquery: true, allows_virtual_table_explore: true, + disable_data_preview: false, backend: 'mysql', changed_on: '2021-03-09T19:02:07.141095', changed_on_delta_humanized: 'a day ago', diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx index 3d28a4a434252..12a712fa35b52 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/ExtraOptions.tsx @@ -182,7 +182,7 @@ const ExtraOptions = ({ /> - +
+ +
+ + +
+
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx index 6fcbfdad43848..9db2333573dfa 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.test.jsx @@ -591,6 +591,15 @@ describe('DatabaseModal', () => { const allowDbExplorationText = screen.getByText( /allow this database to be explored/i, ); + const disableSQLLabDataPreviewQueriesCheckbox = screen.getByRole( + 'checkbox', + { + name: /Disable SQL Lab data preview queries/i, + }, + ); + const disableSQLLabDataPreviewQueriesText = screen.getByText( + /Disable SQL Lab data preview queries/i, + ); // ---------- Assertions ---------- const visibleComponents = [ @@ -610,6 +619,7 @@ describe('DatabaseModal', () => { checkboxOffSVGs[4], checkboxOffSVGs[5], checkboxOffSVGs[6], + checkboxOffSVGs[7], tooltipIcons[0], tooltipIcons[1], tooltipIcons[2], @@ -617,6 +627,7 @@ describe('DatabaseModal', () => { tooltipIcons[4], tooltipIcons[5], tooltipIcons[6], + tooltipIcons[7], exposeInSQLLabText, allowCTASText, allowCVASText, @@ -627,6 +638,7 @@ describe('DatabaseModal', () => { allowMultiSchemaMDFetchText, enableQueryCostEstimationText, allowDbExplorationText, + disableSQLLabDataPreviewQueriesText, ]; // These components exist in the DOM but are not visible const invisibleComponents = [ @@ -637,6 +649,7 @@ describe('DatabaseModal', () => { allowMultiSchemaMDFetchCheckbox, enableQueryCostEstimationCheckbox, allowDbExplorationCheckbox, + disableSQLLabDataPreviewQueriesCheckbox, ]; visibleComponents.forEach(component => { @@ -645,8 +658,8 @@ describe('DatabaseModal', () => { invisibleComponents.forEach(component => { expect(component).not.toBeVisible(); }); - expect(checkboxOffSVGs).toHaveLength(7); - expect(tooltipIcons).toHaveLength(7); + expect(checkboxOffSVGs).toHaveLength(8); + expect(tooltipIcons).toHaveLength(8); }); it('renders the "Advanced" - PERFORMANCE tab correctly', async () => { diff --git a/superset-frontend/src/views/CRUD/data/database/types.ts b/superset-frontend/src/views/CRUD/data/database/types.ts index 4ffb69535cbe2..c03891689e90b 100644 --- a/superset-frontend/src/views/CRUD/data/database/types.ts +++ b/superset-frontend/src/views/CRUD/data/database/types.ts @@ -92,6 +92,7 @@ export type DatabaseObject = { version?: string; cost_estimate_enabled?: boolean; // in SQL Lab + disable_data_preview?: boolean; // in SQL Lab }; // Temporary storage diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index d2f55d2e15ba8..b1831fdcbbe70 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -175,6 +175,7 @@ class DatabaseSchema(Schema): allows_subquery = fields.Bool() allows_cost_estimate = fields.Bool() allows_virtual_table_explore = fields.Bool() + disable_data_preview = fields.Bool() explore_database_id = fields.Int() diff --git a/superset/databases/api.py b/superset/databases/api.py index 1b8b408c1ca91..83cdf2571a60e 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -145,6 +145,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): "extra", "force_ctas_schema", "id", + "disable_data_preview", ] add_columns = [ "database_name", diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index e030a7e06add8..4483b051f1be0 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -115,10 +115,12 @@ '["public", "csv_upload"]**. ' "If database flavor does not support schema or any schema is allowed " "to be accessed, just leave the list empty
" - "4. the ``version`` field is a string specifying the this db's version. " + "4. The ``version`` field is a string specifying the this db's version. " "This should be used with Presto DBs so that the syntax is correct
" "5. The ``allows_virtual_table_explore`` field is a boolean specifying " - "whether or not the Explore button in SQL Lab results is shown.", + "whether or not the Explore button in SQL Lab results is shown.
" + "6. The ``disable_data_preview`` field is a boolean specifying whether or not data " + "preview queries will be run when fetching table metadata in SQL Lab.", True, ) get_export_ids_schema = {"type": "array", "items": {"type": "integer"}} diff --git a/superset/models/core.py b/superset/models/core.py index 7798ddf05930d..51f0731009950 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -212,6 +212,13 @@ def allows_virtual_table_explore(self) -> bool: def explore_database_id(self) -> int: return self.get_extra().get("explore_database_id", self.id) + @property + def disable_data_preview(self) -> bool: + # this will prevent any 'trash value' strings from going through + if self.get_extra().get("disable_data_preview", False) is not True: + return False + return True + @property def data(self) -> Dict[str, Any]: return { @@ -225,6 +232,7 @@ def data(self) -> Dict[str, Any]: "allows_virtual_table_explore": self.allows_virtual_table_explore, "explore_database_id": self.explore_database_id, "parameters": self.parameters, + "disable_data_preview": self.disable_data_preview, "parameters_schema": self.parameters_schema, } diff --git a/superset/views/core.py b/superset/views/core.py index 32844807b60c7..f2f4ab31ddf72 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -182,6 +182,7 @@ "expose_in_sqllab", "force_ctas_schema", "id", + "disable_data_preview", ] DATASOURCE_MISSING_ERR = __("The data source seems to have been deleted") diff --git a/superset/views/database/mixins.py b/superset/views/database/mixins.py index 5382181d2a3ff..d5a5157ef4f7b 100644 --- a/superset/views/database/mixins.py +++ b/superset/views/database/mixins.py @@ -145,7 +145,10 @@ class DatabaseMixin: "4. the ``version`` field is a string specifying the this db's version. " "This should be used with Presto DBs so that the syntax is correct
" "5. The ``allows_virtual_table_explore`` field is a boolean specifying " - "whether or not the Explore button in SQL Lab results is shown.", + "whether or not the Explore button in SQL Lab results is shown
" + "6. The ``disable_data_preview`` field is a boolean specifying whether or" + "not data preview queries will be run when fetching table metadata in" + "SQL Lab.", True, ), "encrypted_extra": utils.markdown( diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 337becbe48387..26674054ae394 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -1517,6 +1517,29 @@ def test_virtual_table_explore_visibility(self): database.extra = json.dumps(extra) self.assertEqual(database.allows_virtual_table_explore, True) + def test_data_preview_visibility(self): + # test that default visibility is allowed + database = utils.get_example_database() + self.assertEqual(database.disable_data_preview, False) + + # test that visibility is disabled when extra is set to true + extra = database.get_extra() + extra["disable_data_preview"] = True + database.extra = json.dumps(extra) + self.assertEqual(database.disable_data_preview, True) + + # test that visibility is enabled when extra is set to false + extra = database.get_extra() + extra["disable_data_preview"] = False + database.extra = json.dumps(extra) + self.assertEqual(database.disable_data_preview, False) + + # test that visibility is not broken with bad values + extra = database.get_extra() + extra["disable_data_preview"] = "trash value" + database.extra = json.dumps(extra) + self.assertEqual(database.disable_data_preview, False) + def test_explore_database_id(self): database = superset.utils.database.get_example_database() explore_database = superset.utils.database.get_example_database() diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 78d7285e010e7..928f3d595730d 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -179,12 +179,14 @@ def test_get_items(self): "changed_on_delta_humanized", "created_by", "database_name", + "disable_data_preview", "explore_database_id", "expose_in_sqllab", "extra", "force_ctas_schema", "id", ] + self.assertGreater(response["count"], 0) self.assertEqual(list(response["result"][0].keys()), expected_columns) From 4b34817b195fe93aa0bffd211bc978966a4aee4e Mon Sep 17 00:00:00 2001 From: Craig Rueda Date: Thu, 17 Mar 2022 08:58:48 -0700 Subject: [PATCH 02/29] feat(ui): Adding manifest prefix config (#19141) * Adding manifest prefix config * Fixing broken tests * Fixing import * Adding prefix for remaining assets * Changing static prefix strategy * Fixing DST test * Fixing up formatting * Fixing up async_query_manager.py types --- superset-frontend/webpack.config.js | 4 +- superset/common/query_object.py | 2 +- superset/config.py | 6 ++- superset/connectors/base/models.py | 2 +- superset/connectors/druid/models.py | 2 +- superset/connectors/druid/views.py | 2 +- superset/connectors/sqla/models.py | 8 ++- superset/connectors/sqla/views.py | 2 +- superset/databases/api.py | 2 +- superset/extensions.py | 36 +++++++------ superset/initialization/__init__.py | 2 +- superset/result_set.py | 2 +- superset/{typing.py => superset_typing.py} | 0 superset/templates/superset/base.html | 2 +- superset/templates/superset/basic.html | 8 +-- superset/templates/superset/theme.html | 2 +- superset/utils/core.py | 2 +- superset/views/alerts.py | 2 +- superset/views/annotations.py | 2 +- superset/views/api.py | 2 +- superset/views/base.py | 2 +- superset/views/base_api.py | 2 +- superset/views/chart/views.py | 2 +- superset/views/core.py | 2 +- superset/views/css_templates.py | 2 +- superset/views/dashboard/views.py | 2 +- superset/views/database/views.py | 2 +- superset/views/datasource/views.py | 2 +- superset/views/health.py | 2 +- superset/views/key_value.py | 2 +- superset/views/redirects.py | 2 +- superset/views/schedules.py | 2 +- superset/views/sql_lab.py | 2 +- superset/views/tags.py | 2 +- superset/views/utils.py | 2 +- superset/viz.py | 8 ++- .../charts/data/api_tests.py | 2 +- tests/unit_tests/dataframe_test.py | 2 +- tests/unit_tests/extension_tests.py | 51 +++++++++++++++++++ .../fixtures/static/assets/manifest.json | 20 ++++++++ 40 files changed, 147 insertions(+), 56 deletions(-) rename superset/{typing.py => superset_typing.py} (100%) create mode 100644 tests/unit_tests/extension_tests.py create mode 100644 tests/unit_tests/fixtures/static/assets/manifest.json diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index a4c1b86b482eb..6b3c4e30a8b13 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -95,10 +95,10 @@ const plugins = [ entryFiles[entry] = { css: chunks .filter(x => x.endsWith('.css')) - .map(x => path.join(output.publicPath, x)), + .map(x => `${output.publicPath}${x}`), js: chunks .filter(x => x.endsWith('.js')) - .map(x => path.join(output.publicPath, x)), + .map(x => `${output.publicPath}${x}`), }; }); diff --git a/superset/common/query_object.py b/superset/common/query_object.py index 2a40155d1ca4f..fd988a36fac05 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -31,7 +31,7 @@ QueryObjectValidationError, ) from superset.sql_parse import validate_filter_clause -from superset.typing import Column, Metric, OrderBy +from superset.superset_typing import Column, Metric, OrderBy from superset.utils import pandas_postprocessing from superset.utils.core import ( DTTM_ALIAS, diff --git a/superset/config.py b/superset/config.py index 7aa0b42694f27..26ce25228a19b 100644 --- a/superset/config.py +++ b/superset/config.py @@ -46,7 +46,7 @@ from superset.jinja_context import BaseTemplateProcessor from superset.key_value.types import KeyType from superset.stats_logger import DummyStatsLogger -from superset.typing import CacheConfig +from superset.superset_typing import CacheConfig from superset.utils.core import is_test, parse_boolean_string from superset.utils.encrypt import SQLAlchemyUtilsAdapter from superset.utils.log import DBEventLogger @@ -1252,6 +1252,10 @@ def SQL_QUERY_MUTATOR( # pylint: disable=invalid-name,unused-argument # SQLALCHEMY_DATABASE_URI by default if set to `None` SQLALCHEMY_EXAMPLES_URI = None +# Optional prefix to be added to all static asset paths when rendering the UI. +# This is useful for hosting assets in an external CDN, for example +STATIC_ASSETS_PREFIX = "" + # Some sqlalchemy connection strings can open Superset to security risks. # Typically these should not be allowed. PREVENT_UNSAFE_DB_CONNECTIONS = True diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index 967235f328c2e..5cf2a8719bf95 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -29,7 +29,7 @@ from superset.datasets.commands.exceptions import DatasetNotFoundError from superset.models.helpers import AuditMixinNullable, ImportExportMixin, QueryResult from superset.models.slice import Slice -from superset.typing import FilterValue, FilterValues, QueryObjectDict +from superset.superset_typing import FilterValue, FilterValues, QueryObjectDict from superset.utils import core as utils from superset.utils.core import GenericDataType diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py index 32edb695279c0..3a17ec5319374 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -58,7 +58,7 @@ from superset.extensions import encrypted_field_factory from superset.models.core import Database from superset.models.helpers import AuditMixinNullable, ImportExportMixin, QueryResult -from superset.typing import ( +from superset.superset_typing import ( AdhocMetric, AdhocMetricColumn, FilterValues, diff --git a/superset/connectors/druid/views.py b/superset/connectors/druid/views.py index 03a3a42ec08cc..cd7e5d279ba25 100644 --- a/superset/connectors/druid/views.py +++ b/superset/connectors/druid/views.py @@ -34,7 +34,7 @@ from superset.connectors.connector_registry import ConnectorRegistry from superset.connectors.druid import models from superset.constants import RouteMethod -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( BaseSupersetView, diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 4a68e666bdac8..99cbc50997559 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -96,8 +96,14 @@ QueryResult, ) from superset.sql_parse import ParsedQuery +from superset.superset_typing import ( + AdhocColumn, + AdhocMetric, + Metric, + OrderBy, + QueryObjectDict, +) from superset.tables.models import Table as NewTable -from superset.typing import AdhocColumn, AdhocMetric, Metric, OrderBy, QueryObjectDict from superset.utils import core as utils from superset.utils.core import ( GenericDataType, diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index fef8a2d8a4356..a16ffa49f62ba 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -36,7 +36,7 @@ from superset.connectors.base.views import DatasourceModelView from superset.connectors.sqla import models from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( check_ownership, diff --git a/superset/databases/api.py b/superset/databases/api.py index 83cdf2571a60e..3737addedeb92 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -70,7 +70,7 @@ from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.extensions import security_manager from superset.models.core import Database -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils.core import error_msg_from_exception from superset.views.base_api import ( BaseSupersetModelRestApi, diff --git a/superset/extensions.py b/superset/extensions.py index 33dc1706a6b78..742182b078d1b 100644 --- a/superset/extensions.py +++ b/superset/extensions.py @@ -63,22 +63,26 @@ def init_app(self, app: Flask) -> None: self.app = app # Preload the cache self.parse_manifest_json() - - @app.context_processor - def get_manifest() -> Dict[str, Callable[[str], List[str]]]: - loaded_chunks = set() - - def get_files(bundle: str, asset_type: str = "js") -> List[str]: - files = self.get_manifest_files(bundle, asset_type) - filtered_files = [f for f in files if f not in loaded_chunks] - for f in filtered_files: - loaded_chunks.add(f) - return filtered_files - - return dict( - js_manifest=lambda bundle: get_files(bundle, "js"), - css_manifest=lambda bundle: get_files(bundle, "css"), - ) + self.register_processor(app) + + def register_processor(self, app: Flask) -> None: + app.template_context_processors[None].append(self.get_manifest) + + def get_manifest(self) -> Dict[str, Callable[[str], List[str]]]: + loaded_chunks = set() + + def get_files(bundle: str, asset_type: str = "js") -> List[str]: + files = self.get_manifest_files(bundle, asset_type) + filtered_files = [f for f in files if f not in loaded_chunks] + for f in filtered_files: + loaded_chunks.add(f) + return filtered_files + + return dict( + js_manifest=lambda bundle: get_files(bundle, "js"), + css_manifest=lambda bundle: get_files(bundle, "css"), + assets_prefix=self.app.config["STATIC_ASSETS_PREFIX"] if self.app else "", + ) def parse_manifest_json(self) -> None: try: diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index ee4c3621abf0e..6e2d927efd522 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -49,7 +49,7 @@ talisman, ) from superset.security import SupersetSecurityManager -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils.core import pessimistic_connection_handling from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value diff --git a/superset/result_set.py b/superset/result_set.py index b95b5e680d7db..19035b6d23788 100644 --- a/superset/result_set.py +++ b/superset/result_set.py @@ -26,7 +26,7 @@ import pyarrow as pa from superset.db_engine_specs import BaseEngineSpec -from superset.typing import DbapiDescription, DbapiResult +from superset.superset_typing import DbapiDescription, DbapiResult from superset.utils import core as utils logger = logging.getLogger(__name__) diff --git a/superset/typing.py b/superset/superset_typing.py similarity index 100% rename from superset/typing.py rename to superset/superset_typing.py diff --git a/superset/templates/superset/base.html b/superset/templates/superset/base.html index a861c659e7034..e3c3d35dfe503 100644 --- a/superset/templates/superset/base.html +++ b/superset/templates/superset/base.html @@ -21,7 +21,7 @@ {% block head_css %} {{ super() }} - + {{ css_bundle("theme") }} {% endblock %} diff --git a/superset/templates/superset/basic.html b/superset/templates/superset/basic.html index 902fc8c328de4..fff57fdb9fa18 100644 --- a/superset/templates/superset/basic.html +++ b/superset/templates/superset/basic.html @@ -40,11 +40,11 @@ rel="{{favicon.rel if favicon.rel else "icon"}}" type="{{favicon.type if favicon.type else "image/png"}}" {% if favicon.sizes %}sizes={{favicon.sizes}}{% endif %} - href="{{favicon.href}}" + href="{{ assets_prefix }}{{favicon.href}}" > {% endfor %} - - + + {{ css_bundle("theme") }} @@ -73,7 +73,7 @@ {% block body %}
- +
{% endblock %} diff --git a/superset/templates/superset/theme.html b/superset/templates/superset/theme.html index feac56f895980..856796a4c4b21 100644 --- a/superset/templates/superset/theme.html +++ b/superset/templates/superset/theme.html @@ -1342,5 +1342,5 @@ {{ super() }} - + {% endblock %} diff --git a/superset/utils/core.py b/superset/utils/core.py index 2fdbc278adb70..36d59333d2ed3 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -98,7 +98,7 @@ SupersetException, SupersetTimeoutException, ) -from superset.typing import ( +from superset.superset_typing import ( AdhocColumn, AdhocMetric, AdhocMetricColumn, diff --git a/superset/views/alerts.py b/superset/views/alerts.py index e96f701c3d1b7..416966fbe7c35 100644 --- a/superset/views/alerts.py +++ b/superset/views/alerts.py @@ -30,8 +30,8 @@ from superset import is_feature_enabled from superset.constants import RouteMethod from superset.models.alerts import Alert, AlertLog, SQLObservation +from superset.superset_typing import FlaskResponse from superset.tasks.alerts.validator import check_validator -from superset.typing import FlaskResponse from superset.utils import core as utils from superset.utils.core import get_email_address_str, markdown diff --git a/superset/views/annotations.py b/superset/views/annotations.py index 4fa83c0ca4be4..dc1df5642af35 100644 --- a/superset/views/annotations.py +++ b/superset/views/annotations.py @@ -26,7 +26,7 @@ from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.annotations import Annotation, AnnotationLayer -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.views.base import SupersetModelView diff --git a/superset/views/api.py b/superset/views/api.py index d4d94ce72346c..bde25236460da 100644 --- a/superset/views/api.py +++ b/superset/views/api.py @@ -31,7 +31,7 @@ ) from superset.legacy import update_time_range from superset.models.slice import Slice -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.utils.date_parser import get_since_until from superset.views.base import api, BaseSupersetView, handle_api_exception diff --git a/superset/views/base.py b/superset/views/base.py index 1249bc43cc4fb..3024c4490d167 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -73,8 +73,8 @@ ) from superset.models.helpers import ImportExportMixin from superset.models.reports import ReportRecipientType +from superset.superset_typing import FlaskResponse from superset.translations.utils import get_language_pack -from superset.typing import FlaskResponse from superset.utils import core as utils from .utils import bootstrap_user_data diff --git a/superset/views/base_api.py b/superset/views/base_api.py index 87e99e7c74a7b..260e5731788bc 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -37,7 +37,7 @@ from superset.schemas import error_payload_content from superset.sql_lab import Query as SqllabQuery from superset.stats_logger import BaseStatsLogger -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils.core import time_function logger = logging.getLogger(__name__) diff --git a/superset/views/chart/views.py b/superset/views/chart/views.py index 37ef9a043e881..9ecc69f7b9e8e 100644 --- a/superset/views/chart/views.py +++ b/superset/views/chart/views.py @@ -24,7 +24,7 @@ from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.slice import Slice -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( check_ownership, diff --git a/superset/views/core.py b/superset/views/core.py index f2f4ab31ddf72..f014ca2845ea2 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -123,8 +123,8 @@ from superset.sqllab.sqllab_execution_context import SqlJsonExecutionContext from superset.sqllab.utils import apply_display_max_row_configuration_if_require from superset.sqllab.validators import CanAccessQueryValidatorImpl +from superset.superset_typing import FlaskResponse from superset.tasks.async_queries import load_explore_json_into_cache -from superset.typing import FlaskResponse from superset.utils import core as utils, csv from superset.utils.async_query_manager import AsyncQueryTokenException from superset.utils.cache import etag_cache diff --git a/superset/views/css_templates.py b/superset/views/css_templates.py index d26acea5cfac7..2cfbd43ae962a 100644 --- a/superset/views/css_templates.py +++ b/superset/views/css_templates.py @@ -22,7 +22,7 @@ from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models import core as models -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.views.base import DeleteMixin, SupersetModelView diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py index 99782def38a68..49ba61d08e0d2 100644 --- a/superset/views/dashboard/views.py +++ b/superset/views/dashboard/views.py @@ -29,7 +29,7 @@ from superset import db, event_logger, is_feature_enabled, security_manager from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.dashboard import Dashboard as DashboardModel -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import ( BaseSupersetView, diff --git a/superset/views/database/views.py b/superset/views/database/views.py index 115d168ed636a..aea4e04383570 100644 --- a/superset/views/database/views.py +++ b/superset/views/database/views.py @@ -37,7 +37,7 @@ from superset.exceptions import CertificateException from superset.extensions import event_logger from superset.sql_parse import Table -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import DeleteMixin, SupersetModelView, YamlExportMixin diff --git a/superset/views/datasource/views.py b/superset/views/datasource/views.py index e2cb204082dd6..7e1ffa0468e90 100644 --- a/superset/views/datasource/views.py +++ b/superset/views/datasource/views.py @@ -38,7 +38,7 @@ from superset.exceptions import SupersetException, SupersetSecurityException from superset.extensions import security_manager from superset.models.core import Database -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.views.base import ( api, BaseSupersetView, diff --git a/superset/views/health.py b/superset/views/health.py index 876e7a5e130be..cf85b8927899d 100644 --- a/superset/views/health.py +++ b/superset/views/health.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. from superset import app, talisman -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse @talisman(force_https=False) diff --git a/superset/views/key_value.py b/superset/views/key_value.py index 8f8aa99787a21..da39f094b812f 100644 --- a/superset/views/key_value.py +++ b/superset/views/key_value.py @@ -23,7 +23,7 @@ from superset import db, event_logger, is_feature_enabled from superset.models import core as models -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from superset.views.base import BaseSupersetView, json_error_response diff --git a/superset/views/redirects.py b/superset/views/redirects.py index 805d917f7e162..831fc978b9473 100644 --- a/superset/views/redirects.py +++ b/superset/views/redirects.py @@ -23,7 +23,7 @@ from superset import db, event_logger from superset.models import core as models -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.views.base import BaseSupersetView logger = logging.getLogger(__name__) diff --git a/superset/views/schedules.py b/superset/views/schedules.py index d1c59f413c839..39d4af9b8b259 100644 --- a/superset/views/schedules.py +++ b/superset/views/schedules.py @@ -42,8 +42,8 @@ SliceEmailSchedule, ) from superset.models.slice import Slice +from superset.superset_typing import FlaskResponse from superset.tasks.schedules import schedule_email_report -from superset.typing import FlaskResponse from superset.utils.core import get_email_address_list, json_iso_dttm_ser from superset.views.core import json_success diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index 6a5ce26d38ad9..5ec525b9cac73 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -24,7 +24,7 @@ from superset import db, is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from superset.utils import core as utils from .base import BaseSupersetView, DeleteMixin, json_success, SupersetModelView diff --git a/superset/views/tags.py b/superset/views/tags.py index c6fac2ff77145..8ab2798f5d84c 100644 --- a/superset/views/tags.py +++ b/superset/views/tags.py @@ -33,7 +33,7 @@ from superset.models.slice import Slice from superset.models.sql_lab import SavedQuery from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes -from superset.typing import FlaskResponse +from superset.superset_typing import FlaskResponse from .base import BaseSupersetView, json_success diff --git a/superset/views/utils.py b/superset/views/utils.py index 17ec6ea1088c9..62639174f647e 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -46,7 +46,7 @@ from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.models.sql_lab import Query -from superset.typing import FormData +from superset.superset_typing import FormData from superset.utils.decorators import stats_timing from superset.viz import BaseViz diff --git a/superset/viz.py b/superset/viz.py index 9a1086442be62..7c0f8e134875b 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -70,7 +70,13 @@ from superset.extensions import cache_manager, security_manager from superset.models.helpers import QueryResult from superset.sql_parse import validate_filter_clause -from superset.typing import Column, Metric, QueryObjectDict, VizData, VizPayload +from superset.superset_typing import ( + Column, + Metric, + QueryObjectDict, + VizData, + VizPayload, +) from superset.utils import core as utils, csv from superset.utils.cache import set_and_log_cache from superset.utils.core import ( diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py index 4f63ad51b65d7..bc8ec74feb0d6 100644 --- a/tests/integration_tests/charts/data/api_tests.py +++ b/tests/integration_tests/charts/data/api_tests.py @@ -47,7 +47,7 @@ from superset.extensions import async_query_manager, db from superset.models.annotations import AnnotationLayer from superset.models.slice import Slice -from superset.typing import AdhocColumn +from superset.superset_typing import AdhocColumn from superset.utils.core import ( AnnotationType, get_example_default_schema, diff --git a/tests/unit_tests/dataframe_test.py b/tests/unit_tests/dataframe_test.py index 3e986a5e43a7f..79625cffe63de 100644 --- a/tests/unit_tests/dataframe_test.py +++ b/tests/unit_tests/dataframe_test.py @@ -16,7 +16,7 @@ # under the License. # pylint: disable=unused-argument, import-outside-toplevel from superset.dataframe import df_to_records -from superset.typing import DbapiDescription +from superset.superset_typing import DbapiDescription def test_df_to_records(app_context: None) -> None: diff --git a/tests/unit_tests/extension_tests.py b/tests/unit_tests/extension_tests.py new file mode 100644 index 0000000000000..724b03f01a2ab --- /dev/null +++ b/tests/unit_tests/extension_tests.py @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from os.path import dirname +from unittest.mock import Mock + +from superset.extensions import UIManifestProcessor + +APP_DIR = f"{dirname(__file__)}/fixtures" + + +def test_get_manifest_with_prefix(): + app = Mock( + config={"STATIC_ASSETS_PREFIX": "https://cool.url/here"}, + template_context_processors={None: []}, + ) + manifest_processor = UIManifestProcessor(APP_DIR) + manifest_processor.init_app(app) + manifest = manifest_processor.get_manifest() + assert manifest["js_manifest"]("main") == ["/static/dist/main-js.js"] + assert manifest["css_manifest"]("main") == ["/static/dist/main-css.css"] + assert manifest["js_manifest"]("styles") == ["/static/dist/styles-js.js"] + assert manifest["css_manifest"]("styles") == [] + assert manifest["assets_prefix"] == "https://cool.url/here" + + +def test_get_manifest_no_prefix(): + app = Mock( + config={"STATIC_ASSETS_PREFIX": ""}, template_context_processors={None: []} + ) + manifest_processor = UIManifestProcessor(APP_DIR) + manifest_processor.init_app(app) + manifest = manifest_processor.get_manifest() + assert manifest["js_manifest"]("main") == ["/static/dist/main-js.js"] + assert manifest["css_manifest"]("main") == ["/static/dist/main-css.css"] + assert manifest["js_manifest"]("styles") == ["/static/dist/styles-js.js"] + assert manifest["css_manifest"]("styles") == [] + assert manifest["assets_prefix"] == "" diff --git a/tests/unit_tests/fixtures/static/assets/manifest.json b/tests/unit_tests/fixtures/static/assets/manifest.json new file mode 100644 index 0000000000000..7482a04eac74e --- /dev/null +++ b/tests/unit_tests/fixtures/static/assets/manifest.json @@ -0,0 +1,20 @@ +{ + "entrypoints": { + "styles": { + "js": [ + "/static/dist/styles-js.js" + ] + }, + "main": { + "css": [ + "/static/dist/main-css.css" + ], + "js": [ + "/static/dist/main-js.js" + ] + } + }, + "main.css": "/static/dist/main.b51d3f6225194da423d6.entry.css", + "main.js": "/static/dist/main.b51d3f6225194da423d6.entry.js", + "styles.js": "/static/dist/styles.35840b4bbf794f902b7c.entry.js" +} From 3230415e22eaa2fdf96d6f5d29f664126e97ef6f Mon Sep 17 00:00:00 2001 From: "Hugh A. Miles II" Date: Thu, 17 Mar 2022 09:19:11 -0700 Subject: [PATCH 03/29] remove config (#19146) --- .../CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx | 5 ++++- superset/config.py | 4 ---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx index 7226efdcb4b37..96a0bfef07cc2 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx @@ -45,7 +45,10 @@ const SqlAlchemyTab = ({ fallbackDocsUrl = SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DOCS_URL; fallbackDisplayText = - SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DOCS_URL; + SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DISPLAY_TEXT; + } else { + fallbackDocsUrl = 'https://docs.sqlalchemy.org/en/13/core/engines.html'; + fallbackDisplayText = 'SQLAlchemy docs'; } return ( <> diff --git a/superset/config.py b/superset/config.py index 26ce25228a19b..f2ed52206b93a 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1343,10 +1343,6 @@ def SQL_QUERY_MUTATOR( # pylint: disable=invalid-name,unused-argument # Do not show user info or profile in the menu MENU_HIDE_USER_INFO = False -# SQLalchemy link doc reference -SQLALCHEMY_DOCS_URL = "https://docs.sqlalchemy.org/en/13/core/engines.html" -SQLALCHEMY_DISPLAY_TEXT = "SQLAlchemy docs" - # Set to False to only allow viewing own recent activity ENABLE_BROAD_ACTIVITY_ACCESS = True From d099f5ed4ad6f5b553c7e3eedbc34cf5ad55eae7 Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Thu, 17 Mar 2022 13:04:03 -0400 Subject: [PATCH 04/29] fix(select): make to consider the case sensitive in case of d3 format selector (#19159) --- .../src/shared-controls/index.tsx | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 81cd563c807bb..a5de8eff91f71 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -94,6 +94,11 @@ type Control = { default?: unknown; }; +type SelectDefaultOption = { + label: string; + value: string; +}; + const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = { type: 'SelectControl', label: t('Group by'), @@ -430,29 +435,36 @@ const size: SharedControlConfig<'MetricsControl'> = { default: null, }; -const y_axis_format: SharedControlConfig<'SelectControl'> = { - type: 'SelectControl', - freeForm: true, - label: t('Y Axis Format'), - renderTrigger: true, - default: DEFAULT_NUMBER_FORMAT, - choices: D3_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - mapStateToProps: state => { - const showWarning = state.controls?.comparison_type?.value === 'percentage'; - return { - warning: showWarning - ? t( - 'When `Calculation type` is set to "Percentage change", the Y ' + - 'Axis Format is forced to `.1%`', - ) - : null, - disabled: showWarning, - }; - }, -}; - -const x_axis_time_format: SharedControlConfig<'SelectControl'> = { +const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> = + { + type: 'SelectControl', + freeForm: true, + label: t('Y Axis Format'), + renderTrigger: true, + default: DEFAULT_NUMBER_FORMAT, + choices: D3_FORMAT_OPTIONS, + description: D3_FORMAT_DOCS, + filterOption: ({ data: option }, search) => + option.label.includes(search) || option.value.includes(search), + mapStateToProps: state => { + const showWarning = + state.controls?.comparison_type?.value === 'percentage'; + return { + warning: showWarning + ? t( + 'When `Calculation type` is set to "Percentage change", the Y ' + + 'Axis Format is forced to `.1%`', + ) + : null, + disabled: showWarning, + }; + }, + }; + +const x_axis_time_format: SharedControlConfig< + 'SelectControl', + SelectDefaultOption +> = { type: 'SelectControl', freeForm: true, label: t('Time format'), @@ -460,6 +472,8 @@ const x_axis_time_format: SharedControlConfig<'SelectControl'> = { default: DEFAULT_TIME_FORMAT, choices: D3_TIME_FORMAT_OPTIONS, description: D3_TIME_FORMAT_DOCS, + filterOption: ({ data: option }, search) => + option.label.includes(search) || option.value.includes(search), }; const adhoc_filters: SharedControlConfig<'AdhocFilterControl'> = { From 19fcd03c8962b5ae2d2bb5cd196b1ef07a27b9c3 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Thu, 17 Mar 2022 13:05:32 -0400 Subject: [PATCH 05/29] fix: allow to select in a native filter single mode (#19076) * fix: allow to select in a native filter single mode * fix lint issue * Update superset-frontend/src/components/Select/utils.ts Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * fix Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> --- .../src/components/Select/utils.ts | 6 +++-- .../Select/SelectFilterPlugin.test.tsx | 27 ++++++++++++++++++- .../components/Select/SelectFilterPlugin.tsx | 3 ++- .../src/filters/components/Select/types.ts | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/components/Select/utils.ts b/superset-frontend/src/components/Select/utils.ts index f62b93ade3668..f3880f52f7d01 100644 --- a/superset-frontend/src/components/Select/utils.ts +++ b/superset-frontend/src/components/Select/utils.ts @@ -60,8 +60,10 @@ export function findValue( return (Array.isArray(value) ? value : [value]).map(find); } -export function getValue(option: string | number | { value: string | number }) { - return typeof option === 'object' ? option.value : option; +export function getValue( + option: string | number | { value: string | number | null } | null, +) { + return option && typeof option === 'object' ? option.value : option; } type LabeledValue = { label?: ReactNode; value?: V }; diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx index 549217619b78a..8c025c97119bd 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.test.tsx @@ -20,6 +20,7 @@ import userEvent from '@testing-library/user-event'; import { AppSection } from '@superset-ui/core'; import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; +import { NULL_STRING } from 'src/utils/common'; import SelectFilterPlugin from './SelectFilterPlugin'; import transformProps from './transformProps'; @@ -55,7 +56,7 @@ const selectMultipleProps = { rowcount: 2, colnames: ['gender'], coltypes: [1], - data: [{ gender: 'boy' }, { gender: 'girl' }], + data: [{ gender: 'boy' }, { gender: 'girl' }, { gender: null }], applied_filters: [{ column: 'gender' }], rejected_filters: [], }, @@ -195,6 +196,30 @@ describe('SelectFilterPlugin', () => { }); }); + it('Select single null (empty) value', () => { + getWrapper(); + userEvent.click(screen.getByRole('combobox')); + userEvent.click(screen.getByTitle(NULL_STRING)); + expect(setDataMask).toHaveBeenLastCalledWith({ + __cache: { + value: ['boy'], + }, + extraFormData: { + filters: [ + { + col: 'gender', + op: 'IN', + val: ['boy', null], + }, + ], + }, + filterState: { + label: `boy, ${NULL_STRING}`, + value: ['boy', null], + }, + }); + }); + it('Add ownState with column types when search all options', () => { getWrapper({ searchAllOptions: true, multiSelect: false }); userEvent.click(screen.getByRole('combobox')); diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index 16c748a174b32..8388e13a3d594 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -209,7 +209,8 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { const handleChange = useCallback( (value?: SelectValue | number | string) => { - const values = ensureIsArray(value); + const values = value === null ? [null] : ensureIsArray(value); + if (values.length === 0) { updateDataMask(null); } else { diff --git a/superset-frontend/src/filters/components/Select/types.ts b/superset-frontend/src/filters/components/Select/types.ts index 2d4f0c958669f..0497b58e55581 100644 --- a/superset-frontend/src/filters/components/Select/types.ts +++ b/superset-frontend/src/filters/components/Select/types.ts @@ -29,7 +29,7 @@ import { import { RefObject } from 'react'; import { PluginFilterHooks, PluginFilterStylesProps } from '../types'; -export type SelectValue = (number | string)[] | null | undefined; +export type SelectValue = (number | string | null)[] | null | undefined; export interface PluginFilterSelectCustomizeProps { defaultValue?: SelectValue; From 6593a727f2b0873788bf20d6c023c8d04cfa8e13 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 17 Mar 2022 18:19:29 +0100 Subject: [PATCH 06/29] chore: Update UPDATING.md with info about flipping dnd feature flag (#19108) --- UPDATING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPDATING.md b/UPDATING.md index 854e18074d9e0..a2e7c76881ddf 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -24,6 +24,7 @@ assists people when migrating to a new version. ## Next +- [19046](/~https://github.com/apache/superset/pull/19046): Enables the drag and drop interface in Explore control panel by default. Flips `ENABLE_EXPLORE_DRAG_AND_DROP` and `ENABLE_DND_WITH_CLICK_UX` feature flags to `True`. - [18936](/~https://github.com/apache/superset/pull/18936): Removes legacy SIP-15 interm logic/flags—specifically the `SIP_15_ENABLED`, `SIP_15_GRACE_PERIOD_END`, `SIP_15_DEFAULT_TIME_RANGE_ENDPOINTS`, and `SIP_15_TOAST_MESSAGE` flags. Time range endpoints are no longer configurable and strictly adhere to the `[start, end)` paradigm, i.e., inclusive of the start and exclusive of the end. Additionally this change removes the now obsolete `time_range_endpoints` from the form-data and resulting in the cache being busted. ### Breaking Changes From c345029fbc1a94af0d0f9d7e079091a2ee5b9f76 Mon Sep 17 00:00:00 2001 From: AAfghahi <48933336+AAfghahi@users.noreply.github.com> Date: Thu, 17 Mar 2022 13:32:50 -0400 Subject: [PATCH 07/29] chore!: turn on Versioned Export in config.py (#19142) * turning off versioned export * deleted two tests that required version export false * added tests * test suggestions --- UPDATING.md | 1 + superset/config.py | 2 +- tests/integration_tests/cli_tests.py | 13 +++++++++--- .../integration_tests/dashboards/api_tests.py | 20 +++++-------------- tests/integration_tests/datasets/api_tests.py | 15 -------------- 5 files changed, 17 insertions(+), 34 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index a2e7c76881ddf..8d17dab043d87 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -38,6 +38,7 @@ assists people when migrating to a new version. - [18970](/~https://github.com/apache/superset/pull/18970): Changes feature flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in config.py to True, thus disabling the feature from being shown in the client. - [19017](/~https://github.com/apache/superset/pull/19017): Removes Python 3.7 support. +- [19142](/~https://github.com/apache/superset/pull/19142): Changes feature flag for versioned export(VERSIONED_EXPORT) to be true. ### Potential Downtime diff --git a/superset/config.py b/superset/config.py index f2ed52206b93a..efb5178679d4a 100644 --- a/superset/config.py +++ b/superset/config.py @@ -408,7 +408,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: "DASHBOARD_NATIVE_FILTERS_SET": False, "DASHBOARD_FILTERS_EXPERIMENTAL": False, "GLOBAL_ASYNC_QUERIES": False, - "VERSIONED_EXPORT": False, + "VERSIONED_EXPORT": True, # Note that: RowLevelSecurityFilter is only given by default to the Admin role # and the Admin Role does have the all_datasources security permission. # But, if users create a specific role with access to RowLevelSecurityFilter MVC diff --git a/tests/integration_tests/cli_tests.py b/tests/integration_tests/cli_tests.py index 3f4725640e3c3..7426d90ea88af 100644 --- a/tests/integration_tests/cli_tests.py +++ b/tests/integration_tests/cli_tests.py @@ -47,6 +47,9 @@ def assert_cli_fails_properly(response, caplog): assert caplog.records[-1].levelname == "ERROR" +@mock.patch.dict( + "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True +) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_export_dashboards_original(app_context, fs): """ @@ -73,6 +76,9 @@ def test_export_dashboards_original(app_context, fs): json.loads(contents) +@mock.patch.dict( + "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True +) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_export_datasources_original(app_context, fs): """ @@ -91,6 +97,7 @@ def test_export_datasources_original(app_context, fs): ) assert response.exit_code == 0 + assert Path("datasources.yaml").exists() # check that file is valid JSON @@ -336,7 +343,7 @@ def test_import_datasets_versioned_export(import_datasets_command, app_context, @mock.patch.dict( - "superset.config.DEFAULT_FEATURE_FLAGS", {"VERSIONED_EXPORT": False}, clear=True + "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True ) @mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand") def test_import_datasets_sync_argument_columns_metrics( @@ -371,7 +378,7 @@ def test_import_datasets_sync_argument_columns_metrics( @mock.patch.dict( - "superset.config.DEFAULT_FEATURE_FLAGS", {"VERSIONED_EXPORT": False}, clear=True + "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True ) @mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand") def test_import_datasets_sync_argument_columns( @@ -406,7 +413,7 @@ def test_import_datasets_sync_argument_columns( @mock.patch.dict( - "superset.config.DEFAULT_FEATURE_FLAGS", {"VERSIONED_EXPORT": False}, clear=True + "superset.cli.lib.feature_flags", {"VERSIONED_EXPORT": False}, clear=True ) @mock.patch("superset.datasets.commands.importers.v0.ImportDatasetsCommand") def test_import_datasets_sync_argument_metrics( diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index 755eb3776016d..2ed627a257247 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -1331,6 +1331,11 @@ def test_update_dashboard_not_owned(self): db.session.delete(user_alpha2) db.session.commit() + @patch.dict( + "superset.extensions.feature_flag_manager._feature_flags", + {"VERSIONED_EXPORT": False}, + clear=True, + ) @pytest.mark.usefixtures( "load_world_bank_dashboard_with_slices", "load_birth_names_dashboard_with_slices", @@ -1376,11 +1381,6 @@ def test_export_not_allowed(self): db.session.delete(dashboard) db.session.commit() - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) def test_export_bundle(self): """ Dashboard API: Test dashboard export @@ -1396,11 +1396,6 @@ def test_export_bundle(self): buf = BytesIO(rv.data) assert is_zipfile(buf) - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) def test_export_bundle_not_found(self): """ Dashboard API: Test dashboard export not found @@ -1411,11 +1406,6 @@ def test_export_bundle_not_found(self): rv = self.client.get(uri) assert rv.status_code == 404 - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) def test_export_bundle_not_allowed(self): """ Dashboard API: Test dashboard export not allowed diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index eeda824500fe6..7626de677bf01 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -1455,11 +1455,6 @@ def test_export_dataset_gamma(self): rv = self.client.get(uri) assert rv.status_code == 200 - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_export_dataset_bundle(self): """ @@ -1482,11 +1477,6 @@ def test_export_dataset_bundle(self): buf = BytesIO(rv.data) assert is_zipfile(buf) - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) def test_export_dataset_bundle_not_found(self): """ Dataset API: Test export dataset not found @@ -1499,11 +1489,6 @@ def test_export_dataset_bundle_not_found(self): assert rv.status_code == 404 - @patch.dict( - "superset.extensions.feature_flag_manager._feature_flags", - {"VERSIONED_EXPORT": True}, - clear=True, - ) @pytest.mark.usefixtures("create_datasets") def test_export_dataset_bundle_gamma(self): """ From 92cd0a18e69efe34be8430620efface4287da85f Mon Sep 17 00:00:00 2001 From: Jesse Yang Date: Thu, 17 Mar 2022 10:35:53 -0700 Subject: [PATCH 08/29] chore: upgrade mypy (#19227) --- .pre-commit-config.yaml | 2 +- RELEASING/changelog.py | 4 ++-- superset/charts/data/api.py | 11 ++++------- superset/cli/main.py | 2 +- superset/models/helpers.py | 2 +- superset/reports/commands/alert.py | 3 +-- superset/reports/notifications/base.py | 2 +- superset/utils/async_query_manager.py | 2 +- superset/views/core.py | 11 ++++------- 9 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8731b8aa3d1bb..1f29891dfddc9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: /~https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.941 hooks: - id: mypy additional_dependencies: [types-all] diff --git a/RELEASING/changelog.py b/RELEASING/changelog.py index 0cf600280b799..441e3092d047e 100644 --- a/RELEASING/changelog.py +++ b/RELEASING/changelog.py @@ -381,12 +381,12 @@ def change_log( with open(csv, "w") as csv_file: log_items = list(logs) field_names = log_items[0].keys() - writer = lib_csv.DictWriter( # type: ignore + writer = lib_csv.DictWriter( csv_file, delimiter=",", quotechar='"', quoting=lib_csv.QUOTE_ALL, - fieldnames=field_names, # type: ignore + fieldnames=field_names, ) writer.writeheader() for log in logs: diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py index dc92d97458c41..73468d651cbc5 100644 --- a/superset/charts/data/api.py +++ b/superset/charts/data/api.py @@ -306,16 +306,13 @@ def _run_async( Execute command as an async query. """ # First, look for the chart query results in the cache. + result = None try: result = command.run(force_cached=True) + if result is not None: + return self._send_chart_response(result) except ChartDataCacheLoadError: - result = None # type: ignore - - already_cached_result = result is not None - - # If the chart query has already been cached, return it immediately. - if already_cached_result: - return self._send_chart_response(result) + pass # Otherwise, kick off a background job to run the chart query. # Clients will either poll or be notified of query completion, diff --git a/superset/cli/main.py b/superset/cli/main.py index 45b4c9e46a101..a1a03e9de26d0 100755 --- a/superset/cli/main.py +++ b/superset/cli/main.py @@ -45,7 +45,7 @@ def make_shell_context() -> Dict[str, Any]: # add sub-commands for load, module_name, is_pkg in pkgutil.walk_packages( - cli.__path__, cli.__name__ + "." # type: ignore + cli.__path__, cli.__name__ + "." ): module = importlib.import_module(module_name) for attribute in module.__dict__.values(): diff --git a/superset/models/helpers.py b/superset/models/helpers.py index f1adadfbc453f..86ac2c1a98717 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -221,7 +221,7 @@ def import_from_dict( if not obj: is_new_obj = True # Create new DB object - obj = cls(**dict_rep) # type: ignore + obj = cls(**dict_rep) logger.info("Importing new %s %s", obj.__tablename__, str(obj)) if cls.export_parent and parent: setattr(obj, cls.export_parent, parent) diff --git a/superset/reports/commands/alert.py b/superset/reports/commands/alert.py index e00ac9f2df5c1..f5879a037f378 100644 --- a/superset/reports/commands/alert.py +++ b/superset/reports/commands/alert.py @@ -77,8 +77,7 @@ def run(self) -> bool: threshold = json.loads(self._report_schedule.validator_config_json)[ "threshold" ] - - return OPERATOR_FUNCTIONS[operator](self._result, threshold) + return OPERATOR_FUNCTIONS[operator](self._result, threshold) # type: ignore except (KeyError, json.JSONDecodeError) as ex: raise AlertValidatorConfigError() from ex diff --git a/superset/reports/notifications/base.py b/superset/reports/notifications/base.py index 3331e51297a75..06bfecf790144 100644 --- a/superset/reports/notifications/base.py +++ b/superset/reports/notifications/base.py @@ -50,7 +50,7 @@ class BaseNotification: # pylint: disable=too-few-public-methods """ def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: - super().__init_subclass__(*args, **kwargs) # type: ignore + super().__init_subclass__(*args, **kwargs) cls.plugins.append(cls) def __init__( diff --git a/superset/utils/async_query_manager.py b/superset/utils/async_query_manager.py index fcda931fcd880..a026fd6f3f3d7 100644 --- a/superset/utils/async_query_manager.py +++ b/superset/utils/async_query_manager.py @@ -71,7 +71,7 @@ class AsyncQueryManager: def __init__(self) -> None: super().__init__() - self._redis: redis.Redis + self._redis: redis.Redis # type: ignore self._stream_prefix: str = "" self._stream_limit: Optional[int] self._stream_limit_firehose: Optional[int] diff --git a/superset/views/core.py b/superset/views/core.py index f014ca2845ea2..6957296ab634c 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -653,14 +653,11 @@ def explore_json( force=force, ) payload = viz_obj.get_payload() + # If the chart query has already been cached, return it immediately. + if payload is not None: + return self.send_data_payload_response(viz_obj, payload) except CacheLoadError: - payload = None # type: ignore - - already_cached_result = payload is not None - - # If the chart query has already been cached, return it immediately. - if already_cached_result: - return self.send_data_payload_response(viz_obj, payload) + pass # Otherwise, kick off a background job to run the chart query. # Clients will either poll or be notified of query completion, From 51061f0d672abca29f84943acb16a37403f25c2e Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Thu, 17 Mar 2022 10:51:17 -0700 Subject: [PATCH 09/29] feat: import/export assets commands (#19217) * feat: import/export assets commands * Add overwrite test * Fix tests --- superset/commands/export/assets.py | 64 +++++++++ superset/commands/importers/v1/assets.py | 164 ++++++++++++++++++++++ tests/integration_tests/commands_test.py | 167 +++++++++++++++++++++++ tests/unit_tests/commands/__init__.py | 16 +++ tests/unit_tests/commands/export_test.py | 94 +++++++++++++ 5 files changed, 505 insertions(+) create mode 100644 superset/commands/export/assets.py create mode 100644 superset/commands/importers/v1/assets.py create mode 100644 tests/unit_tests/commands/__init__.py create mode 100644 tests/unit_tests/commands/export_test.py diff --git a/superset/commands/export/assets.py b/superset/commands/export/assets.py new file mode 100644 index 0000000000000..8711cac4dd01c --- /dev/null +++ b/superset/commands/export/assets.py @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from datetime import datetime, timezone +from typing import Iterator, Tuple + +import yaml + +from superset.charts.commands.export import ExportChartsCommand +from superset.commands.base import BaseCommand +from superset.dashboards.commands.export import ExportDashboardsCommand +from superset.databases.commands.export import ExportDatabasesCommand +from superset.datasets.commands.export import ExportDatasetsCommand +from superset.queries.saved_queries.commands.export import ExportSavedQueriesCommand +from superset.utils.dict_import_export import EXPORT_VERSION + +METADATA_FILE_NAME = "metadata.yaml" + + +class ExportAssetsCommand(BaseCommand): + """ + Command that exports all databases, datasets, charts, dashboards and saved queries. + """ + + def run(self) -> Iterator[Tuple[str, str]]: + + metadata = { + "version": EXPORT_VERSION, + "type": "assets", + "timestamp": datetime.now(tz=timezone.utc).isoformat(), + } + yield METADATA_FILE_NAME, yaml.safe_dump(metadata, sort_keys=False) + seen = {METADATA_FILE_NAME} + + commands = [ + ExportDatabasesCommand, + ExportDatasetsCommand, + ExportChartsCommand, + ExportDashboardsCommand, + ExportSavedQueriesCommand, + ] + for command in commands: + ids = [model.id for model in command.dao.find_all()] + for file_name, file_content in command(ids, export_related=False).run(): + if file_name not in seen: + yield file_name, file_content + seen.add(file_name) + + def validate(self) -> None: + pass diff --git a/superset/commands/importers/v1/assets.py b/superset/commands/importers/v1/assets.py new file mode 100644 index 0000000000000..9f945c560af5f --- /dev/null +++ b/superset/commands/importers/v1/assets.py @@ -0,0 +1,164 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from typing import Any, Dict, List, Optional, Tuple + +from marshmallow import Schema +from marshmallow.exceptions import ValidationError +from sqlalchemy.orm import Session +from sqlalchemy.sql import select + +from superset import db +from superset.charts.commands.importers.v1.utils import import_chart +from superset.charts.schemas import ImportV1ChartSchema +from superset.commands.base import BaseCommand +from superset.commands.exceptions import CommandInvalidError, ImportFailedError +from superset.commands.importers.v1.utils import ( + load_configs, + load_metadata, + validate_metadata_type, +) +from superset.dashboards.commands.importers.v1.utils import ( + find_chart_uuids, + import_dashboard, + update_id_refs, +) +from superset.dashboards.schemas import ImportV1DashboardSchema +from superset.databases.commands.importers.v1.utils import import_database +from superset.databases.schemas import ImportV1DatabaseSchema +from superset.datasets.commands.importers.v1.utils import import_dataset +from superset.datasets.schemas import ImportV1DatasetSchema +from superset.models.dashboard import dashboard_slices +from superset.queries.saved_queries.commands.importers.v1.utils import ( + import_saved_query, +) +from superset.queries.saved_queries.schemas import ImportV1SavedQuerySchema + + +class ImportAssetsCommand(BaseCommand): + """ + Command for importing databases, datasets, charts, dashboards and saved queries. + + This command is used for managing Superset assets externally under source control, + and will overwrite everything. + """ + + schemas: Dict[str, Schema] = { + "charts/": ImportV1ChartSchema(), + "dashboards/": ImportV1DashboardSchema(), + "datasets/": ImportV1DatasetSchema(), + "databases/": ImportV1DatabaseSchema(), + "queries/": ImportV1SavedQuerySchema(), + } + + # pylint: disable=unused-argument + def __init__(self, contents: Dict[str, str], *args: Any, **kwargs: Any): + self.contents = contents + self.passwords: Dict[str, str] = kwargs.get("passwords") or {} + self._configs: Dict[str, Any] = {} + + # pylint: disable=too-many-locals + @staticmethod + def _import(session: Session, configs: Dict[str, Any]) -> None: + # import databases first + database_ids: Dict[str, int] = {} + for file_name, config in configs.items(): + if file_name.startswith("databases/"): + database = import_database(session, config, overwrite=True) + database_ids[str(database.uuid)] = database.id + + # import saved queries + for file_name, config in configs.items(): + if file_name.startswith("queries/"): + config["db_id"] = database_ids[config["database_uuid"]] + import_saved_query(session, config, overwrite=True) + + # import datasets + dataset_info: Dict[str, Dict[str, Any]] = {} + for file_name, config in configs.items(): + if file_name.startswith("datasets/"): + config["database_id"] = database_ids[config["database_uuid"]] + dataset = import_dataset(session, config, overwrite=True) + dataset_info[str(dataset.uuid)] = { + "datasource_id": dataset.id, + "datasource_type": dataset.datasource_type, + "datasource_name": dataset.table_name, + } + + # import charts + chart_ids: Dict[str, int] = {} + for file_name, config in configs.items(): + if file_name.startswith("charts/"): + config.update(dataset_info[config["dataset_uuid"]]) + chart = import_chart(session, config, overwrite=True) + chart_ids[str(chart.uuid)] = chart.id + + # store the existing relationship between dashboards and charts + existing_relationships = session.execute( + select([dashboard_slices.c.dashboard_id, dashboard_slices.c.slice_id]) + ).fetchall() + + # import dashboards + dashboard_chart_ids: List[Tuple[int, int]] = [] + for file_name, config in configs.items(): + if file_name.startswith("dashboards/"): + config = update_id_refs(config, chart_ids, dataset_info) + dashboard = import_dashboard(session, config, overwrite=True) + for uuid in find_chart_uuids(config["position"]): + if uuid not in chart_ids: + break + chart_id = chart_ids[uuid] + if (dashboard.id, chart_id) not in existing_relationships: + dashboard_chart_ids.append((dashboard.id, chart_id)) + + # set ref in the dashboard_slices table + values = [ + {"dashboard_id": dashboard_id, "slice_id": chart_id} + for (dashboard_id, chart_id) in dashboard_chart_ids + ] + # pylint: disable=no-value-for-parameter # sqlalchemy/issues/4656 + session.execute(dashboard_slices.insert(), values) + + def run(self) -> None: + self.validate() + + # rollback to prevent partial imports + try: + self._import(db.session, self._configs) + db.session.commit() + except Exception as ex: + db.session.rollback() + raise ImportFailedError() from ex + + def validate(self) -> None: + exceptions: List[ValidationError] = [] + + # verify that the metadata file is present and valid + try: + metadata: Optional[Dict[str, str]] = load_metadata(self.contents) + except ValidationError as exc: + exceptions.append(exc) + metadata = None + validate_metadata_type(metadata, "assets", exceptions) + + self._configs = load_configs( + self.contents, self.schemas, self.passwords, exceptions + ) + + if exceptions: + exception = CommandInvalidError("Error importing assets") + exception.add_list(exceptions) + raise exception diff --git a/tests/integration_tests/commands_test.py b/tests/integration_tests/commands_test.py index 1adf5bd646288..5ff18b02a93e4 100644 --- a/tests/integration_tests/commands_test.py +++ b/tests/integration_tests/commands_test.py @@ -14,9 +14,31 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import copy +import json +from unittest.mock import patch + +import yaml + +from superset import db, security_manager from superset.commands.exceptions import CommandInvalidError +from superset.commands.importers.v1.assets import ImportAssetsCommand from superset.commands.importers.v1.utils import is_valid_config +from superset.models.dashboard import Dashboard +from superset.models.slice import Slice from tests.integration_tests.base_tests import SupersetTestCase +from tests.integration_tests.fixtures.importexport import ( + chart_config, + dashboard_config, + database_config, + dataset_config, +) + +metadata_config = { + "version": "1.0.0", + "type": "assets", + "timestamp": "2020-11-04T21:27:44.423819+00:00", +} class TestCommandsExceptions(SupersetTestCase): @@ -33,3 +55,148 @@ def test_is_valid_config(self): assert not is_valid_config( "__MACOSX/chart_export_20210111T145253/databases/._examples.yaml" ) + + +class TestImportAssetsCommand(SupersetTestCase): + @patch("superset.dashboards.commands.importers.v1.utils.g") + def test_import_assets(self, mock_g): + """Test that we can import multiple assets""" + mock_g.user = security_manager.find_user("admin") + contents = { + "metadata.yaml": yaml.safe_dump(metadata_config), + "databases/imported_database.yaml": yaml.safe_dump(database_config), + "datasets/imported_dataset.yaml": yaml.safe_dump(dataset_config), + "charts/imported_chart.yaml": yaml.safe_dump(chart_config), + "dashboards/imported_dashboard.yaml": yaml.safe_dump(dashboard_config), + } + command = ImportAssetsCommand(contents) + command.run() + + dashboard = ( + db.session.query(Dashboard).filter_by(uuid=dashboard_config["uuid"]).one() + ) + + assert len(dashboard.slices) == 1 + chart = dashboard.slices[0] + assert str(chart.uuid) == chart_config["uuid"] + new_chart_id = chart.id + + assert dashboard.dashboard_title == "Test dash" + assert dashboard.description is None + assert dashboard.css == "" + assert dashboard.slug is None + assert json.loads(dashboard.position_json) == { + "CHART-SVAlICPOSJ": { + "children": [], + "id": "CHART-SVAlICPOSJ", + "meta": { + "chartId": new_chart_id, + "height": 50, + "sliceName": "Number of California Births", + "uuid": "0c23747a-6528-4629-97bf-e4b78d3b9df1", + "width": 4, + }, + "parents": ["ROOT_ID", "GRID_ID", "ROW-dP_CHaK2q"], + "type": "CHART", + }, + "DASHBOARD_VERSION_KEY": "v2", + "GRID_ID": { + "children": ["ROW-dP_CHaK2q"], + "id": "GRID_ID", + "parents": ["ROOT_ID"], + "type": "GRID", + }, + "HEADER_ID": { + "id": "HEADER_ID", + "meta": {"text": "Test dash"}, + "type": "HEADER", + }, + "ROOT_ID": {"children": ["GRID_ID"], "id": "ROOT_ID", "type": "ROOT"}, + "ROW-dP_CHaK2q": { + "children": ["CHART-SVAlICPOSJ"], + "id": "ROW-dP_CHaK2q", + "meta": {"0": "ROOT_ID", "background": "BACKGROUND_TRANSPARENT"}, + "parents": ["ROOT_ID", "GRID_ID"], + "type": "ROW", + }, + } + assert json.loads(dashboard.json_metadata) == { + "color_scheme": None, + "default_filters": "{}", + "expanded_slices": {str(new_chart_id): True}, + "filter_scopes": { + str(new_chart_id): { + "region": {"scope": ["ROOT_ID"], "immune": [new_chart_id]} + }, + }, + "import_time": 1604342885, + "refresh_frequency": 0, + "remote_id": 7, + "timed_refresh_immune_slices": [new_chart_id], + } + + dataset = chart.table + assert str(dataset.uuid) == dataset_config["uuid"] + + database = dataset.database + assert str(database.uuid) == database_config["uuid"] + + assert dashboard.owners == [mock_g.user] + + dashboard.owners = [] + chart.owners = [] + dataset.owners = [] + database.owners = [] + db.session.delete(dashboard) + db.session.delete(chart) + db.session.delete(dataset) + db.session.delete(database) + db.session.commit() + + @patch("superset.dashboards.commands.importers.v1.utils.g") + def test_import_v1_dashboard_overwrite(self, mock_g): + """Test that assets can be overwritten""" + mock_g.user = security_manager.find_user("admin") + + contents = { + "metadata.yaml": yaml.safe_dump(metadata_config), + "databases/imported_database.yaml": yaml.safe_dump(database_config), + "datasets/imported_dataset.yaml": yaml.safe_dump(dataset_config), + "charts/imported_chart.yaml": yaml.safe_dump(chart_config), + "dashboards/imported_dashboard.yaml": yaml.safe_dump(dashboard_config), + } + command = ImportAssetsCommand(contents) + command.run() + chart = db.session.query(Slice).filter_by(uuid=chart_config["uuid"]).one() + assert chart.cache_timeout is None + + modified_chart_config = copy.deepcopy(chart_config) + modified_chart_config["cache_timeout"] = 3600 + contents = { + "metadata.yaml": yaml.safe_dump(metadata_config), + "databases/imported_database.yaml": yaml.safe_dump(database_config), + "datasets/imported_dataset.yaml": yaml.safe_dump(dataset_config), + "charts/imported_chart.yaml": yaml.safe_dump(modified_chart_config), + "dashboards/imported_dashboard.yaml": yaml.safe_dump(dashboard_config), + } + command = ImportAssetsCommand(contents) + command.run() + chart = db.session.query(Slice).filter_by(uuid=chart_config["uuid"]).one() + assert chart.cache_timeout == 3600 + + dashboard = ( + db.session.query(Dashboard).filter_by(uuid=dashboard_config["uuid"]).one() + ) + chart = dashboard.slices[0] + dataset = chart.table + database = dataset.database + dashboard.owners = [] + + chart.owners = [] + dataset.owners = [] + database.owners = [] + db.session.delete(dashboard) + db.session.delete(chart) + db.session.delete(dataset) + db.session.delete(database) + db.session.commit() diff --git a/tests/unit_tests/commands/__init__.py b/tests/unit_tests/commands/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/tests/unit_tests/commands/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/unit_tests/commands/export_test.py b/tests/unit_tests/commands/export_test.py new file mode 100644 index 0000000000000..91aebf1b684eb --- /dev/null +++ b/tests/unit_tests/commands/export_test.py @@ -0,0 +1,94 @@ +# 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. +# pylint: disable=invalid-name, unused-argument, import-outside-toplevel + +from freezegun import freeze_time +from pytest_mock import MockFixture + + +def test_export_assets_command(mocker: MockFixture, app_context: None) -> None: + """ + Test that all assets are exported correctly. + """ + from superset.commands.export.assets import ExportAssetsCommand + + ExportDatabasesCommand = mocker.patch( + "superset.commands.export.assets.ExportDatabasesCommand" + ) + ExportDatabasesCommand.return_value.run.return_value = [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: Database\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("databases/example.yaml", ""), + ] + ExportDatasetsCommand = mocker.patch( + "superset.commands.export.assets.ExportDatasetsCommand" + ) + ExportDatasetsCommand.return_value.run.return_value = [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: Dataset\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("datasets/example/dataset.yaml", ""), + ] + ExportChartsCommand = mocker.patch( + "superset.commands.export.assets.ExportChartsCommand" + ) + ExportChartsCommand.return_value.run.return_value = [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: Slice\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("charts/pie.yaml", ""), + ] + ExportDashboardsCommand = mocker.patch( + "superset.commands.export.assets.ExportDashboardsCommand" + ) + ExportDashboardsCommand.return_value.run.return_value = [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: Dashboard\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("dashboards/sales.yaml", ""), + ] + ExportSavedQueriesCommand = mocker.patch( + "superset.commands.export.assets.ExportSavedQueriesCommand" + ) + ExportSavedQueriesCommand.return_value.run.return_value = [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: SavedQuery\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("queries/example/metric.yaml", ""), + ] + + with freeze_time("2022-01-01T00:00:00Z"): + command = ExportAssetsCommand() + output = list(command.run()) + + assert output == [ + ( + "metadata.yaml", + "version: 1.0.0\ntype: assets\ntimestamp: '2022-01-01T00:00:00+00:00'\n", + ), + ("databases/example.yaml", ""), + ("datasets/example/dataset.yaml", ""), + ("charts/pie.yaml", ""), + ("dashboards/sales.yaml", ""), + ("queries/example/metric.yaml", ""), + ] From aa5c80bda6856295368c937307bcec75bba957cf Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Thu, 17 Mar 2022 17:01:18 -0400 Subject: [PATCH 10/29] fix(sql lab): deleting the last saved query or the last executed from history (#19225) * fix: fix issue when deleting the last saved query or the last executed query * merge migration --- ...14_add_on_saved_query_delete_tab_state_.py | 66 +++++++++++++++++++ superset/models/sql_lab.py | 4 +- superset/views/sql_lab.py | 24 +++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py diff --git a/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py b/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py new file mode 100644 index 0000000000000..220370f828049 --- /dev/null +++ b/superset/migrations/versions/58df9d617f14_add_on_saved_query_delete_tab_state_.py @@ -0,0 +1,66 @@ +# 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. +"""add_on_saved_query_delete_tab_state_null_constraint" + +Revision ID: 58df9d617f14 +Revises: 6766938c6065 +Create Date: 2022-03-16 23:24:40.278937 + +""" + +# revision identifiers, used by Alembic. +revision = "58df9d617f14" +down_revision = "6766938c6065" + +import sqlalchemy as sa +from alembic import op + +from superset.utils.core import generic_find_fk_constraint_name + + +def upgrade(): + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + with op.batch_alter_table("tab_state") as batch_op: + batch_op.drop_constraint( + generic_find_fk_constraint_name("tab_state", {"id"}, "saved_query", insp), + type_="foreignkey", + ) + + batch_op.create_foreign_key( + "saved_query_id", + "saved_query", + ["saved_query_id"], + ["id"], + ondelete="SET NULL", + ) + + +def downgrade(): + bind = op.get_bind() + insp = sa.engine.reflection.Inspector.from_engine(bind) + + with op.batch_alter_table("tab_state") as batch_op: + batch_op.drop_constraint( + generic_find_fk_constraint_name("tab_state", {"id"}, "saved_query", insp), + type_="foreignkey", + ) + + batch_op.create_foreign_key( + "saved_query_id", "saved_query", ["saved_query_id"], ["id"], + ) diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index d2e9b3fefb018..6a3b4ad8bfd7c 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -291,7 +291,9 @@ class TabState(Model, AuditMixinNullable, ExtraJSONMixin): hide_left_bar = Column(Boolean, default=False) # any saved queries that are associated with the Tab State - saved_query_id = Column(Integer, ForeignKey("saved_query.id"), nullable=True) + saved_query_id = Column( + Integer, ForeignKey("saved_query.id", ondelete="SET NULL"), nullable=True + ) saved_query = relationship("SavedQuery", foreign_keys=[saved_query_id]) def to_dict(self) -> Dict[str, Any]: diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index 5ec525b9cac73..49336a84a18d6 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -20,6 +20,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_appbuilder.security.decorators import has_access, has_access_api from flask_babel import lazy_gettext as _ +from sqlalchemy import and_ from superset import db, is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod @@ -228,6 +229,29 @@ def migrate_query( # pylint: disable=no-self-use def delete_query( # pylint: disable=no-self-use self, tab_state_id: int, client_id: str ) -> FlaskResponse: + # Before deleting the query, ensure it's not tied to any + # active tab as the last query. If so, replace the query + # with the latest one created in that tab + tab_state_query = db.session.query(TabState).filter_by( + id=tab_state_id, latest_query_id=client_id + ) + if tab_state_query.count(): + query = ( + db.session.query(Query) + .filter( + and_( + Query.client_id != client_id, + Query.user_id == g.user.get_id(), + Query.sql_editor_id == str(tab_state_id), + ), + ) + .order_by(Query.id.desc()) + .first() + ) + tab_state_query.update( + {"latest_query_id": query.client_id if query else None} + ) + db.session.query(Query).filter_by( client_id=client_id, user_id=g.user.get_id(), From f6291545fb8140466315c4af8c5aec66b47d2b76 Mon Sep 17 00:00:00 2001 From: Jesse Yang Date: Thu, 17 Mar 2022 21:51:47 -0700 Subject: [PATCH 11/29] chore: turn on SQLLAB_BACKEND_PERSISTENCE by default (#19107) * chore: turn on SQLLAB_BACKEND_PERSISTENCE by default * Rewrite SQL tabs test to make it more rerunnable --- .github/workflows/bashlib.sh | 14 ++-- UPDATING.md | 4 +- .../cypress/integration/sqllab/tabs.test.js | 65 ------------------- .../cypress/integration/sqllab/tabs.test.ts | 60 +++++++++++++++++ .../components/TabbedSqlEditors/index.jsx | 4 +- .../src/components/Checkbox/Checkbox.tsx | 4 +- .../src/components/Dropdown/index.tsx | 2 +- superset/config.py | 2 +- ...test_config_sqllab_backend_persist_off.py} | 2 +- 9 files changed, 74 insertions(+), 83 deletions(-) delete mode 100644 superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js create mode 100644 superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts rename tests/integration_tests/{superset_test_config_sqllab_backend_persist.py => superset_test_config_sqllab_backend_persist_off.py} (94%) diff --git a/.github/workflows/bashlib.sh b/.github/workflows/bashlib.sh index 0f9a8fd10e01b..32e89be43174d 100644 --- a/.github/workflows/bashlib.sh +++ b/.github/workflows/bashlib.sh @@ -38,10 +38,10 @@ default-setup-command() { } apt-get-install() { - say "::group::apt-get install dependencies" - sudo apt-get update && sudo apt-get install --yes \ - libsasl2-dev - say "::endgroup::" + say "::group::apt-get install dependencies" + sudo apt-get update && sudo apt-get install --yes \ + libsasl2-dev + say "::endgroup::" } pip-upgrade() { @@ -161,7 +161,7 @@ cypress-run() { if [[ -z $CYPRESS_KEY ]]; then $cypress --spec "cypress/integration/$page" --browser "$browser" else - export CYPRESS_RECORD_KEY=`echo $CYPRESS_KEY | base64 --decode` + export CYPRESS_RECORD_KEY=$(echo $CYPRESS_KEY | base64 --decode) # additional flags for Cypress dashboard recording $cypress --spec "cypress/integration/$page" --browser "$browser" \ --record --group "$group" --tag "${GITHUB_REPOSITORY},${GITHUB_EVENT_NAME}" \ @@ -190,8 +190,8 @@ cypress-run-all() { cat "$flasklog" say "::endgroup::" - # Rerun SQL Lab tests with backend persist enabled - export SUPERSET_CONFIG=tests.integration_tests.superset_test_config_sqllab_backend_persist + # Rerun SQL Lab tests with backend persist disabled + export SUPERSET_CONFIG=tests.integration_tests.superset_test_config_sqllab_backend_persist_off # Restart Flask with new configs kill $flaskProcessId diff --git a/UPDATING.md b/UPDATING.md index 8d17dab043d87..07193a462d3be 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -35,10 +35,10 @@ assists people when migrating to a new version. - [17984](/~https://github.com/apache/superset/pull/17984): Default Flask SECRET_KEY has changed for security reasons. You should always override with your own secret. Set `PREVIOUS_SECRET_KEY` (ex: PREVIOUS_SECRET_KEY = "\2\1thisismyscretkey\1\2\\e\\y\\y\\h") with your previous key and use `superset re-encrypt-secrets` to rotate you current secrets - [15254](/~https://github.com/apache/superset/pull/15254): Previously `QUERY_COST_FORMATTERS_BY_ENGINE`, `SQL_VALIDATORS_BY_ENGINE` and `SCHEDULED_QUERIES` were expected to be defined in the feature flag dictionary in the `config.py` file. These should now be defined as a top-level config, with the feature flag dictionary being reserved for boolean only values. - [17539](/~https://github.com/apache/superset/pull/17539): all Superset CLI commands (init, load_examples and etc) require setting the FLASK_APP environment variable (which is set by default when `.flaskenv` is loaded) -- [18970](/~https://github.com/apache/superset/pull/18970): Changes feature -flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in config.py to True, thus disabling the feature from being shown in the client. +- [18970](/~https://github.com/apache/superset/pull/18970): Changes feature flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in config.py to True, thus disabling the feature from being shown in the client. - [19017](/~https://github.com/apache/superset/pull/19017): Removes Python 3.7 support. - [19142](/~https://github.com/apache/superset/pull/19142): Changes feature flag for versioned export(VERSIONED_EXPORT) to be true. +- [19107](/~https://github.com/apache/superset/pull/19107): Feature flag `SQLLAB_BACKEND_PERSISTENCE` is now on by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`. ### Potential Downtime diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js deleted file mode 100644 index 24dd074992b02..0000000000000 --- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * 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. - */ -describe('SqlLab query tabs', () => { - beforeEach(() => { - cy.login(); - cy.visit('/superset/sqllab'); - }); - - it('allows you to create a tab', () => { - cy.get('[data-test="sql-editor-tabs"]').then(tabList => { - const initialTabCount = tabList.length; - // add tab - cy.get('[data-test="add-tab-icon"]').first().click(); - // wait until we find the new tab - cy.get('[data-test="sql-editor-tabs"]') - .children() - .eq(0) - .contains(`Untitled Query ${initialTabCount}`); - cy.get('[data-test="sql-editor-tabs"]') - .children() - .eq(0) - .contains(`Untitled Query ${initialTabCount + 1}`); - }); - }); - - it('allows you to close a tab', () => { - cy.get('[data-test="sql-editor-tabs"]') - .children() - .then(tabListA => { - const initialTabCount = tabListA.length; - - // open the tab dropdown to remove - cy.get('[data-test="dropdown-toggle-button"]') - .children() - .first() - .click({ - force: true, - }); - - // first item is close - cy.get('[data-test="close-tab-menu-option"]').click(); - - cy.get('[data-test="sql-editor-tabs"]').should( - 'have.length', - initialTabCount - 1, - ); - }); - }); -}); diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts new file mode 100644 index 0000000000000..0e85664cb785a --- /dev/null +++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts @@ -0,0 +1,60 @@ +/** + * 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. + */ +describe('SqlLab query tabs', () => { + beforeEach(() => { + cy.login(); + cy.visit('/superset/sqllab'); + }); + + it('allows you to create and close a tab', () => { + const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]'; + const tabSelector = `${tablistSelector} [role="tab"]`; + cy.get(tabSelector).then(tabs => { + const initialTabCount = tabs.length; + const initialUntitledCount = Math.max( + 0, + ...tabs + .map((i, tabItem) => + Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]), + ) + .toArray(), + ); + + // add two new tabs + cy.get('[data-test="add-tab-icon"]:visible:last').click(); + cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`); + cy.get(tabSelector).should('have.length', initialTabCount + 1); + + cy.get('[data-test="add-tab-icon"]:visible:last').click(); + cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 2}`); + cy.get(tabSelector).should('have.length', initialTabCount + 2); + + // close the tabs + cy.get(`${tabSelector}:last [data-test="dropdown-trigger"]`).click({ + force: true, + }); + cy.get('[data-test="close-tab-menu-option"]').click(); + cy.get(tabSelector).should('have.length', initialTabCount + 1); + cy.contains('[role="tab"]', `Untitled Query ${initialUntitledCount + 1}`); + + cy.get(`${tablistSelector} [aria-label="remove"]:last`).click(); + cy.get(tabSelector).should('have.length', initialTabCount); + }); + }); +}); diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx index 11c6fa8b6c097..8c20a493b0876 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx @@ -386,9 +386,7 @@ class TabbedSqlEditors extends React.PureComponent { ); const tabHeader = ( -
- -
+ {qe.title} {' '}
); diff --git a/superset-frontend/src/components/Checkbox/Checkbox.tsx b/superset-frontend/src/components/Checkbox/Checkbox.tsx index a256677be6164..7162929a967f0 100644 --- a/superset-frontend/src/components/Checkbox/Checkbox.tsx +++ b/superset-frontend/src/components/Checkbox/Checkbox.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { styled } from '@superset-ui/core'; import { CheckboxChecked, CheckboxUnchecked } from 'src/components/Checkbox'; -interface CheckboxProps { +export interface CheckboxProps { checked: boolean; onChange: (val?: boolean) => void; style?: React.CSSProperties; @@ -49,5 +49,3 @@ export default function Checkbox({ checked, onChange, style }: CheckboxProps) { ); } - -export type { CheckboxProps }; diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx index fdfa9f945c6c2..e5d5f9f8526c5 100644 --- a/superset-frontend/src/components/Dropdown/index.tsx +++ b/superset-frontend/src/components/Dropdown/index.tsx @@ -72,7 +72,7 @@ export interface DropdownProps { export const Dropdown = ({ overlay, ...rest }: DropdownProps) => ( - + diff --git a/superset/config.py b/superset/config.py index efb5178679d4a..775765d08f0aa 100644 --- a/superset/config.py +++ b/superset/config.py @@ -391,7 +391,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: "REMOVE_SLICE_LEVEL_LABEL_COLORS": False, "SHARE_QUERIES_VIA_KV_STORE": False, "TAGGING_SYSTEM": False, - "SQLLAB_BACKEND_PERSISTENCE": False, + "SQLLAB_BACKEND_PERSISTENCE": True, "LISTVIEWS_DEFAULT_CARD_VIEW": False, # Enables the replacement React views for all the FAB views (list, edit, show) with # designs introduced in /~https://github.com/apache/superset/issues/8976 diff --git a/tests/integration_tests/superset_test_config_sqllab_backend_persist.py b/tests/integration_tests/superset_test_config_sqllab_backend_persist_off.py similarity index 94% rename from tests/integration_tests/superset_test_config_sqllab_backend_persist.py rename to tests/integration_tests/superset_test_config_sqllab_backend_persist_off.py index 41a720deb6953..9f6dd2ead1fa2 100644 --- a/tests/integration_tests/superset_test_config_sqllab_backend_persist.py +++ b/tests/integration_tests/superset_test_config_sqllab_backend_persist_off.py @@ -21,4 +21,4 @@ from .superset_test_config import * -FEATURE_FLAGS = {"SQLLAB_BACKEND_PERSISTENCE": True} +FEATURE_FLAGS = {"SQLLAB_BACKEND_PERSISTENCE": False} From b5e9fad11a2146c471c7f27d88cc425d928f94dd Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Fri, 18 Mar 2022 14:05:30 +0800 Subject: [PATCH 12/29] fix: adhoc column in legacy chart (#19234) --- superset/connectors/base/models.py | 9 ++++--- tests/integration_tests/model_tests.py | 34 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index 5cf2a8719bf95..939a1fc1c7c27 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -339,11 +339,14 @@ def data_for_slices( # pylint: disable=too-many-locals or [] ) else: - column_names.update( - column + _columns = [ + utils.get_column_name(column) + if utils.is_adhoc_column(column) + else column for column_param in COLUMN_FORM_DATA_PARAMS for column in utils.get_iterable(form_data.get(column_param) or []) - ) + ] + column_names.update(_columns) filtered_metrics = [ metric diff --git a/tests/integration_tests/model_tests.py b/tests/integration_tests/model_tests.py index 6371a06123ae8..5ffa65e583ead 100644 --- a/tests/integration_tests/model_tests.py +++ b/tests/integration_tests/model_tests.py @@ -15,10 +15,12 @@ # specific language governing permissions and limitations # under the License. # isort:skip_file +import json import textwrap import unittest from unittest import mock +from superset.connectors.sqla.models import SqlaTable from superset.exceptions import SupersetException from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, @@ -578,6 +580,38 @@ def test_data_for_slices_with_query_context(self): "state", } + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_data_for_slices_with_adhoc_column(self): + # should perform sqla.model.BaseDatasource.data_for_slices() with adhoc + # column and legacy chart + tbl = self.get_table(name="birth_names") + dashboard = self.get_dash_by_slug("births") + slc = Slice( + slice_name="slice with adhoc column", + datasource_type="table", + viz_type="table", + params=json.dumps( + { + "adhoc_filters": [], + "granularity_sqla": "ds", + "groupby": [ + "name", + {"label": "adhoc_column", "sqlExpression": "name"}, + ], + "metrics": ["sum__num"], + "time_range": "No filter", + "viz_type": "table", + } + ), + datasource_id=tbl.id, + ) + dashboard.slices.append(slc) + datasource_info = slc.datasource.data_for_slices([slc]) + assert "database" in datasource_info + + # clean up and auto commit + metadata_db.session.delete(slc) + def test_literal_dttm_type_factory(): orig_type = DateTime() From 10eb6c77a45193816707fe4d71f65454effe0832 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Fri, 18 Mar 2022 21:28:47 +0800 Subject: [PATCH 13/29] chore: use order_desc shared control consistently (#19172) --- .../src/shared-controls/index.tsx | 6 +++++- .../src/controlPanel.ts | 12 +----------- .../src/controlPanel.ts | 12 +----------- .../src/controlPanel.ts | 14 +------------- .../src/controlPanel.tsx | 12 +----------- .../src/controlPanel.ts | 14 +------------- .../legacy-plugin-chart-rose/src/controlPanel.tsx | 12 +----------- .../src/controlPanel.ts | 14 +------------- .../src/DistBar/controlPanel.ts | 14 +------------- .../src/Timeseries/Area/controlPanel.tsx | 14 +------------- .../src/Timeseries/Regular/Bar/controlPanel.tsx | 14 +------------- .../Timeseries/Regular/Scatter/controlPanel.tsx | 14 +------------- .../src/Timeseries/Regular/controlPanel.tsx | 14 +------------- .../src/Timeseries/Step/controlPanel.tsx | 14 +------------- .../src/Timeseries/controlPanel.tsx | 14 +------------- .../src/explore/controlPanels/sections.tsx | 12 +----------- 16 files changed, 20 insertions(+), 186 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index a5de8eff91f71..0419c55ed3184 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -43,6 +43,7 @@ import { SequentialScheme, legacyValidateInteger, validateNonEmpty, + JsonArray, } from '@superset-ui/core'; import { @@ -345,7 +346,10 @@ const order_desc: SharedControlConfig<'CheckboxControl'> = { default: true, description: t('Whether to sort descending or ascending'), visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), + Boolean( + controls?.timeseries_limit_metric.value && + (controls?.timeseries_limit_metric.value as JsonArray).length, + ), }; const limit: SharedControlConfig<'SelectControl'> = { diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts index c87bcc0fef68b..ca18b712b82f9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts @@ -34,18 +34,8 @@ const config: ControlPanelConfig = { ['adhoc_filters'], ['groupby'], ['limit', 'timeseries_limit_metric'], + ['order_desc'], [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, { name: 'contribution', config: { diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts index a8f8d7e7be3ce..ea87c024e8533 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts @@ -37,18 +37,8 @@ const config: ControlPanelConfig = { }, ], ['limit', 'timeseries_limit_metric'], + ['order_desc'], [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, { name: 'contribution', config: { diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts index 7023f019ee28c..66fd5dcca834c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts @@ -32,19 +32,7 @@ const config: ControlPanelConfig = { ['adhoc_filters'], ['limit', 'row_limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ], }, { diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx b/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx index 03c18b601612c..c742e6d1335cb 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx @@ -40,18 +40,8 @@ const config: ControlPanelConfig = { ['adhoc_filters'], ['groupby'], ['limit', 'timeseries_limit_metric'], + ['order_desc'], [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, { name: 'contribution', config: { diff --git a/superset-frontend/plugins/legacy-plugin-chart-pivot-table/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-pivot-table/src/controlPanel.ts index b1dd768e1d4b4..e4c0b477c4827 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-pivot-table/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-pivot-table/src/controlPanel.ts @@ -39,19 +39,7 @@ const config: ControlPanelConfig = { ['columns'], ['row_limit', null], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ], }, { diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx b/superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx index a400095185fc1..fd04117e6217c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx @@ -38,18 +38,8 @@ const config: ControlPanelConfig = { ['adhoc_filters'], ['groupby'], ['limit', 'timeseries_limit_metric'], + ['order_desc'], [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, { name: 'contribution', config: { diff --git a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/controlPanel.ts index 666bbd41ddd68..bc400d2f4b0a9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/controlPanel.ts @@ -36,19 +36,7 @@ const config: ControlPanelConfig = { ['groupby'], ['row_limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ], }, { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts index ed8b31b9e9809..3df9e00057b1a 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts @@ -51,19 +51,7 @@ const config: ControlPanelConfig = { ['columns'], ['row_limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], [ { name: 'contribution', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index bae735b692bad..87503166b7977 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -83,19 +83,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 08d09a0147272..bd40eeebe0e75 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -80,19 +80,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index 21110c82cfefa..4cdf16c8395a2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -60,19 +60,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx index 701c3a57ff561..d2f3acce9e08f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx @@ -77,19 +77,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 8b40330730af5..1416a7db4686c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -83,19 +83,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx index f7fd56b7fa577..1f1e22b49b3a5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx @@ -84,19 +84,7 @@ const config: ControlPanelConfig = { emitFilterControl, ['limit'], ['timeseries_limit_metric'], - [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, - ], + ['order_desc'], ['row_limit'], ], }, diff --git a/superset-frontend/src/explore/controlPanels/sections.tsx b/superset-frontend/src/explore/controlPanels/sections.tsx index c1c68ad2f70b0..a1c786a73c15d 100644 --- a/superset-frontend/src/explore/controlPanels/sections.tsx +++ b/superset-frontend/src/explore/controlPanels/sections.tsx @@ -108,18 +108,8 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [ ['adhoc_filters'], ['groupby'], ['limit', 'timeseries_limit_metric'], + ['order_desc'], [ - { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - Boolean(controls?.timeseries_limit_metric.value), - }, - }, { name: 'contribution', config: { From 2a89da2ef1e9653959bfb5b46ccb396855141bee Mon Sep 17 00:00:00 2001 From: Venu Vardhan Reddy Tekula Date: Fri, 18 Mar 2022 19:12:14 +0530 Subject: [PATCH 14/29] docs: fix broken links in the documentation (#19235) --- CONTRIBUTING.md | 6 +++--- docs/docs/contributing/hooks-and-linting.mdx | 4 ++-- docs/docs/databases/mysql.mdx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a5e42b3aa4b1..910572e3db7d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -198,7 +198,7 @@ Finally, never submit a PR that will put master branch in broken state. If the P #### Authoring - Fill in all sections of the PR template. -- Title the PR with one of the following semantic prefixes (inspired by [Karma](http://karma-runner.github.io/0.10/dev/git-commit-msg.html])): +- Title the PR with one of the following semantic prefixes (inspired by [Karma](http://karma-runner.github.io/0.10/dev/git-commit-msg.html)): - `feat` (new feature) - `fix` (bug fix) @@ -663,8 +663,8 @@ tox -e pylint In terms of best practices please advoid blanket disablement of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disablement should occur inline as it prevents masking issues and provides context as to why said message is disabled. -Additionally the Python code is auto-formatted using [Black](/~https://github.com/python/black) which -is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/editor_integration.html) +Additionally, the Python code is auto-formatted using [Black](/~https://github.com/python/black) which +is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/integrations/editors.html) ### TypeScript diff --git a/docs/docs/contributing/hooks-and-linting.mdx b/docs/docs/contributing/hooks-and-linting.mdx index b6d82420184c6..dc8cfef0dcebd 100644 --- a/docs/docs/contributing/hooks-and-linting.mdx +++ b/docs/docs/contributing/hooks-and-linting.mdx @@ -41,8 +41,8 @@ tox -e pylint In terms of best practices please advoid blanket disablement of Pylint messages globally (via `.pylintrc`) or top-level within the file header, albeit there being a few exceptions. Disablement should occur inline as it prevents masking issues and provides context as to why said message is disabled. -Additionally the Python code is auto-formatted using [Black](/~https://github.com/python/black) which -is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/editor_integration.html) +Additionally, the Python code is auto-formatted using [Black](/~https://github.com/python/black) which +is configured as a pre-commit hook. There are also numerous [editor integrations](https://black.readthedocs.io/en/stable/integrations/editors.html) ### TypeScript diff --git a/docs/docs/databases/mysql.mdx b/docs/docs/databases/mysql.mdx index 32bde7db732c9..e784321515b4c 100644 --- a/docs/docs/databases/mysql.mdx +++ b/docs/docs/databases/mysql.mdx @@ -7,7 +7,7 @@ version: 1 ## MySQL -The recommended connector library for MySQL is `[mysqlclient](https://pypi.org/project/mysqlclient/)`. +The recommended connector library for MySQL is [mysqlclient](https://pypi.org/project/mysqlclient/). Here's the connection string: From e7355b9610d1371d1d3fca51c17d1999ca3ecef3 Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Fri, 18 Mar 2022 10:39:26 -0400 Subject: [PATCH 15/29] =?UTF-8?q?fix(explore=20comma):=20make=20that=20the?= =?UTF-8?q?=20comma=20can=20be=20added=20by=20removing=20it=20from=20token?= =?UTF-8?q?=20separators=E2=80=A6=20(#18926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * make that the comma can be added by removing it from token separators in select component. * fix(explore comma): add the allowTokenSeperators props into Select * fix(explore comma): make to allow to customize the token separators in Select. * fix(explore comma): make to add the unit test and story book. * fix(explore comma): make to fix the lint * fix(explore comma): make to fix the spell & add tokenSeparatprs props to PickedSelectProps * Update Select.tsx * fix(explore comma): make to run lint fix --- .../src/shared-controls/index.tsx | 1 + .../src/components/Select/Select.stories.tsx | 1 + superset-frontend/src/components/Select/Select.tsx | 4 +++- .../src/explore/components/controls/SelectControl.jsx | 3 +++ .../explore/components/controls/SelectControl.test.jsx | 8 ++++++++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 0419c55ed3184..aee3717938c9e 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -448,6 +448,7 @@ const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> = default: DEFAULT_NUMBER_FORMAT, choices: D3_FORMAT_OPTIONS, description: D3_FORMAT_DOCS, + tokenSeparators: ['\n', '\t', ';'], filterOption: ({ data: option }, search) => option.label.includes(search) || option.value.includes(search), mapStateToProps: state => { diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx index ed27f6a3fbdcc..5526c2fc2ac01 100644 --- a/superset-frontend/src/components/Select/Select.stories.tsx +++ b/superset-frontend/src/components/Select/Select.stories.tsx @@ -495,6 +495,7 @@ AsyncSelect.args = { pageSize: 10, withError: false, withInitialValue: false, + tokenSeparators: ['\n', '\t', ';'], }; AsyncSelect.argTypes = { diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index e910f38ee6657..92e0ec8b3379d 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -64,6 +64,7 @@ type PickedSelectProps = Pick< | 'onDropdownVisibleChange' | 'placeholder' | 'showSearch' + | 'tokenSeparators' | 'value' >; @@ -310,6 +311,7 @@ const Select = ( placeholder = t('Select ...'), showSearch = true, sortComparator = DEFAULT_SORT_COMPARATOR, + tokenSeparators, value, ...props }: SelectProps, @@ -706,7 +708,7 @@ const Select = ( placeholder={placeholder} showSearch={shouldShowSearch} showArrow - tokenSeparators={TOKEN_SEPARATORS} + tokenSeparators={tokenSeparators || TOKEN_SEPARATORS} value={selectValue} suffixIcon={getSuffixIcon()} menuItemSelectedIcon={ diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx index f8ad181b7e136..53b7440cb4b9a 100644 --- a/superset-frontend/src/explore/components/controls/SelectControl.jsx +++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx @@ -52,6 +52,7 @@ const propTypes = { options: PropTypes.array, placeholder: PropTypes.string, filterOption: PropTypes.func, + tokenSeparators: PropTypes.arrayOf(PropTypes.string), // ControlHeader props label: PropTypes.string, @@ -177,6 +178,7 @@ export default class SelectControl extends React.PureComponent { optionRenderer, showHeader, value, + tokenSeparators, // ControlHeader props description, renderTrigger, @@ -242,6 +244,7 @@ export default class SelectControl extends React.PureComponent { placeholder, sortComparator: this.props.sortComparator, value: getValue(), + tokenSeparators, }; return ( diff --git a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx b/superset-frontend/src/explore/components/controls/SelectControl.test.jsx index 6c95f10fb8a40..1a004ee71a37e 100644 --- a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx +++ b/superset-frontend/src/explore/components/controls/SelectControl.test.jsx @@ -78,6 +78,14 @@ describe('SelectControl', () => { expect(wrapper.find(SelectComponent).prop('allowNewOptions')).toBe(false); }); + it('renders with tokenSeparators', () => { + wrapper.setProps({ tokenSeparators: ['\n', '\t', ';'] }); + expect(wrapper.find(SelectComponent)).toExist(); + expect(wrapper.find(SelectComponent).prop('tokenSeparators')).toEqual( + expect.arrayContaining([expect.any(String)]), + ); + }); + describe('empty placeholder', () => { describe('withMulti', () => { it('does not show a placeholder if there are no choices', () => { From 48a12ade8c422266e953a582e1f16d517bd941a9 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Fri, 18 Mar 2022 08:27:57 -0700 Subject: [PATCH 16/29] update changelog and updating files from 1.4.1 (#18648) --- CHANGELOG.md | 351 ++------------------------------------------ RELEASING/README.md | 2 + UPDATING.md | 12 ++ 3 files changed, 24 insertions(+), 341 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa85e8e843569..34657f0ba60b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,351 +17,20 @@ specific language governing permissions and limitations under the License. --> ## Change Log -### 1.4 +### 1.4.1 **Database Migrations** -- [#17335](/~https://github.com/apache/superset/pull/17335) feat: Certify Charts and Dashboards (@geido) -- [#17078](/~https://github.com/apache/superset/pull/17078) chore(engine): Translate fractional time grains—requires @superset-ui bump (@john-bodley) -- [#16849](/~https://github.com/apache/superset/pull/16849) chore: db migrate timeseries_limit_metric to legacy_order_by (@zhaoyongjie) -- [#14015](/~https://github.com/apache/superset/pull/14015) feat(filter-set): Add filterset resource (@ofekisr) -- [#16454](/~https://github.com/apache/superset/pull/16454) feat: add certifiedby & certification details fields to the edit dataset columns fields (@pkdotson) -- [#16549](/~https://github.com/apache/superset/pull/16549) feat(dashboard): Native filters - add type to native filter configuration (@m-ajay) -- [#16301](/~https://github.com/apache/superset/pull/16301) fix: remove mergepoint from past migration (@etr2460) **Features** -- [#17353](/~https://github.com/apache/superset/pull/17353) feat: Drill ODBC/JDBC Impersonation feature (@Z0ltrix) -- [#17006](/~https://github.com/apache/superset/pull/17006) feat: Custom filters control (@simcha90) -- [#16889](/~https://github.com/apache/superset/pull/16889) feat: upgrade docker image to py38 and add support for py39 (@villebro) -- [#16903](/~https://github.com/apache/superset/pull/16903) feat: add Firebolt DB engine spec (@apurva-sigmoid) -- [#16862](/~https://github.com/apache/superset/pull/16862) feat: add Databricks ODBC engine spec (@betodealmeida) -- [#16628](/~https://github.com/apache/superset/pull/16628) feat: Add Private Google Sheets to dynamic form (@AAfghahi) -- [#16219](/~https://github.com/apache/superset/pull/16219) feat: added extraEnvRaw variable to load values from other secrets in Helm chart (@elyzov) -- [#16795](/~https://github.com/apache/superset/pull/16795) feat: handle temporal columns in group bys (@betodealmeida) -- [#16770](/~https://github.com/apache/superset/pull/16770) feat: add support for JOIN in Druid (@betodealmeida) -- [#16533](/~https://github.com/apache/superset/pull/16533) feat: Add Cypress makefile cmds (@hughhhh) -- [#16607](/~https://github.com/apache/superset/pull/16607) feat: add resample operator in post processing (@zhaoyongjie) -- [#16683](/~https://github.com/apache/superset/pull/16683) feat: add global max row limit (@villebro) -- [#16703](/~https://github.com/apache/superset/pull/16703) feat: Helm chart: Support hostAliases (@xasx) -- [#16660](/~https://github.com/apache/superset/pull/16660) feat: add support for generic series limit (@villebro) -- [#16695](/~https://github.com/apache/superset/pull/16695) feat: show nice error page in prod (@betodealmeida) -- [#16527](/~https://github.com/apache/superset/pull/16527) feat: adding logging to validation (@AAfghahi) -- [#16680](/~https://github.com/apache/superset/pull/16680) feat(sqla): add time grain and time column to jinja params (@villebro) -- [#16618](/~https://github.com/apache/superset/pull/16618) feat: feature flag configurable custom backend (@dpgaspar) -- [#16593](/~https://github.com/apache/superset/pull/16593) feat: Tabs in column (@simcha90) -- [#16375](/~https://github.com/apache/superset/pull/16375) feat: Backend Validation for Creation Method (@AAfghahi) -- [#16535](/~https://github.com/apache/superset/pull/16535) feat: Add Aurora Data API engine spec (@betodealmeida) -- [#14449](/~https://github.com/apache/superset/pull/14449) feat: Add parquet upload (@exemplary-citizen) -- [#16234](/~https://github.com/apache/superset/pull/16234) feat: add function list to auto-complete to Clickhouse datasource (@Slach) -- [#16394](/~https://github.com/apache/superset/pull/16394) feat: Draggable and Resizable Modal (@geido) -- [#16404](/~https://github.com/apache/superset/pull/16404) feat: add activate command (@hughhhh) -- [#16386](/~https://github.com/apache/superset/pull/16386) feat: config to customize bootstrap data overrides (@suddjian) -- [#16361](/~https://github.com/apache/superset/pull/16361) feat: Add extraVolumes and extraVolumeMounts to all main containers (@cccs-tom) -- [#16327](/~https://github.com/apache/superset/pull/16327) feat: Add new dev commands to Makefile (@hughhhh) -- [#16335](/~https://github.com/apache/superset/pull/16335) feat: improve embedded data table in text reports (@betodealmeida) -- [#16318](/~https://github.com/apache/superset/pull/16318) feat(sqla): apply time grain to all temporal groupbys (@villebro) -- [#16281](/~https://github.com/apache/superset/pull/16281) feat: timezone editor (@AAfghahi) -- [#16119](/~https://github.com/apache/superset/pull/16119) feat(explore): make dnd controls clickable (@kgabryje) -- [#15149](/~https://github.com/apache/superset/pull/15149) feat(dao): admin can remove self from object owners (@villebro) -- [#16201](/~https://github.com/apache/superset/pull/16201) feat: Allow users to connect via legacy SQLA form (@hughhhh) -- [#15686](/~https://github.com/apache/superset/pull/15686) feat: import configuration from directory (@betodealmeida) -- [#16090](/~https://github.com/apache/superset/pull/16090) feat(explore): each control can define its own canDrop for dnd (@kgabryje) -- [#16136](/~https://github.com/apache/superset/pull/16136) feat: add profiling to Superset pages (@betodealmeida) **Fixes** -- [#17945](/~https://github.com/apache/superset/pull/17945) fix(dashboard): scope status of filter not update in dashboard metadata (@stephenLYZ) -- [#17349](/~https://github.com/apache/superset/pull/17349) fix(Dashboard): Check validity of control item (@geido) -- [#17842](/~https://github.com/apache/superset/pull/17842) fix(dashboard): update native filter info in metadata is not updated (@stephenLYZ) -- [#17835](/~https://github.com/apache/superset/pull/17835) fix: resolve tests for 1.4 (@eschutho) -- [#17781](/~https://github.com/apache/superset/pull/17781) fix(dashboard): commit update once (@serenajiang) -- [#17766](/~https://github.com/apache/superset/pull/17766) fix: Remove positions from json_metadata (@geido) -- [#17330](/~https://github.com/apache/superset/pull/17330) fix: import should accept old keys (@eschutho) -- [#17570](/~https://github.com/apache/superset/pull/17570) fix: Save properties after applying changes in Dashboard (@geido) -- [#17707](/~https://github.com/apache/superset/pull/17707) fix(Dashboard): Copy dashboard with duplicating charts 500 error (@geido) -- [#16041](/~https://github.com/apache/superset/pull/16041) fix: set correct schema on config import (@betodealmeida) -- [#17386](/~https://github.com/apache/superset/pull/17386) fix(sqllab): Have table name tooltip only show when name is truncated (@corbinrobb) -- [#17431](/~https://github.com/apache/superset/pull/17431) fix: use full resultType with csv download on chart in dashboard (@eschutho) -- [#17419](/~https://github.com/apache/superset/pull/17419) fix: avoid escaping bind-like params containing colons (@villebro) -- [#17311](/~https://github.com/apache/superset/pull/17311) fix: Revert "fix(native-filters): Fix update ownState" (@etr2460) -- [#17183](/~https://github.com/apache/superset/pull/17183) fix(Dashboard): Handle undefined tab when collapsing tabs (@geido) -- [#17133](/~https://github.com/apache/superset/pull/17133) fix: sql lab crash caused by invalid template (@graceguo-supercat) -- [#17123](/~https://github.com/apache/superset/pull/17123) fix(explore): remove unnecessary parameters from the explore url (@suddjian) -- [#17117](/~https://github.com/apache/superset/pull/17117) fix: undefined error when anonymous user browses dashboards or charts (@wijnanjo) -- [#17068](/~https://github.com/apache/superset/pull/17068) fix(sqllab): Hover tooltip flashes in SQL Lab (@lyndsiWilliams) -- [#17100](/~https://github.com/apache/superset/pull/17100) fix: prevent caching error pages (@etr2460) -- [#17080](/~https://github.com/apache/superset/pull/17080) fix: accept headers on import (@betodealmeida) -- [#17029](/~https://github.com/apache/superset/pull/17029) fix(other): column name in created content on profile page (@jinghua-qa) -- [#17018](/~https://github.com/apache/superset/pull/17018) fix: Exclude SUPERSET_DEFAULT from the list of available color schemes (@geido) -- [#16998](/~https://github.com/apache/superset/pull/16998) fix: ensure known dashboard id is used in save first (@pkdotson) -- [#17330](/~https://github.com/apache/superset/pull/17330) fix: import should accept old keys (@betodealmeida) -- [#17345](/~https://github.com/apache/superset/pull/17345) fix: clear 'delete' confirmation (@betodealmeida) -- [#17338](/~https://github.com/apache/superset/pull/17338) fix: add fallback and validation for report and cron timezones (@eschutho) -- [#17265](/~https://github.com/apache/superset/pull/17265) fix: Allow users to update database in Dataset Edit Modal (@hughhhh) -- [#17124](/~https://github.com/apache/superset/pull/17124) fix: update values for default timezone selector (@eschutho) -- [#17176](/~https://github.com/apache/superset/pull/17176) fix(AlertReportModal): Text Area Change (@AAfghahi) -- [#17201](/~https://github.com/apache/superset/pull/17201) fix(explore): Metrics disappearing after removing metric from dataset (@kgabryje) -- [#16994](/~https://github.com/apache/superset/pull/16994) fix: Unnecessary queries when changing filter values (@michael-s-molina) -- [#17003](/~https://github.com/apache/superset/pull/17003) fix: letter format of sort chart in dashboard edit (@jinghua-qa) -- [#16997](/~https://github.com/apache/superset/pull/16997) fix(sqllab): SqlJsonExecutionContext.query null pointer (@serenajiang) -- [#16912](/~https://github.com/apache/superset/pull/16912) fix: FilterableTable result div width (@lyndsiWilliams) -- [#16978](/~https://github.com/apache/superset/pull/16978) fix: Use production build config for cypress tests and fix webpack (@etr2460) -- [#17089](/~https://github.com/apache/superset/pull/17089) fix: Color consistency (@geido) -- [#17034](/~https://github.com/apache/superset/pull/17034) fix: show onhover menu only in edit mode (@pkdotson) -- [#17013](/~https://github.com/apache/superset/pull/17013) fix: Verify when null value should be undefined in Select (@geido) -- [#17263](/~https://github.com/apache/superset/pull/17263) fix(sqllab): Bugfix for tracking url transformation (@CodeingBoy) -- [#16976](/~https://github.com/apache/superset/pull/16976) fix(cli): fail CLI script on failed import/export (@EBoisseauSierra) -- [#17181](/~https://github.com/apache/superset/pull/17181) fix(native-filters): Fix update ownState (@simcha90) -- [#17027](/~https://github.com/apache/superset/pull/17027) fix: error alert levels again (@etr2460) -- [#17026](/~https://github.com/apache/superset/pull/17026) fix: error alerts again (@etr2460) -- [#17015](/~https://github.com/apache/superset/pull/17015) fix: error alerts js crash (@etr2460) -- [#17023](/~https://github.com/apache/superset/pull/17023) fix: Filtering db names while creating dataset is not working (@michael-s-molina) -- [#17174](/~https://github.com/apache/superset/pull/17174) fix: use typing_extension instead (@hughhhh) -- [#17167](/~https://github.com/apache/superset/pull/17167) fix(Explore): Undefined owners (@geido) -- [#17140](/~https://github.com/apache/superset/pull/17140) fix(filter-indicator): show filters handled by jinja as applied (@villebro) -- [#17111](/~https://github.com/apache/superset/pull/17111) fix: escape bind-like strings in virtual table query (@villebro) -- [#17113](/~https://github.com/apache/superset/pull/17113) fix: Bump FAB to 3.3.4 (@dpgaspar) -- [#17084](/~https://github.com/apache/superset/pull/17084) fix(dashboard): race condition between hydrating dashboard and set active tabs (@kgabryje) -- [#17063](/~https://github.com/apache/superset/pull/17063) fix: Owners selection in dataset edit UX (@hughhhh) -- [#17044](/~https://github.com/apache/superset/pull/17044) fix: clear modal state after adding dataset (@betodealmeida) -- [#17040](/~https://github.com/apache/superset/pull/17040) fix: Loading indicator of table and schema selectors (@michael-s-molina) -- [#17019](/~https://github.com/apache/superset/pull/17019) fix(gsheets): bug fix for private sheets (@AAfghahi) -- [#17007](/~https://github.com/apache/superset/pull/17007) fix(dashboard): Race condition when setting activeTabs with nested tabs (@kgabryje) -- [#16945](/~https://github.com/apache/superset/pull/16945) fix: rolling and cum operator on multiple series (@zhaoyongjie) -- [#16941](/~https://github.com/apache/superset/pull/16941) fix: check if owners are actually being updated in `PUT /datasets/` (@hughhhh) -- [#16822](/~https://github.com/apache/superset/pull/16822) fix(BigQuery): explicitly quote columns in select_star (@betodealmeida) -- [#16988](/~https://github.com/apache/superset/pull/16988) fix: When click on "View all" from favorite tab, get error (@michael-s-molina) -- [#16968](/~https://github.com/apache/superset/pull/16968) fix: Revert "fix: RBAC hide right menu (#16902)" (@eschutho) -- [#16958](/~https://github.com/apache/superset/pull/16958) fix(build): make npm linking work pt. 2 (@villebro) -- [#16930](/~https://github.com/apache/superset/pull/16930) fix: replace absolute difference with difference in compareOperator (@zhaoyongjie) -- [#16946](/~https://github.com/apache/superset/pull/16946) fix(query_object): missing series validation not raised an exception (@ofekisr) -- [#16931](/~https://github.com/apache/superset/pull/16931) fix: replace absolute difference with difference in legacy charts (@zhaoyongjie) -- [#16902](/~https://github.com/apache/superset/pull/16902) fix: RBAC hide right menu (@hughhhh) -- [#16921](/~https://github.com/apache/superset/pull/16921) fix: Native filters cyclic dependency (@michael-s-molina) -- [#16925](/~https://github.com/apache/superset/pull/16925) fix: Unable to add dataset (@michael-s-molina) -- [#16923](/~https://github.com/apache/superset/pull/16923) fix(Explore): Handle undefined operatorId (@geido) -- [#16871](/~https://github.com/apache/superset/pull/16871) fix(Explore): Clear filter value when changing columns (@geido) -- [#16906](/~https://github.com/apache/superset/pull/16906) fix: Inclusive sign in time range display (@michael-s-molina) -- [#16908](/~https://github.com/apache/superset/pull/16908) fix: Disable lazy loading for the Database selector (@michael-s-molina) -- [#16895](/~https://github.com/apache/superset/pull/16895) fix: time comparison can't guarantee the accuracy (@zhaoyongjie) -- [#16859](/~https://github.com/apache/superset/pull/16859) fix: Fix Uniqueness check before update for Sqllab Overwrites (@hughhhh) -- [#16899](/~https://github.com/apache/superset/pull/16899) fix(GSheets): Fixing DB Connections Bug (@AAfghahi) -- [#16876](/~https://github.com/apache/superset/pull/16876) fix: Removing parent filter causes incorrect state of child filter (@michael-s-molina) -- [#16896](/~https://github.com/apache/superset/pull/16896) fix(sqla): allow series limit without subquery support (@villebro) -- [#16877](/~https://github.com/apache/superset/pull/16877) fix(native-filters): Overhead when changing the filter name (@michael-s-molina) -- [#16867](/~https://github.com/apache/superset/pull/16867) fix(build): enable hot reloading of linked packages (@villebro) -- [#16851](/~https://github.com/apache/superset/pull/16851) fix(dashboard): Fill form with the latest values when undo in native filters (@geido) -- [#16854](/~https://github.com/apache/superset/pull/16854) fix(native-filters): ignore unset filter box time range (@villebro) -- [#16840](/~https://github.com/apache/superset/pull/16840) fix(gallery): Hide the bottom info section when no chart is being selected (@stephenLYZ) -- [#16828](/~https://github.com/apache/superset/pull/16828) fix(native-filters): emitted filter label format (@villebro) -- [#16831](/~https://github.com/apache/superset/pull/16831) fix(native-filters): filter indicator stale state (@villebro) -- [#16758](/~https://github.com/apache/superset/pull/16758) fix(helm): Exit init script immediately on error (@sourcecode-glitch) -- [#16837](/~https://github.com/apache/superset/pull/16837) fix(SqlLab): display tooltip when disabled (@AAfghahi) -- [#16836](/~https://github.com/apache/superset/pull/16836) fix: 500 tab title (@etr2460) -- [#16833](/~https://github.com/apache/superset/pull/16833) fix: Updates the selected values when changing the native filter type, column or default value (@michael-s-molina) -- [#16800](/~https://github.com/apache/superset/pull/16800) fix: list Db2 as supported databases (@shawnzhu) -- [#16763](/~https://github.com/apache/superset/pull/16763) fix: show Import button only if has perms (@betodealmeida) -- [#16768](/~https://github.com/apache/superset/pull/16768) fix: encode rison characters when searching (@betodealmeida) -- [#16767](/~https://github.com/apache/superset/pull/16767) fix: typo in log (@betodealmeida) -- [#16769](/~https://github.com/apache/superset/pull/16769) fix: handle CTEs with comments on is_select (@betodealmeida) -- [#16754](/~https://github.com/apache/superset/pull/16754) fix: only fetch db function when db exists in sql lab (@eschutho) -- [#16753](/~https://github.com/apache/superset/pull/16753) fix: save query should use the correct sql (@eschutho) -- [#16736](/~https://github.com/apache/superset/pull/16736) fix: update execution logs and states for alerts (@eschutho) -- [#16656](/~https://github.com/apache/superset/pull/16656) fix: set importer as owner (@betodealmeida) -- [#16674](/~https://github.com/apache/superset/pull/16674) fix: report with timeout chart (@graceguo-supercat) -- [#16706](/~https://github.com/apache/superset/pull/16706) fix: Ignores case and special keys when searching in the Select component (@michael-s-molina) -- [#16700](/~https://github.com/apache/superset/pull/16700) fix(explore): make clicked dnd filters unique (@villebro) -- [#16666](/~https://github.com/apache/superset/pull/16666) fix: Select refactoring known issues (@geido) -- [#16624](/~https://github.com/apache/superset/pull/16624) fix(dataset): create ES-View dataset raise exception #16623 (@aniaan) -- [#16696](/~https://github.com/apache/superset/pull/16696) fix: remove useless-suppression for pylint (@zhaoyongjie) -- [#16608](/~https://github.com/apache/superset/pull/16608) fix: Normalise `*.sh` File Endings (@gvee-uk) -- [#16668](/~https://github.com/apache/superset/pull/16668) fix: reset perf logger timer for soft navigation for SPA pages (@graceguo-supercat) -- [#16639](/~https://github.com/apache/superset/pull/16639) fix: Ensure alerts & reports aren't schduled when flag is off (@jfrag1) -- [#16629](/~https://github.com/apache/superset/pull/16629) fix: pybabel extract fails (@hushaoqing) -- [#16621](/~https://github.com/apache/superset/pull/16621) fix(dashboard): label colors included in explore url (@kgabryje) -- [#16632](/~https://github.com/apache/superset/pull/16632) fix(dnd): make clicked dnd metrics unique (@villebro) -- [#16570](/~https://github.com/apache/superset/pull/16570) fix(tests): make parquet select deterministic with order by (@villebro) -- [#16531](/~https://github.com/apache/superset/pull/16531) fix: Adds a loading message when needed in the Select component (@michael-s-molina) -- [#16461](/~https://github.com/apache/superset/pull/16461) fix(datasets): add support for removing owners (@villebro) -- [#16472](/~https://github.com/apache/superset/pull/16472) fix: select database fix (@AAfghahi) -- [#16411](/~https://github.com/apache/superset/pull/16411) fix: make chart rerender on timeseries columns change (@pkdotson) -- [#16511](/~https://github.com/apache/superset/pull/16511) fix: stop endless loading when dataset no longer exist (@pkdotson) -- [#16469](/~https://github.com/apache/superset/pull/16469) fix: sql lab refetch button (@graceguo-supercat) -- [#16451](/~https://github.com/apache/superset/pull/16451) fix: create example DB if needed (@betodealmeida) -- [#16478](/~https://github.com/apache/superset/pull/16478) fix: Revert "chore: Changes the DatabaseSelector and TableSelector to use the new Select component" (@etr2460) -- [#16477](/~https://github.com/apache/superset/pull/16477) fix(explore): JS error for creating new metrics from columns (@ktmud) -- [#16437](/~https://github.com/apache/superset/pull/16437) fix(explore): update overwrite button on perm change (@villebro) -- [#16417](/~https://github.com/apache/superset/pull/16417) fix(dashboard): undo and redo buttons weird alignment (@MaxHuiYYDS) -- [#16413](/~https://github.com/apache/superset/pull/16413) fix: setupPlugin in chart list page (@graceguo-supercat) -- [#16367](/~https://github.com/apache/superset/pull/16367) fix: Disable Slack notification method if no api token (@graceguo-supercat) -- [#16408](/~https://github.com/apache/superset/pull/16408) fix: Revert "fix(explore): let admin overwrite slice" (@rusackas) -- [#16419](/~https://github.com/apache/superset/pull/16419) fix(explore): retain chart ownership on query context update (@villebro) -- [#16391](/~https://github.com/apache/superset/pull/16391) fix: Show cross filter option only when cross filter is enabled (@michael-s-molina) -- [#16323](/~https://github.com/apache/superset/pull/16323) fix: Return original document title when leaving a dashboard (@geido) -- [#16397](/~https://github.com/apache/superset/pull/16397) fix(api): return total count on related endpoint (@villebro) -- [#16410](/~https://github.com/apache/superset/pull/16410) fix: regex for multi-region IPs (@AAfghahi) -- [#16405](/~https://github.com/apache/superset/pull/16405) fix(pylint): Fix master (@john-bodley) -- [#16366](/~https://github.com/apache/superset/pull/16366) fix: show run button when time series column is updated. (@pkdotson) -- [#16383](/~https://github.com/apache/superset/pull/16383) fix: big number default date format (@etr2460) -- [#16380](/~https://github.com/apache/superset/pull/16380) fix: ensure certified fields are populated in metrics (@pkdotson) -- [#16360](/~https://github.com/apache/superset/pull/16360) fix: import dashboard w/o metadata (@betodealmeida) -- [#16330](/~https://github.com/apache/superset/pull/16330) fix: Fix parsing onSaving reports toast when user hasn't saved chart (@hughhhh) -- [#16355](/~https://github.com/apache/superset/pull/16355) fix: columns/index rebuild (@betodealmeida) -- [#16324](/~https://github.com/apache/superset/pull/16324) fix: Blank space in Change dataset modal without warning message (@geido) -- [#16347](/~https://github.com/apache/superset/pull/16347) fix: send CSV pivoted in reports (@betodealmeida) -- [#16329](/~https://github.com/apache/superset/pull/16329) fix: adjust initial state of report modal (@eschutho) -- [#16322](/~https://github.com/apache/superset/pull/16322) fix(explore): reordering columns with dnd sometimes glitching (@kgabryje) -- [#16306](/~https://github.com/apache/superset/pull/16306) fix: pass correct report_format (@eschutho) -- [#16303](/~https://github.com/apache/superset/pull/16303) fix: allow reports to update query_context (@betodealmeida) -- [#16296](/~https://github.com/apache/superset/pull/16296) fix: revert "disable text reports for now" (@betodealmeida) -- [#16243](/~https://github.com/apache/superset/pull/16243) fix: reverting Dataset names (@AAfghahi) -- [#16297](/~https://github.com/apache/superset/pull/16297) fix: rename Databricks (@betodealmeida) -- [#16280](/~https://github.com/apache/superset/pull/16280) fix: set dashboard mine tab to created_by filter (@pkdotson) -- [#16275](/~https://github.com/apache/superset/pull/16275) fix: Fix table height in Change dataset modal when pagination is off (@geido) -- [#16290](/~https://github.com/apache/superset/pull/16290) fix(explore): let admin overwrite slice (@villebro) -- [#16272](/~https://github.com/apache/superset/pull/16272) fix(dashboard): unset empty time filter indicator (@villebro) -- [#16257](/~https://github.com/apache/superset/pull/16257) fix: disable text reports for now (@betodealmeida) -- [#16232](/~https://github.com/apache/superset/pull/16232) fix: Stop the scrollbar in the Change Dataset modal from scrolling down to the pagination component (@geido) -- [#16168](/~https://github.com/apache/superset/pull/16168) fix(Dashboard): Omnibar dropdown visibility and keyboard commands (@geido) -- [#16250](/~https://github.com/apache/superset/pull/16250) fix: skip perms on query context update (@betodealmeida) -- [#16235](/~https://github.com/apache/superset/pull/16235) fix: Revert "feat: Changing Dataset names (#16199)" (@AAfghahi) -- [#16060](/~https://github.com/apache/superset/pull/16060) fix(Explore): Show the tooltip only when label does not fit the container in METRICS/FILTERS/GROUP BY/SORT BY of the DATA panel (@geido) -- [#16192](/~https://github.com/apache/superset/pull/16192) fix(Explore): Show the tooltip only when label does not fit the container in the Dataset panel (@geido) -- [#16194](/~https://github.com/apache/superset/pull/16194) fix(viz): deduce metric name if empty (@villebro) -- [#16211](/~https://github.com/apache/superset/pull/16211) fix: pyinstrument dependency (@betodealmeida) -- [#16145](/~https://github.com/apache/superset/pull/16145) fix: Hide Safari default tooltip (@geido) -- [#16056](/~https://github.com/apache/superset/pull/16056) fix: Make sheet_name into a `ValidationInputError` (@hughhhh) -- [#16137](/~https://github.com/apache/superset/pull/16137) fix: test_import_2_slices_for_same_table (@betodealmeida) -- [#15659](/~https://github.com/apache/superset/pull/15659) fix: Make db service use correct env file (@jongillham) -- [#15762](/~https://github.com/apache/superset/pull/15762) fix: Align alert solid small svg center (@duynguyenhoang) +- [#17980](/~https://github.com/apache/superset/pull/17980) fix: css template API response, less data (@dpgaspar) +- [#17984](/~https://github.com/apache/superset/pull/17984) fix: Change default SECRET_KEY, improve docs and banner warning on de… (@dpgaspar) +- [#17981](/~https://github.com/apache/superset/pull/17981) fix: API logger output (@dpgaspar) +- [#18006](/~https://github.com/apache/superset/pull/18006) fix: SQL Lab sorting of non-numbers (@etr2460) +- [#17573](/~https://github.com/apache/superset/pull/17573) fix(sqllab): Floating numbers not sorting correctly in result column (@lyndsiWilliams) +- [#17961](/~https://github.com/apache/superset/pull/17961) fix: update slug name (@pkdotson) +- [#17992](/~https://github.com/apache/superset/pull/17992) fix: dashboard reload crash (@pkdotson) +- [#18048](/~https://github.com/apache/superset/pull/18048) fix(dashboard): scope status of native filter not update (@stephenLYZ) +- [#16869](/~https://github.com/apache/superset/pull/16869) fix: handle TIME column serialization (@frafra) **Others** -- [#17964](/~https://github.com/apache/superset/pull/17964) chore: bump FAB to 3.4.3 (@dpgaspar) -- [#17894](/~https://github.com/apache/superset/pull/17894) chore: bump gunicorn to 20.1.0 (@mporracindie) -- [#17420](/~https://github.com/apache/superset/pull/17420) chore: Bump FAB to 3.4.0 (@kamalkeshavani-aiinside) -- [#17752](/~https://github.com/apache/superset/pull/17752) chore: add release to pip requirements (@eschutho) -- [#17724](/~https://github.com/apache/superset/pull/17724) ci: temp fix for mysqlclient on an OS regression bug (@dpgaspar) -- [#17702](/~https://github.com/apache/superset/pull/17702) chore(sql): clean up invalid filter clause exception types (@villebro) -- [#17579](/~https://github.com/apache/superset/pull/17579) chore(datasets): Sanitizing /save response (@craig-rueda) -- [#17005](/~https://github.com/apache/superset/pull/17005) ci: skip unnecessary test steps (@villebro) -- [#16609](/~https://github.com/apache/superset/pull/16609) chore: Select component refactoring - SelectAsyncControl - Iteration 5 (@geido) -- [#17037](/~https://github.com/apache/superset/pull/17037) chore(Dashboard): Disable save button in Native Filters when an error is present (@geido) -- [#16940](/~https://github.com/apache/superset/pull/16940) chore(Dashboard): Highlight errored filters on the left pane of the Native Filters form plus several enhancements (@geido) -- [#17065](/~https://github.com/apache/superset/pull/17065) chore: add logging on successful data uploads (@eschutho) -- [#16990](/~https://github.com/apache/superset/pull/16990) chore: Translates the favorite filter param (@michael-s-molina) -- [#16965](/~https://github.com/apache/superset/pull/16965) chore: upgrade superset-ui dependencies (@graceguo-supercat) -- [#16510](/~https://github.com/apache/superset/pull/16510) chore: Select component refactoring - SelectControl - Iteration 5 (@geido) -- [#16943](/~https://github.com/apache/superset/pull/16943) chore: Moves spec files to the src folder - iteration 7 (@michael-s-molina) -- [#16935](/~https://github.com/apache/superset/pull/16935) chore: Moves spec files to the src folder - iteration 6 (@michael-s-molina) -- [#16917](/~https://github.com/apache/superset/pull/16917) refactor: sql lab command: separate concerns into different modules (@ofekisr) -- [#16874](/~https://github.com/apache/superset/pull/16874) chore(native_filter): feature on by default (@junlincc) -- [#16910](/~https://github.com/apache/superset/pull/16910) chore: add certified columns to top of list (@pkdotson) -- [#16927](/~https://github.com/apache/superset/pull/16927) chore: Moves spec files to the src folder - iteration 5 (@michael-s-molina) -- [#16919](/~https://github.com/apache/superset/pull/16919) chore: Adds the drag icon (@michael-s-molina) -- [#16880](/~https://github.com/apache/superset/pull/16880) chore: Moves the stylesheets folder to the assets folder (@michael-s-molina) -- [#16916](/~https://github.com/apache/superset/pull/16916) ci: check npm lockfile version (@villebro) -- [#16852](/~https://github.com/apache/superset/pull/16852) refactor: sql lab: handling command exceptions (@ofekisr) -- [#16857](/~https://github.com/apache/superset/pull/16857) chore: Upgrades Storybook to version 6.3.8 to make it compatible with Webpack 5 (@michael-s-molina) -- [#16819](/~https://github.com/apache/superset/pull/16819) chore: move repro steps up in issue template (@junlincc) -- [#16442](/~https://github.com/apache/superset/pull/16442) chore: Select component refactoring - TimeSeriesColumnControl - Iteration 5 (@geido) -- [#16446](/~https://github.com/apache/superset/pull/16446) chore: Select component refactoring - SaveModal - Iteration 5 (@geido) -- [#16445](/~https://github.com/apache/superset/pull/16445) chore: Select component refactoring - PropertiesModal - Iteration 5 (@geido) -- [#16440](/~https://github.com/apache/superset/pull/16440) chore: Select component refactoring - DndColumnSelectControl - Iteration 5 (@geido) -- [#16423](/~https://github.com/apache/superset/pull/16423) chore: Select component refactoring - MetricControl - Iteration 5 (@geido) -- [#15777](/~https://github.com/apache/superset/pull/15777) chore: Select component refactoring - FilterControl - Iteration 5 (@geido) -- [#16850](/~https://github.com/apache/superset/pull/16850) chore: bump superset-ui to 0.18.8 (@villebro) -- [#16843](/~https://github.com/apache/superset/pull/16843) refactor: sqllab: move sqllab ralated enumns and utils to more logical place (@ofekisr) -- [#16809](/~https://github.com/apache/superset/pull/16809) chore: upgrade to Node 16 (@villebro) -- [#16823](/~https://github.com/apache/superset/pull/16823) chore: Remove immutable.js (@etr2460) -- [#16807](/~https://github.com/apache/superset/pull/16807) chore: bump superset to 0.18.6 (@villebro) -- [#16784](/~https://github.com/apache/superset/pull/16784) chore: Update documentation on schema changes (@frafra) -- [#16672](/~https://github.com/apache/superset/pull/16672) chore: Update OpenAPI definition /database/available (@WingCode) -- [#16626](/~https://github.com/apache/superset/pull/16626) test: RTL overhaul - hackathon (@lyndsiWilliams) -- [#14429](/~https://github.com/apache/superset/pull/14429) chore: Moves the images folder to the assets folder (@michael-s-molina) -- [#16701](/~https://github.com/apache/superset/pull/16701) chore: Upgrade Webpack to v5 (@kgabryje) -- [#14431](/~https://github.com/apache/superset/pull/14431) chore: Moves messageToasts to the components folder (@michael-s-molina) -- [#16393](/~https://github.com/apache/superset/pull/16393) refactor: Changes the list views to use the new Select component (@michael-s-molina) -- [#16483](/~https://github.com/apache/superset/pull/16483) refactor: Changes the DatabaseSelector and TableSelector to use the new Select component (@michael-s-molina) -- [#16762](/~https://github.com/apache/superset/pull/16762) chore: log URI before downloading data on import (@betodealmeida) -- [#16732](/~https://github.com/apache/superset/pull/16732) chore: add browser info to template (@junlincc) -- [#16748](/~https://github.com/apache/superset/pull/16748) ci: bump npm to version 7 (@villebro) -- [#16741](/~https://github.com/apache/superset/pull/16741) chore: Upgrade immer package version (@simcha90) -- [#16725](/~https://github.com/apache/superset/pull/16725) chore: bump superset-ui 0.18.5 (@zhaoyongjie) -- [#16627](/~https://github.com/apache/superset/pull/16627) other: Provide option to add environment variables to only supersetNode (@dd-willgan) -- [#16693](/~https://github.com/apache/superset/pull/16693) chore: add semantic title to the pull request template (@suddjian) -- [#16720](/~https://github.com/apache/superset/pull/16720) chore: bump path-parse module in websocket sidecar app (@rusackas) -- [#16712](/~https://github.com/apache/superset/pull/16712) chore: Improves the Select component to avoid additional queries when all values have been loaded (@michael-s-molina) -- [#16589](/~https://github.com/apache/superset/pull/16589) chore(pylint): Remove top-level disable (@john-bodley) -- [#16540](/~https://github.com/apache/superset/pull/16540) chore: Add option to set a custom color scheme as default (@suddjian) -- [#16669](/~https://github.com/apache/superset/pull/16669) chore: bump sasl (@eschutho) -- [#16287](/~https://github.com/apache/superset/pull/16287) chore(pylint): Reenable too-many-lines check (@john-bodley) -- [#16682](/~https://github.com/apache/superset/pull/16682) refactor: sql_json view endpoint: move all logic from view to Command class (@ofekisr) -- [#16677](/~https://github.com/apache/superset/pull/16677) refactor: sql_json view endpoint: use execution context instead of query (@ofekisr) -- [#16676](/~https://github.com/apache/superset/pull/16676) refactor: sql_json view endpoint: separate flask response creation concern (@ofekisr) -- [#16675](/~https://github.com/apache/superset/pull/16675) refactor: sql_json view endpoint: extract methods (@ofekisr) -- [#16653](/~https://github.com/apache/superset/pull/16653) refactor: sql_json view endpoint: separate setting query limit concern (@ofekisr) -- [#16649](/~https://github.com/apache/superset/pull/16649) refactor: sql_json view endpoint: separate query rendering concern (@ofekisr) -- [#16647](/~https://github.com/apache/superset/pull/16647) refactor: sql_json view endpoint: separate validate query concern (@ofekisr) -- [#16646](/~https://github.com/apache/superset/pull/16646) refactor: sql_json view endpoint: separate save query concern (@ofekisr) -- [#16638](/~https://github.com/apache/superset/pull/16638) chore: Writes the tests for the new Select component (@michael-s-molina) -- [#16615](/~https://github.com/apache/superset/pull/16615) chore: Bump FAB to 3.3.2 (@dpgaspar) -- [#16617](/~https://github.com/apache/superset/pull/16617) chore: Pylint downgrade (@amitmiran137) -- [#16587](/~https://github.com/apache/superset/pull/16587) chore: Merges latest Select changes (@michael-s-molina) -- [#16545](/~https://github.com/apache/superset/pull/16545) perf(dashboard): decrease number of rerenders of FiltersBadge (@kgabryje) -- [#16525](/~https://github.com/apache/superset/pull/16525) perf(dashboard): reduce rerenders of DragDroppable (@kgabryje) -- [#16601](/~https://github.com/apache/superset/pull/16601) chore(deps): bump superset-ui to 0.18.2 (@villebro) -- [#16595](/~https://github.com/apache/superset/pull/16595) refactor: sql_json view endpoint: separate concern into ad hod method (@ofekisr) -- [#16548](/~https://github.com/apache/superset/pull/16548) refactor: sql_json view endpoint: encapsulate ctas parameters (@ofekisr) -- [#16568](/~https://github.com/apache/superset/pull/16568) docs: update security page for small typos (@joeADSP) -- [#16559](/~https://github.com/apache/superset/pull/16559) chore: bump emotion to help with cache clobbering (@eschutho) -- [#16563](/~https://github.com/apache/superset/pull/16563) chore: bump superset-ui to 0.18.1 (@zhaoyongjie) -- [#16544](/~https://github.com/apache/superset/pull/16544) chore: bump superset-ui to 0.18.0 (@villebro) -- [#16546](/~https://github.com/apache/superset/pull/16546) refactor: sql_json view endpoint: extract to method for code reusing (@ofekisr) -- [#16449](/~https://github.com/apache/superset/pull/16449) refactor: sql_json view endpoint: separate getting and checking existi… (@ofekisr) -- [#16447](/~https://github.com/apache/superset/pull/16447) chore: Make View Query Modal draggable and resizable in Dashboard (@geido) -- [#16470](/~https://github.com/apache/superset/pull/16470) chore: remove myself from codeowners on Preset integration (@willbarrett) -- [#16496](/~https://github.com/apache/superset/pull/16496) docs: update entries for v1.2 and v1.3 (@villebro) -- [#16390](/~https://github.com/apache/superset/pull/16390) chore: Docs/superset1.3 release notes (@srinify) -- [#16473](/~https://github.com/apache/superset/pull/16473) docs: Make code snippet usable with required imports in configuration doc (@shawnzhu) -- [#16421](/~https://github.com/apache/superset/pull/16421) perf(dashboard): decouple redux props from dashboard components (@kgabryje) -- [#16444](/~https://github.com/apache/superset/pull/16444) perf(dashboard): reduce number of rerenders of Charts (@kgabryje) -- [#16463](/~https://github.com/apache/superset/pull/16463) chore(ci): bump pylint to 2.10.2 (@villebro) -- [#16466](/~https://github.com/apache/superset/pull/16466) chore: fixed slack invite link (@srinify) -- [#16362](/~https://github.com/apache/superset/pull/16362) refactor(explore): improve typing for Dnd controls (@ktmud) -- [#16441](/~https://github.com/apache/superset/pull/16441) refactor: sql_json view endpoint (@ofekisr) -- [#16415](/~https://github.com/apache/superset/pull/16415) docs: make FEATURE_FLAGS.md reference a link (@suddjian) -- [#16420](/~https://github.com/apache/superset/pull/16420) chore(viz): bump superset-ui to 0.17.87 (@villebro) -- [#16422](/~https://github.com/apache/superset/pull/16422) chore: Removes the TODOs and uses the default page size in AlertReportModal (@michael-s-molina) -- [#16144](/~https://github.com/apache/superset/pull/16144) chore: Changes the AlertReportModal to use the new Select component (@michael-s-molina) -- [#16273](/~https://github.com/apache/superset/pull/16273) chore: Enhance Omnibar (@geido) -- [#16334](/~https://github.com/apache/superset/pull/16334) chore: Changes the DatabaseSelector and TableSelector to use the new Select component (@michael-s-molina) -- [#16392](/~https://github.com/apache/superset/pull/16392) chore: Displays the dataset description in a tooltip in the datasets list (@michael-s-molina) -- [#16388](/~https://github.com/apache/superset/pull/16388) chore(pylint): Enable useless-suppression check (@john-bodley) -- [#16148](/~https://github.com/apache/superset/pull/16148) test: Functional RTL for email report modal II (@lyndsiWilliams) -- [#16286](/~https://github.com/apache/superset/pull/16286) docs: document FLASK_APP_MUTATOR (@shawnzhu) -- [#16353](/~https://github.com/apache/superset/pull/16353) chore(viz): bump deckgl plugin to 0.4.11 (@villebro) -- [#16113](/~https://github.com/apache/superset/pull/16113) docs: add VkusVill and TechAudit to users list (@ETselikov) -- [#16350](/~https://github.com/apache/superset/pull/16350) chore: bump superset-ui to v0.17.85 (@rusackas) -- [#16320](/~https://github.com/apache/superset/pull/16320) chore(explore): make metric/column search input clearable (@kgabryje) -- [#16308](/~https://github.com/apache/superset/pull/16308) docs: Add Care to users list of Apache Superset (@alandao2021) -- [#16285](/~https://github.com/apache/superset/pull/16285) refactor: re-arrange dashboard page js bundles (@graceguo-supercat) -- [#16288](/~https://github.com/apache/superset/pull/16288) chore(explore): remove unnecessary favstar redirect (@villebro) -- [#16266](/~https://github.com/apache/superset/pull/16266) chore(pylint): Reenable raise-missing-from check (@john-bodley) -- [#16264](/~https://github.com/apache/superset/pull/16264) chore(pylint): Reenable too-few-public-methods check (@john-bodley) -- [#16263](/~https://github.com/apache/superset/pull/16263) chore(pylint): Reenable import-outside-toplevel check (@john-bodley) -- [#16268](/~https://github.com/apache/superset/pull/16268) chore(pylint): Reenable too-many-locals check (@john-bodley) -- [#16256](/~https://github.com/apache/superset/pull/16256) chore(pylint): Reenable ungrouped-imports check (@john-bodley) -- [#16138](/~https://github.com/apache/superset/pull/16138) chore(pylint): Reenable super-with-arguments check (@john-bodley) -- [#16252](/~https://github.com/apache/superset/pull/16252) chore: Improves the flow to create a new chart (@michael-s-molina) -- [#16227](/~https://github.com/apache/superset/pull/16227) chore: upgrade mypy and add type guards (@villebro) -- [#16146](/~https://github.com/apache/superset/pull/16146) chore(pylint): Bump Pylint to 2.9.6 (@john-bodley) -- [#16200](/~https://github.com/apache/superset/pull/16200) chore: Shows the dataset description in the gallery dropdown (@michael-s-molina) -- [#16213](/~https://github.com/apache/superset/pull/16213) chore: bump py version for integration test (@hughhhh) -- [#16215](/~https://github.com/apache/superset/pull/16215) chore: Add feature flags to PR template (@junlincc) -- [#16163](/~https://github.com/apache/superset/pull/16163) chore: remove TerserPlugin step for build (@mistercrunch) -- [#15386](/~https://github.com/apache/superset/pull/15386) build: Removed jsx-remove-data-test-id usage from code for multi-build-variant testing (@adam-stasiak) -- [#16074](/~https://github.com/apache/superset/pull/16074) refactor: proper TypeError handling in memoize decorator (@sabiroid) -- [#16110](/~https://github.com/apache/superset/pull/16110) refactor: remove unnecessary dataset queries from dashboard requests (@graceguo-supercat) -- [#16129](/~https://github.com/apache/superset/pull/16129) docs: update install-from-scratch instructions for CentOS (@jberkus) -- [#16043](/~https://github.com/apache/superset/pull/16043) chore: Replaces the select for a dropdown button in the CSS editor (@michael-s-molina) -- [#16048](/~https://github.com/apache/superset/pull/16048) chore: Changes the RefreshIntervalModal component to use the new select component (@michael-s-molina) -- [#16064](/~https://github.com/apache/superset/pull/16064) chore: Changes the dashboard properties modal to use the new select component (@michael-s-molina) -- [#16101](/~https://github.com/apache/superset/pull/16101) docs: fix link and clarify postgres install instructions (@nytai) -- [#16040](/~https://github.com/apache/superset/pull/16040) refactor: adopt --app as celery global option (@john-bodley) diff --git a/RELEASING/README.md b/RELEASING/README.md index 493e6cbfe6881..8724cd1642c89 100644 --- a/RELEASING/README.md +++ b/RELEASING/README.md @@ -287,6 +287,8 @@ cd ~/src/superset/ git branch # Create the release tag git tag -f ${SUPERSET_VERSION} +# push the tag to the remote +git push upstream ${SUPERSET_VERSION} ``` ### Update CHANGELOG and UPDATING on superset diff --git a/UPDATING.md b/UPDATING.md index 07193a462d3be..2f00b14ef8bf9 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -29,6 +29,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [17556](/~https://github.com/apache/superset/pull/17556): Bumps mysqlclient from v1 to v2 - [19113](/~https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` setting has moved from app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward. - [18976](/~https://github.com/apache/superset/pull/18976): When running the app in debug mode, the app will default to use `SimpleCache` for `FILTER_STATE_CACHE_CONFIG` and `EXPLORE_FORM_DATA_CACHE_CONFIG`. When running in non-debug mode, a cache backend will need to be defined, otherwise the application will fail to start. For installations using Redis or other caching backends, it is recommended to use the same backend for both cache configs. - [17881](/~https://github.com/apache/superset/pull/17881): Previously simple adhoc filter values on string columns were stripped of enclosing single and double quotes. To fully support literal quotes in filters, both single and double quotes will no longer be removed from filter values. @@ -60,6 +61,17 @@ assists people when migrating to a new version. - [17536](/~https://github.com/apache/superset/pull/17536): introduced a key-value endpoint to store dashboard filter state. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memchached, you'll probably want to change this setting in `superset_config.py`. The key is `FILTER_STATE_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/). - [17882](/~https://github.com/apache/superset/pull/17882): introduced a key-value endpoint to store Explore form data. This endpoint is backed by Flask-Caching and the default configuration assumes that the values will be stored in the file system. If you are already using another cache backend like Redis or Memchached, you'll probably want to change this setting in `superset_config.py`. The key is `EXPLORE_FORM_DATA_CACHE_CONFIG` and the available settings can be found in Flask-Caching [docs](https://flask-caching.readthedocs.io/en/latest/). +## 1.4.1 + +### Breaking Changes +- [17984](/~https://github.com/apache/superset/pull/17984): Default Flask SECRET_KEY has changed for security reasons. You should always override with your own secret. Set `PREVIOUS_SECRET_KEY` (ex: PREVIOUS_SECRET_KEY = "\2\1thisismyscretkey\1\2\\e\\y\\y\\h") with your previous key and use `superset re-encrypt-secrets` to rotate you current secrets + +### Potential Downtime + +### Deprecations + +### Other + ## 1.4.0 ### Breaking Changes From 50902d51f56eac7813572178527ee83b429db284 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Fri, 18 Mar 2022 10:04:55 -0700 Subject: [PATCH 17/29] fix: allow subquery in ad-hoc SQL (WIP) (#19242) * allow adhoc subquery * add config for allow ad hoc subquery * default to true allow adhoc subquery * fix test * Update superset/errors.py Co-authored-by: Beto Dealmeida * Update superset/connectors/sqla/utils.py Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> * rename and add doc string * fix for big query test * Update superset/connectors/sqla/utils.py Co-authored-by: Beto Dealmeida * Apply suggestions from code review Co-authored-by: Beto Dealmeida * add test * update validate adhoc subquery Co-authored-by: Beto Dealmeida Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> --- superset/config.py | 1 + superset/connectors/sqla/models.py | 7 +++++ superset/connectors/sqla/utils.py | 28 +++++++++++++++++- superset/errors.py | 3 ++ tests/integration_tests/sqla_models_tests.py | 31 +++++++++++++++++++- tests/unit_tests/sql_parse_tests.py | 2 ++ 6 files changed, 70 insertions(+), 2 deletions(-) diff --git a/superset/config.py b/superset/config.py index 775765d08f0aa..6a84a1cf40976 100644 --- a/superset/config.py +++ b/superset/config.py @@ -443,6 +443,7 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: "ALLOW_FULL_CSV_EXPORT": False, "UX_BETA": False, "GENERIC_CHART_AXES": False, + "ALLOW_ADHOC_SUBQUERY": False, } # Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars. diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 99cbc50997559..23d3d326cd9a5 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -78,6 +78,7 @@ from superset.connectors.sqla.utils import ( get_physical_table_metadata, get_virtual_table_metadata, + validate_adhoc_subquery, ) from superset.datasets.models import Dataset as NewDataset from superset.db_engine_specs.base import BaseEngineSpec, CTE_ALIAS, TimestampExpression @@ -885,6 +886,7 @@ def adhoc_metric_to_sqla( elif expression_type == utils.AdhocMetricExpressionType.SQL: tp = self.get_template_processor() expression = tp.process_template(cast(str, metric["sqlExpression"])) + validate_adhoc_subquery(expression) sqla_metric = literal_column(expression) else: raise QueryObjectValidationError("Adhoc metric expressionType is invalid") @@ -908,6 +910,8 @@ def adhoc_column_to_sqla( expression = col["sqlExpression"] if template_processor and expression: expression = template_processor.process_template(expression) + if expression: + validate_adhoc_subquery(expression) sqla_metric = literal_column(expression) return self.make_sqla_column_compatible(sqla_metric, label) @@ -1166,6 +1170,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma elif selected in columns_by_name: outer = columns_by_name[selected].get_sqla_col() else: + validate_adhoc_subquery(selected) outer = literal_column(f"({selected})") outer = self.make_sqla_column_compatible(outer, selected) else: @@ -1178,6 +1183,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma select_exprs.append(outer) elif columns: for selected in columns: + validate_adhoc_subquery(selected) select_exprs.append( columns_by_name[selected].get_sqla_col() if selected in columns_by_name @@ -1389,6 +1395,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma and db_engine_spec.allows_hidden_cc_in_orderby and col.name in [select_col.name for select_col in select_exprs] ): + validate_adhoc_subquery(str(col.expression)) col = literal_column(col.name) direction = asc if ascending else desc qry = qry.order_by(direction(col)) diff --git a/superset/connectors/sqla/utils.py b/superset/connectors/sqla/utils.py index e5209e08dcf68..984eef78f4b76 100644 --- a/superset/connectors/sqla/utils.py +++ b/superset/connectors/sqla/utils.py @@ -17,6 +17,7 @@ from contextlib import closing from typing import Dict, List, Optional, TYPE_CHECKING +import sqlparse from flask_babel import lazy_gettext as _ from sqlalchemy.exc import NoSuchTableError from sqlalchemy.sql.type_api import TypeEngine @@ -28,7 +29,7 @@ ) from superset.models.core import Database from superset.result_set import SupersetResultSet -from superset.sql_parse import ParsedQuery +from superset.sql_parse import has_table_query, ParsedQuery if TYPE_CHECKING: from superset.connectors.sqla.models import SqlaTable @@ -119,3 +120,28 @@ def get_virtual_table_metadata(dataset: "SqlaTable") -> List[Dict[str, str]]: except Exception as ex: raise SupersetGenericDBErrorException(message=str(ex)) from ex return cols + + +def validate_adhoc_subquery(raw_sql: str) -> None: + """ + Check if adhoc SQL contains sub-queries or nested sub-queries with table + :param raw_sql: adhoc sql expression + :raise SupersetSecurityException if sql contains sub-queries or + nested sub-queries with table + """ + # pylint: disable=import-outside-toplevel + from superset import is_feature_enabled + + if is_feature_enabled("ALLOW_ADHOC_SUBQUERY"): + return + + for statement in sqlparse.parse(raw_sql): + if has_table_query(statement): + raise SupersetSecurityException( + SupersetError( + error_type=SupersetErrorType.ADHOC_SUBQUERY_NOT_ALLOWED_ERROR, + message=_("Custom SQL fields cannot contain sub-queries."), + level=ErrorLevel.ERROR, + ) + ) + return diff --git a/superset/errors.py b/superset/errors.py index 9b3414ecb57e2..9198a82d3fe61 100644 --- a/superset/errors.py +++ b/superset/errors.py @@ -80,6 +80,7 @@ class SupersetErrorType(str, Enum): SQLLAB_TIMEOUT_ERROR = "SQLLAB_TIMEOUT_ERROR" RESULTS_BACKEND_ERROR = "RESULTS_BACKEND_ERROR" ASYNC_WORKERS_ERROR = "ASYNC_WORKERS_ERROR" + ADHOC_SUBQUERY_NOT_ALLOWED_ERROR = "ADHOC_SUBQUERY_NOT_ALLOWED_ERROR" # Generic errors GENERIC_COMMAND_ERROR = "GENERIC_COMMAND_ERROR" @@ -138,10 +139,12 @@ class SupersetErrorType(str, Enum): 1034: _("The port number is invalid."), 1035: _("Failed to start remote query on a worker."), 1036: _("The database was deleted."), + 1037: _("Custom SQL fields cannot contain sub-queries."), } ERROR_TYPES_TO_ISSUE_CODES_MAPPING = { + SupersetErrorType.ADHOC_SUBQUERY_NOT_ALLOWED_ERROR: [1037], SupersetErrorType.BACKEND_TIMEOUT_ERROR: [1000, 1001], SupersetErrorType.GENERIC_DB_ENGINE_ERROR: [1002], SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR: [1003, 1004], diff --git a/tests/integration_tests/sqla_models_tests.py b/tests/integration_tests/sqla_models_tests.py index 54779fcbbff9b..223d48a4899a7 100644 --- a/tests/integration_tests/sqla_models_tests.py +++ b/tests/integration_tests/sqla_models_tests.py @@ -34,7 +34,7 @@ from superset.constants import EMPTY_STRING, NULL_STRING from superset.db_engine_specs.bigquery import BigQueryEngineSpec from superset.db_engine_specs.druid import DruidEngineSpec -from superset.exceptions import QueryObjectValidationError +from superset.exceptions import QueryObjectValidationError, SupersetSecurityException from superset.models.core import Database from superset.utils.core import ( AdhocMetricExpressionType, @@ -239,6 +239,35 @@ def test_jinja_metrics_and_calc_columns(self, flask_g): db.session.delete(table) db.session.commit() + def test_adhoc_metrics_and_calc_columns(self): + base_query_obj = { + "granularity": None, + "from_dttm": None, + "to_dttm": None, + "groupby": ["user", "expr"], + "metrics": [ + { + "expressionType": AdhocMetricExpressionType.SQL, + "sqlExpression": "(SELECT (SELECT * from birth_names) " + "from test_validate_adhoc_sql)", + "label": "adhoc_metrics", + } + ], + "is_timeseries": False, + "filter": [], + } + + table = SqlaTable( + table_name="test_validate_adhoc_sql", database=get_example_database() + ) + db.session.commit() + + with pytest.raises(SupersetSecurityException): + table.get_sqla_query(**base_query_obj) + # Cleanup + db.session.delete(table) + db.session.commit() + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_where_operators(self): filters: Tuple[FilterTestCase, ...] = ( diff --git a/tests/unit_tests/sql_parse_tests.py b/tests/unit_tests/sql_parse_tests.py index aa811bdef757e..886eb368e4aa4 100644 --- a/tests/unit_tests/sql_parse_tests.py +++ b/tests/unit_tests/sql_parse_tests.py @@ -1208,6 +1208,8 @@ def test_sqlparse_issue_652(): ("SELECT * FROM (SELECT 1 AS foo, 2 AS bar) ORDER BY foo ASC, bar", False), ("SELECT * FROM other_table", True), ("extract(HOUR from from_unixtime(hour_ts)", False), + ("(SELECT * FROM table)", True), + ("(SELECT COUNT(DISTINCT name) from birth_names)", True), ], ) def test_has_table_query(sql: str, expected: bool) -> None: From b8883410905a8212b9863a652c4d5a775e2a703b Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Fri, 18 Mar 2022 13:57:34 -0400 Subject: [PATCH 18/29] fix(chart-crud): make to update Viz type Filter label to Chart type (#19140) --- superset-frontend/src/views/CRUD/chart/ChartList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 74493751b28b6..2645aa41c74ba 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -519,7 +519,7 @@ function ChartList(props: ChartListProps) { paginate: true, }, { - Header: t('Viz type'), + Header: t('Chart type'), id: 'viz_type', input: 'select', operator: FilterOperator.equals, From d41f9b23a403a5d6882814ac897717b5a33a8f98 Mon Sep 17 00:00:00 2001 From: Grace Guo Date: Fri, 18 Mar 2022 11:23:00 -0700 Subject: [PATCH 19/29] chore(superset 2.0): remove front-end deprecated code (#19241) --- RESOURCES/FEATURE_FLAGS.md | 1 - superset-frontend/package-lock.json | 16 -- superset-frontend/package.json | 1 - .../TimeFormatterRegistrySingleton.ts | 2 +- .../src/utils/featureFlags.ts | 1 - .../OmniContainer/OmniContainer.test.tsx | 150 ------------------ .../components/OmniContainer/Omnibar.test.tsx | 38 ----- .../src/components/OmniContainer/Omnibar.tsx | 45 ------ .../components/OmniContainer/getDashboards.ts | 54 ------- .../src/components/OmniContainer/index.tsx | 108 ------------- .../src/dashboard/components/Dashboard.jsx | 2 - superset-frontend/src/logger/LogUtils.ts | 2 - .../views/CRUD/dashboard/DashboardList.tsx | 3 - superset/config.py | 2 - 14 files changed, 1 insertion(+), 424 deletions(-) delete mode 100644 superset-frontend/src/components/OmniContainer/OmniContainer.test.tsx delete mode 100644 superset-frontend/src/components/OmniContainer/Omnibar.test.tsx delete mode 100644 superset-frontend/src/components/OmniContainer/Omnibar.tsx delete mode 100644 superset-frontend/src/components/OmniContainer/getDashboards.ts delete mode 100644 superset-frontend/src/components/OmniContainer/index.tsx diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index 69f8f8a5a691d..e2199e86b7440 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -41,7 +41,6 @@ These features are **finished** but currently being tested. They are usable, but - DYNAMIC_PLUGINS: [(docs)](https://superset.apache.org/docs/installation/running-on-kubernetes) - DASHBOARD_NATIVE_FILTERS - GLOBAL_ASYNC_QUERIES [(docs)](/~https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries) -- OMNIBAR - VERSIONED_EXPORT - ENABLE_JAVASCRIPT_CONTROLS diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index c6831265e098b..24c235692a7eb 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -89,7 +89,6 @@ "moment-timezone": "^0.5.33", "mousetrap": "^1.6.1", "mustache": "^2.2.1", - "omnibar": "^2.1.1", "polished": "^3.7.2", "prop-types": "^15.7.2", "query-string": "^6.13.7", @@ -45525,15 +45524,6 @@ "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", "integrity": "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==" }, - "node_modules/omnibar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/omnibar/-/omnibar-2.1.1.tgz", - "integrity": "sha512-8txe0of2sb6amV+0vB/VbF7kzwQF8vo9lwmzZTt7TIuugWI661STacLORfl4O6r/A9c4fu69o8Rb+6HIdqArEQ==", - "peerDependencies": { - "react": "^15.5.4", - "react-dom": "^15.5.4" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -95703,12 +95693,6 @@ "resolved": "https://registry.npmjs.org/omit.js/-/omit.js-2.0.2.tgz", "integrity": "sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==" }, - "omnibar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/omnibar/-/omnibar-2.1.1.tgz", - "integrity": "sha512-8txe0of2sb6amV+0vB/VbF7kzwQF8vo9lwmzZTt7TIuugWI661STacLORfl4O6r/A9c4fu69o8Rb+6HIdqArEQ==", - "requires": {} - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 4dabb388c1eb2..d8161d0d7d637 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -149,7 +149,6 @@ "moment-timezone": "^0.5.33", "mousetrap": "^1.6.1", "mustache": "^2.2.1", - "omnibar": "^2.1.1", "polished": "^3.7.2", "prop-types": "^15.7.2", "query-string": "^6.13.7", diff --git a/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistrySingleton.ts b/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistrySingleton.ts index 2eae7a41b50d0..c9aaa2e9a129e 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistrySingleton.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-format/TimeFormatterRegistrySingleton.ts @@ -75,7 +75,7 @@ export function getTimeFormatter( /** * Syntactic sugar for backward compatibility - * TODO: Deprecate this in the next breaking change. + * TODO: will be deprecated in a future version * @param granularity */ export function getTimeFormatterForGranularity(granularity?: TimeGranularity) { diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 8ed617cc3e631..341561f33a51e 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -21,7 +21,6 @@ export enum FeatureFlag { ALLOW_DASHBOARD_DOMAIN_SHARDING = 'ALLOW_DASHBOARD_DOMAIN_SHARDING', ALERT_REPORTS = 'ALERT_REPORTS', - OMNIBAR = 'OMNIBAR', CLIENT_CACHE = 'CLIENT_CACHE', DYNAMIC_PLUGINS = 'DYNAMIC_PLUGINS', SCHEDULED_QUERIES = 'SCHEDULED_QUERIES', diff --git a/superset-frontend/src/components/OmniContainer/OmniContainer.test.tsx b/superset-frontend/src/components/OmniContainer/OmniContainer.test.tsx deleted file mode 100644 index dd926b632df58..0000000000000 --- a/superset-frontend/src/components/OmniContainer/OmniContainer.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/** - * 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 React from 'react'; -import { render, screen, fireEvent } from 'spec/helpers/testing-library'; -import { isFeatureEnabled } from 'src/featureFlags'; -import OmniContainer from './index'; - -jest.mock('src/featureFlags', () => ({ - isFeatureEnabled: jest.fn(), - FeatureFlag: { OMNIBAR: 'OMNIBAR' }, - initFeatureFlags: jest.fn(), -})); - -test('Do not open Omnibar with the featureflag disabled', () => { - (isFeatureEnabled as jest.Mock).mockImplementation( - (ff: string) => !(ff === 'OMNIBAR'), - ); - render( -
- -
, - ); - - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); - fireEvent.keyDown(screen.getByTestId('test'), { - ctrlKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); -}); - -test('Open Omnibar with ctrl + k with featureflag enabled', () => { - (isFeatureEnabled as jest.Mock).mockImplementation( - (ff: string) => ff === 'OMNIBAR', - ); - render( -
- -
, - ); - - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); - - // show Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - ctrlKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).toBeInTheDocument(); - - // hide Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - ctrlKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); -}); - -test('Open Omnibar with Command + k with featureflag enabled', () => { - (isFeatureEnabled as jest.Mock).mockImplementation( - (ff: string) => ff === 'OMNIBAR', - ); - render( -
- -
, - ); - - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); - - // show Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - metaKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).toBeInTheDocument(); - - // hide Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - metaKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); -}); - -test('Open Omnibar with Cmd/Ctrl-K and close with ESC', () => { - (isFeatureEnabled as jest.Mock).mockImplementation( - (ff: string) => ff === 'OMNIBAR', - ); - render( -
- -
, - ); - - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); - - // show Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - ctrlKey: true, - code: 'KeyK', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).toBeInTheDocument(); - - // Close Omnibar - fireEvent.keyDown(screen.getByTestId('test'), { - key: 'Escape', - code: 'Escape', - }); - expect( - screen.queryByPlaceholderText('Search all dashboards'), - ).not.toBeInTheDocument(); -}); diff --git a/superset-frontend/src/components/OmniContainer/Omnibar.test.tsx b/superset-frontend/src/components/OmniContainer/Omnibar.test.tsx deleted file mode 100644 index 39b367326a1bf..0000000000000 --- a/superset-frontend/src/components/OmniContainer/Omnibar.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 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 React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; -import { Omnibar } from './Omnibar'; - -test('Must put id on input', () => { - render( - , - ); - - expect(screen.getByPlaceholderText('Test Omnibar')).toBeInTheDocument(); - expect(screen.getByPlaceholderText('Test Omnibar')).toHaveAttribute( - 'id', - 'test-id-attribute', - ); -}); diff --git a/superset-frontend/src/components/OmniContainer/Omnibar.tsx b/superset-frontend/src/components/OmniContainer/Omnibar.tsx deleted file mode 100644 index aeeffd1ef6268..0000000000000 --- a/superset-frontend/src/components/OmniContainer/Omnibar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 React from 'react'; -import OmnibarDeprecated from 'omnibar'; - -interface Props { - id: string; - placeholder: string; - extensions: ((query: string) => Promise)[]; -} - -/** - * @deprecated Component "omnibar" does not support prop className or id (the original implementation used className). However, the original javascript code was sending these prop and was working correctly. lol - * As this behavior is unpredictable, and does not works whitch types, I have isolated this component so that in the future a better solution can be found and implemented. - * We need to find a substitute for this component or some way of working around this problem - */ -export function Omnibar({ extensions, placeholder, id }: Props) { - return ( - - ); -} diff --git a/superset-frontend/src/components/OmniContainer/getDashboards.ts b/superset-frontend/src/components/OmniContainer/getDashboards.ts deleted file mode 100644 index faa8336b0bc3b..0000000000000 --- a/superset-frontend/src/components/OmniContainer/getDashboards.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 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, SupersetClient } from '@superset-ui/core'; - -interface DashboardItem { - changed_by_name: string; - changed_on: string; - creator: string; - dashboard_link: string; - dashboard_title: string; - id: number; - modified: string; - url: string; -} - -interface Dashboards extends DashboardItem { - title: string; -} - -export const getDashboards = async ( - query: string, -): Promise<(Dashboards | { title: string })[]> => { - // todo: Build a dedicated endpoint for dashboard searching - // i.e. superset/v1/api/dashboards?q=${query} - let response; - try { - response = await SupersetClient.get({ - endpoint: `/dashboardasync/api/read?_oc_DashboardModelViewAsync=changed_on&_od_DashboardModelViewAsync=desc&_flt_2_dashboard_title=${query}`, - }); - } catch (error) { - return [{ title: t('An error occurred while fetching dashboards') }]; - } - return response?.json.result.map((item: DashboardItem) => ({ - title: item.dashboard_title, - ...item, - })); -}; diff --git a/superset-frontend/src/components/OmniContainer/index.tsx b/superset-frontend/src/components/OmniContainer/index.tsx deleted file mode 100644 index c5ca25c380ce3..0000000000000 --- a/superset-frontend/src/components/OmniContainer/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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 React, { useRef, useState } from 'react'; -import { styled, t } from '@superset-ui/core'; -import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; -import Modal from 'src/components/Modal'; -import { useComponentDidMount } from 'src/hooks/useComponentDidMount'; -import { logEvent } from 'src/logger/actions'; -import { Omnibar } from './Omnibar'; -import { LOG_ACTIONS_OMNIBAR_TRIGGERED } from '../../logger/LogUtils'; -import { getDashboards } from './getDashboards'; - -const OmniModal = styled(Modal)` - margin-top: 20%; - - .ant-modal-body { - padding: 0; - overflow: visible; - } -`; - -export default function OmniContainer() { - const showOmni = useRef(); - const modalRef = useRef(null); - const [showModal, setShowModal] = useState(false); - const handleLogEvent = (show: boolean) => - logEvent(LOG_ACTIONS_OMNIBAR_TRIGGERED, { - show_omni: show, - }); - const handleClose = () => { - showOmni.current = false; - setShowModal(false); - handleLogEvent(false); - }; - - useComponentDidMount(() => { - showOmni.current = false; - - function handleKeydown(event: KeyboardEvent) { - if (!isFeatureEnabled(FeatureFlag.OMNIBAR)) return; - const controlOrCommand = event.ctrlKey || event.metaKey; - const isOk = ['KeyK'].includes(event.code); - const isEsc = event.key === 'Escape'; - - if (isEsc && showOmni.current) { - handleClose(); - return; - } - if (controlOrCommand && isOk) { - showOmni.current = !showOmni.current; - setShowModal(showOmni.current); - handleLogEvent(!!showOmni.current); - } - } - - function handleClickOutside(event: MouseEvent) { - if ( - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { - handleClose(); - } - } - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleKeydown); - return () => { - document.removeEventListener('keydown', handleKeydown); - document.removeEventListener('mousedown', handleClickOutside); - }; - }); - - return ( - {}} - destroyOnClose - > -
- -
-
- ); -} diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx index 22c20883fe0ad..f04eb696aade8 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.jsx @@ -36,7 +36,6 @@ import { LOG_ACTIONS_MOUNT_DASHBOARD, Logger, } from '../../logger/LogUtils'; -import OmniContainer from '../../components/OmniContainer'; import { areObjectsEqual } from '../../reduxUtils'; import '../stylesheets/index.less'; @@ -292,7 +291,6 @@ class Dashboard extends React.PureComponent { } return ( <> - ); diff --git a/superset-frontend/src/logger/LogUtils.ts b/superset-frontend/src/logger/LogUtils.ts index 9cdb5e4634bd5..b3e834bc935df 100644 --- a/superset-frontend/src/logger/LogUtils.ts +++ b/superset-frontend/src/logger/LogUtils.ts @@ -35,7 +35,6 @@ export const LOG_ACTIONS_EXPLORE_DASHBOARD_CHART = 'explore_dashboard_chart'; export const LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART = 'export_csv_dashboard_chart'; export const LOG_ACTIONS_CHANGE_DASHBOARD_FILTER = 'change_dashboard_filter'; -export const LOG_ACTIONS_OMNIBAR_TRIGGERED = 'omnibar_triggered'; // Log event types -------------------------------------------------------------- export const LOG_EVENT_TYPE_TIMING = new Set([ @@ -54,7 +53,6 @@ export const LOG_EVENT_TYPE_USER = new Set([ LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, - LOG_ACTIONS_OMNIBAR_TRIGGERED, LOG_ACTIONS_MOUNT_EXPLORER, ]); diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index aa1b9fc7b717a..b3da8ee8e3534 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -46,7 +46,6 @@ import FaveStar from 'src/components/FaveStar'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; import { Tooltip } from 'src/components/Tooltip'; import ImportModelsModal from 'src/components/ImportModal/index'; -import OmniContainer from 'src/components/OmniContainer'; import Dashboard from 'src/dashboard/containers/Dashboard'; import CertifiedBadge from 'src/components/CertifiedBadge'; @@ -689,8 +688,6 @@ function DashboardList(props: DashboardListProps) { setPasswordFields={setPasswordFields} /> - - {preparingExport && } ); diff --git a/superset/config.py b/superset/config.py index 6a84a1cf40976..576428f06b56b 100644 --- a/superset/config.py +++ b/superset/config.py @@ -420,8 +420,6 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: "EMBEDDED_SUPERSET": False, # Enables Alerts and reports new implementation "ALERT_REPORTS": False, - # Enable experimental feature to search for other dashboards - "OMNIBAR": False, "DASHBOARD_RBAC": False, "ENABLE_EXPLORE_DRAG_AND_DROP": True, "ENABLE_FILTER_BOX_MIGRATION": False, From 4f0074a4aec7bf0868c3032ef00c4eb92930422a Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Fri, 18 Mar 2022 15:17:12 -0400 Subject: [PATCH 20/29] fix: custom SQL in Sort By Breaks Bar Chart (#19069) --- superset/viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/viz.py b/superset/viz.py index 7c0f8e134875b..4998da12dcd57 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1842,7 +1842,7 @@ def get_data(self, df: pd.DataFrame) -> VizData: # pylint: disable=too-many-loc sortby = utils.get_metric_name( self.form_data.get("timeseries_limit_metric") or metrics[0] ) - row = df.groupby(groupby).sum()[sortby].copy() + row = df.groupby(groupby)[sortby].sum().copy() is_asc = not self.form_data.get("order_desc") row.sort_values(ascending=is_asc, inplace=True) pt = df.pivot_table(index=groupby, columns=columns, values=metrics) From 97abc28a1f951da66b889645212820b9f5be3ce0 Mon Sep 17 00:00:00 2001 From: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> Date: Fri, 18 Mar 2022 14:00:23 -0700 Subject: [PATCH 21/29] chore!: remove `ENABLE_REACT_CRUD_VIEWS` feature flag (permanently enable) (#19231) * remove ENABLE_REACT_CRUD_VIEWS feature flag * docs * deal with problematic tests * empty test suite * skip test * test conditions changed * removing the tests instead of skipping --- .github/workflows/superset-e2e.yml | 1 - CONTRIBUTING.md | 1 - RESOURCES/FEATURE_FLAGS.md | 1 - UPDATING.md | 1 + docker/docker-bootstrap.sh | 1 - docker/docker-init.sh | 1 - docs/docs/contributing/testing-locally.mdx | 1 - .../src/utils/featureFlags.ts | 1 - .../src/SqlLab/components/App/index.jsx | 23 +- .../Datasource/DatasourceModal.test.jsx | 33 +- .../components/Datasource/DatasourceModal.tsx | 6 +- superset-frontend/src/views/routes.tsx | 2 - superset/config.py | 5 - superset/connectors/sqla/views.py | 3 - superset/views/alerts.py | 10 +- superset/views/annotations.py | 7 - superset/views/chart/views.py | 4 - superset/views/core.py | 3 - superset/views/css_templates.py | 4 - superset/views/dashboard/views.py | 3 - superset/views/database/views.py | 5 +- superset/views/sql_lab.py | 5 +- tests/integration_tests/core_tests.py | 11 - tests/integration_tests/csv_upload_tests.py | 315 ------------------ .../dashboards/security/base_case.py | 26 +- .../security/security_rbac_tests.py | 109 ------ tests/integration_tests/datasets/api_tests.py | 9 +- tests/integration_tests/security_tests.py | 18 - .../integration_tests/superset_test_config.py | 1 - tox.ini | 5 - 30 files changed, 19 insertions(+), 596 deletions(-) diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index bc47d6a17752f..be0df99551a40 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -24,7 +24,6 @@ jobs: browser: ["chrome"] env: FLASK_ENV: development - ENABLE_REACT_CRUD_VIEWS: true SUPERSET_CONFIG: tests.integration_tests.superset_test_config SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset PYTHONPATH: ${{ github.workspace }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 910572e3db7d1..84a948511ab15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -804,7 +804,6 @@ We use [Cypress](https://www.cypress.io/) for integration tests. Tests can be ru ```bash export SUPERSET_CONFIG=tests.integration_tests.superset_test_config export SUPERSET_TESTENV=true -export ENABLE_REACT_CRUD_VIEWS=true export CYPRESS_BASE_URL="http://localhost:8081" superset db upgrade superset load_test_users diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index e2199e86b7440..77e381c7275cc 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -63,4 +63,3 @@ These features flags currently default to True and **will be removed in a future - ALLOW_DASHBOARD_DOMAIN_SHARDING - DISPLAY_MARKDOWN_HTML -- ENABLE_REACT_CRUD_VIEWS diff --git a/UPDATING.md b/UPDATING.md index 2f00b14ef8bf9..4272ecc56bb32 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -29,6 +29,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [19231](/~https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case. - [17556](/~https://github.com/apache/superset/pull/17556): Bumps mysqlclient from v1 to v2 - [19113](/~https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` setting has moved from app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward. - [18976](/~https://github.com/apache/superset/pull/18976): When running the app in debug mode, the app will default to use `SimpleCache` for `FILTER_STATE_CACHE_CONFIG` and `EXPLORE_FORM_DATA_CACHE_CONFIG`. When running in non-debug mode, a cache backend will need to be defined, otherwise the application will fail to start. For installations using Redis or other caching backends, it is recommended to use the same backend for both cache configs. diff --git a/docker/docker-bootstrap.sh b/docker/docker-bootstrap.sh index 67e5294be5fdc..150f351e4b0d7 100755 --- a/docker/docker-bootstrap.sh +++ b/docker/docker-bootstrap.sh @@ -23,7 +23,6 @@ REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt" if [ "$CYPRESS_CONFIG" == "true" ]; then export SUPERSET_CONFIG=tests.integration_tests.superset_test_config export SUPERSET_TESTENV=true - export ENABLE_REACT_CRUD_VIEWS=true export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset fi # diff --git a/docker/docker-init.sh b/docker/docker-init.sh index d5ead5039857e..07830694048a7 100755 --- a/docker/docker-init.sh +++ b/docker/docker-init.sh @@ -43,7 +43,6 @@ if [ "$CYPRESS_CONFIG" == "true" ]; then ADMIN_PASSWORD="general" export SUPERSET_CONFIG=tests.superset_test_config export SUPERSET_TESTENV=true - export ENABLE_REACT_CRUD_VIEWS=true export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset fi # Initialize the database diff --git a/docs/docs/contributing/testing-locally.mdx b/docs/docs/contributing/testing-locally.mdx index 17a1c81086444..22a628b661502 100644 --- a/docs/docs/contributing/testing-locally.mdx +++ b/docs/docs/contributing/testing-locally.mdx @@ -76,7 +76,6 @@ We use [Cypress](https://www.cypress.io/) for integration tests. Tests can be ru ```bash export SUPERSET_CONFIG=tests.integration_tests.superset_test_config export SUPERSET_TESTENV=true -export ENABLE_REACT_CRUD_VIEWS=true export CYPRESS_BASE_URL="http://localhost:8081" superset db upgrade superset load_test_users diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 341561f33a51e..03d44078344f7 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -31,7 +31,6 @@ export enum FeatureFlag { THUMBNAILS = 'THUMBNAILS', LISTVIEWS_DEFAULT_CARD_VIEW = 'LISTVIEWS_DEFAULT_CARD_VIEW', DISABLE_LEGACY_DATASOURCE_EDITOR = 'DISABLE_LEGACY_DATASOURCE_EDITOR', - ENABLE_REACT_CRUD_VIEWS = 'ENABLE_REACT_CRUD_VIEWS', DISABLE_DATASET_SOURCE_EDIT = 'DISABLE_DATASET_SOURCE_EDIT', DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML', ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML', diff --git a/superset-frontend/src/SqlLab/components/App/index.jsx b/superset-frontend/src/SqlLab/components/App/index.jsx index bce43f477800e..c98ff12e87017 100644 --- a/superset-frontend/src/SqlLab/components/App/index.jsx +++ b/superset-frontend/src/SqlLab/components/App/index.jsx @@ -21,7 +21,6 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { t, supersetTheme, ThemeProvider } from '@superset-ui/core'; -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import throttle from 'lodash/throttle'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import { @@ -32,7 +31,6 @@ import { import * as Actions from 'src/SqlLab/actions/sqlLab'; import TabbedSqlEditors from '../TabbedSqlEditors'; import QueryAutoRefresh from '../QueryAutoRefresh'; -import QuerySearch from '../QuerySearch'; class App extends React.PureComponent { constructor(props) { @@ -96,29 +94,14 @@ class App extends React.PureComponent { } render() { - let content; if (this.state.hash && this.state.hash === '#search') { - if (isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS)) { - return window.location.replace('/superset/sqllab/history/'); - } - content = ( - - ); - } else { - content = ( - <> - - - - ); + return window.location.replace('/superset/sqllab/history/'); } return (
- {content} + +
diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx index c9d608817794d..9743e3a325f36 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.test.jsx @@ -24,7 +24,7 @@ import { Provider } from 'react-redux'; import fetchMock from 'fetch-mock'; import thunk from 'redux-thunk'; import sinon from 'sinon'; -import { supersetTheme, ThemeProvider, FeatureFlag } from '@superset-ui/core'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; import Modal from 'src/components/Modal'; @@ -70,11 +70,7 @@ describe('DatasourceModal', () => { let wrapper; let isFeatureEnabledMock; beforeEach(async () => { - isFeatureEnabledMock = jest - .spyOn(featureFlags, 'isFeatureEnabled') - .mockImplementation( - featureFlag => featureFlag === FeatureFlag.ENABLE_REACT_CRUD_VIEWS, - ); + isFeatureEnabledMock = jest.spyOn(featureFlags, 'isFeatureEnabled'); fetchMock.reset(); wrapper = await mountAndWait(); }); @@ -125,28 +121,3 @@ describe('DatasourceModal', () => { ).toExist(); }); }); - -describe('DatasourceModal without legacy data btn', () => { - let wrapper; - let isFeatureEnabledMock; - beforeEach(async () => { - isFeatureEnabledMock = jest - .spyOn(featureFlags, 'isFeatureEnabled') - .mockReturnValue(false); - fetchMock.reset(); - wrapper = await mountAndWait(); - }); - - afterAll(() => { - isFeatureEnabledMock.restore(); - }); - - it('hides legacy data source btn', () => { - isFeatureEnabledMock = jest - .spyOn(featureFlags, 'isFeatureEnabled') - .mockReturnValue(false); - expect( - wrapper.find('button[data-test="datasource-modal-legacy-edit"]'), - ).not.toExist(); - }); -}); diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index 124c404082612..92f35d622edd8 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -183,9 +183,9 @@ const DatasourceModal: FunctionComponent = ({ }); }; - const showLegacyDatasourceEditor = - isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS) && - !isFeatureEnabled(FeatureFlag.DISABLE_LEGACY_DATASOURCE_EDITOR); + const showLegacyDatasourceEditor = !isFeatureEnabled( + FeatureFlag.DISABLE_LEGACY_DATASOURCE_EDITOR, + ); return ( Optional[str]: "TAGGING_SYSTEM": False, "SQLLAB_BACKEND_PERSISTENCE": True, "LISTVIEWS_DEFAULT_CARD_VIEW": False, - # Enables the replacement React views for all the FAB views (list, edit, show) with - # designs introduced in /~https://github.com/apache/superset/issues/8976 - # (SIP-34). This is a work in progress so not all features available in FAB have - # been implemented. - "ENABLE_REACT_CRUD_VIEWS": True, # When True, this flag allows display of HTML tags in Markdown components "DISPLAY_MARKDOWN_HTML": True, # When True, this escapes HTML (rather than rendering it) in Markdown components diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index a16ffa49f62ba..4b43d2f046f6b 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -647,7 +647,4 @@ class RefreshResults: @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() diff --git a/superset/views/alerts.py b/superset/views/alerts.py index 416966fbe7c35..04640fa223fe8 100644 --- a/superset/views/alerts.py +++ b/superset/views/alerts.py @@ -92,10 +92,7 @@ class BaseAlertReportView(BaseSupersetView): @has_access @permission_name("read") def list(self) -> FlaskResponse: - if not ( - is_feature_enabled("ENABLE_REACT_CRUD_VIEWS") - and is_feature_enabled("ALERT_REPORTS") - ): + if not is_feature_enabled("ALERT_REPORTS"): return abort(404) return super().render_app_template() @@ -103,10 +100,7 @@ def list(self) -> FlaskResponse: @has_access @permission_name("read") def log(self, pk: int) -> FlaskResponse: # pylint: disable=unused-argument - if not ( - is_feature_enabled("ENABLE_REACT_CRUD_VIEWS") - and is_feature_enabled("ALERT_REPORTS") - ): + if not is_feature_enabled("ALERT_REPORTS"): return abort(404) return super().render_app_template() diff --git a/superset/views/annotations.py b/superset/views/annotations.py index dc1df5642af35..87150c5cdda31 100644 --- a/superset/views/annotations.py +++ b/superset/views/annotations.py @@ -23,7 +23,6 @@ from flask_babel import lazy_gettext as _ from wtforms.validators import StopValidation -from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.annotations import Annotation, AnnotationLayer from superset.superset_typing import FlaskResponse @@ -100,9 +99,6 @@ def pre_update(self, item: "AnnotationModelView") -> None: @expose("//annotation/", methods=["GET"]) @has_access def annotation(self, pk: int) -> FlaskResponse: # pylint: disable=unused-argument - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() @@ -128,7 +124,4 @@ class AnnotationLayerModelView(SupersetModelView): @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() diff --git a/superset/views/chart/views.py b/superset/views/chart/views.py index 9ecc69f7b9e8e..72058c32e7f33 100644 --- a/superset/views/chart/views.py +++ b/superset/views/chart/views.py @@ -21,7 +21,6 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import lazy_gettext as _ -from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.slice import Slice from superset.superset_typing import FlaskResponse @@ -73,9 +72,6 @@ def add(self) -> FlaskResponse: @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() diff --git a/superset/views/core.py b/superset/views/core.py index 6957296ab634c..50a56569c547c 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2919,9 +2919,6 @@ def sqllab(self) -> FlaskResponse: @expose("/sqllab/history/", methods=["GET"]) @event_logger.log_this def sqllab_history(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return redirect("/superset/sqllab#search", code=307) - return super().render_app_template() @api diff --git a/superset/views/css_templates.py b/superset/views/css_templates.py index 2cfbd43ae962a..74505e7109dc6 100644 --- a/superset/views/css_templates.py +++ b/superset/views/css_templates.py @@ -19,7 +19,6 @@ from flask_appbuilder.security.decorators import has_access from flask_babel import lazy_gettext as _ -from superset import is_feature_enabled from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models import core as models from superset.superset_typing import FlaskResponse @@ -46,9 +45,6 @@ class CssTemplateModelView(SupersetModelView, DeleteMixin): @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py index 49ba61d08e0d2..471d168e1ec64 100644 --- a/superset/views/dashboard/views.py +++ b/superset/views/dashboard/views.py @@ -61,9 +61,6 @@ class DashboardModelView( @has_access @expose("/list/") def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() @action("mulexport", __("Export"), __("Export dashboards?"), "fa-database") diff --git a/superset/views/database/views.py b/superset/views/database/views.py index aea4e04383570..659a1be78d8e3 100644 --- a/superset/views/database/views.py +++ b/superset/views/database/views.py @@ -31,7 +31,7 @@ from wtforms.validators import ValidationError import superset.models.core as models -from superset import app, db, is_feature_enabled +from superset import app, db from superset.connectors.sqla.models import SqlaTable from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.exceptions import CertificateException @@ -106,9 +106,6 @@ def _delete(self, pk: int) -> None: @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py index 49336a84a18d6..0e17f46f16f07 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -22,7 +22,7 @@ from flask_babel import lazy_gettext as _ from sqlalchemy import and_ -from superset import db, is_feature_enabled +from superset import db from superset.constants import MODEL_VIEW_RW_METHOD_PERMISSION_MAP, RouteMethod from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState from superset.superset_typing import FlaskResponse @@ -79,9 +79,6 @@ class SavedQueryView(SupersetModelView, DeleteMixin): @expose("/list/") @has_access def list(self) -> FlaskResponse: - if not is_feature_enabled("ENABLE_REACT_CRUD_VIEWS"): - return super().list() - return super().render_app_template() def pre_add(self, item: "SavedQueryView") -> None: diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 26674054ae394..f573e68075441 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -428,17 +428,6 @@ def test_slices(self): resp = self.client.get(url) self.assertEqual(resp.status_code, 200) - def test_tablemodelview_list(self): - self.login(username="admin") - - url = "/tablemodelview/list/" - resp = self.get_resp(url) - - # assert that a table is listed - table = db.session.query(SqlaTable).first() - assert table.name in resp - assert "/superset/explore/table/{}".format(table.id) in resp - def test_add_slice(self): self.login(username="admin") # assert that /chart/add responds with 200 diff --git a/tests/integration_tests/csv_upload_tests.py b/tests/integration_tests/csv_upload_tests.py index d5da25c38c115..c9bc11db98557 100644 --- a/tests/integration_tests/csv_upload_tests.py +++ b/tests/integration_tests/csv_upload_tests.py @@ -203,318 +203,3 @@ def mock_upload_to_s3(filename: str, upload_prefix: str, table: Table) -> str: container.exec_run(f"hdfs dfs -put {src} {dest}") # hive external table expectes a directory for the location return dest_dir - - -@pytest.mark.usefixtures("setup_csv_upload") -@pytest.mark.usefixtures("create_csv_files") -@mock.patch( - "superset.models.core.config", - {**app.config, "ALLOWED_USER_CSV_SCHEMA_FUNC": lambda d, u: ["admin_database"]}, -) -@mock.patch("superset.db_engine_specs.hive.upload_to_s3", mock_upload_to_s3) -@mock.patch("superset.views.database.views.event_logger.log_with_context") -def test_import_csv_enforced_schema(mock_event_logger): - if utils.backend() == "sqlite": - pytest.skip("Sqlite doesn't support schema / database creation") - - full_table_name = f"admin_database.{CSV_UPLOAD_TABLE_W_SCHEMA}" - - # Invalid table name - resp = upload_csv(CSV_FILENAME1, full_table_name) - assert "Table name cannot contain a schema" in resp - - # no schema specified, fail upload - resp = upload_csv(CSV_FILENAME1, CSV_UPLOAD_TABLE_W_SCHEMA, extra={"schema": None}) - assert ( - f'Database "{CSV_UPLOAD_DATABASE}" schema "None" is not allowed for csv uploads' - in resp - ) - - success_msg = f'CSV file "{CSV_FILENAME1}" uploaded to table "{full_table_name}"' - resp = upload_csv( - CSV_FILENAME1, - CSV_UPLOAD_TABLE_W_SCHEMA, - extra={"schema": "admin_database", "if_exists": "replace"}, - ) - assert success_msg in resp - mock_event_logger.assert_called_with( - action="successful_csv_upload", - database=get_upload_db().name, - schema="admin_database", - table=CSV_UPLOAD_TABLE_W_SCHEMA, - ) - - engine = get_upload_db().get_sqla_engine() - data = engine.execute( - f"SELECT * from {ADMIN_SCHEMA_NAME}.{CSV_UPLOAD_TABLE_W_SCHEMA}" - ).fetchall() - assert data == [("john", 1), ("paul", 2)] - - # user specified schema doesn't match, fail - resp = upload_csv( - CSV_FILENAME1, CSV_UPLOAD_TABLE_W_SCHEMA, extra={"schema": "gold"} - ) - assert ( - f'Database "{CSV_UPLOAD_DATABASE}" schema "gold" is not allowed for csv uploads' - in resp - ) - - # user specified schema matches the expected schema, append - if utils.backend() == "hive": - pytest.skip("Hive database doesn't support append csv uploads.") - resp = upload_csv( - CSV_FILENAME1, - CSV_UPLOAD_TABLE_W_SCHEMA, - extra={"schema": "admin_database", "if_exists": "append"}, - ) - assert success_msg in resp - - -@mock.patch("superset.db_engine_specs.hive.upload_to_s3", mock_upload_to_s3) -def test_import_csv_explore_database(setup_csv_upload, create_csv_files): - schema = utils.get_example_default_schema() - full_table_name = ( - f"{schema}.{CSV_UPLOAD_TABLE_W_EXPLORE}" - if schema - else CSV_UPLOAD_TABLE_W_EXPLORE - ) - - if utils.backend() == "sqlite": - pytest.skip("Sqlite doesn't support schema / database creation") - - resp = upload_csv(CSV_FILENAME1, CSV_UPLOAD_TABLE_W_EXPLORE) - assert f'CSV file "{CSV_FILENAME1}" uploaded to table "{full_table_name}"' in resp - table = SupersetTestCase.get_table(name=CSV_UPLOAD_TABLE_W_EXPLORE) - assert table.database_id == superset.utils.database.get_example_database().id - - -@pytest.mark.usefixtures("setup_csv_upload") -@pytest.mark.usefixtures("create_csv_files") -@mock.patch("superset.db_engine_specs.hive.upload_to_s3", mock_upload_to_s3) -@mock.patch("superset.views.database.views.event_logger.log_with_context") -def test_import_csv(mock_event_logger): - schema = utils.get_example_default_schema() - full_table_name = f"{schema}.{CSV_UPLOAD_TABLE}" if schema else CSV_UPLOAD_TABLE - success_msg_f1 = f'CSV file "{CSV_FILENAME1}" uploaded to table "{full_table_name}"' - - test_db = get_upload_db() - - # initial upload with fail mode - resp = upload_csv(CSV_FILENAME1, CSV_UPLOAD_TABLE) - assert success_msg_f1 in resp - - # upload again with fail mode; should fail - fail_msg = ( - f'Unable to upload CSV file "{CSV_FILENAME1}" to table "{CSV_UPLOAD_TABLE}"' - ) - resp = upload_csv(CSV_FILENAME1, CSV_UPLOAD_TABLE) - assert fail_msg in resp - - if utils.backend() != "hive": - # upload again with append mode - resp = upload_csv( - CSV_FILENAME1, CSV_UPLOAD_TABLE, extra={"if_exists": "append"} - ) - assert success_msg_f1 in resp - mock_event_logger.assert_called_with( - action="successful_csv_upload", - database=test_db.name, - schema=schema, - table=CSV_UPLOAD_TABLE, - ) - - # upload again with replace mode and specific columns - resp = upload_csv( - CSV_FILENAME1, - CSV_UPLOAD_TABLE, - extra={"if_exists": "replace", "usecols": '["a"]'}, - ) - assert success_msg_f1 in resp - - # make sure only specified column name was read - table = SupersetTestCase.get_table(name=CSV_UPLOAD_TABLE) - assert "b" not in table.column_names - - # upload again with replace mode - resp = upload_csv(CSV_FILENAME1, CSV_UPLOAD_TABLE, extra={"if_exists": "replace"}) - assert success_msg_f1 in resp - - # try to append to table from file with different schema - resp = upload_csv(CSV_FILENAME2, CSV_UPLOAD_TABLE, extra={"if_exists": "append"}) - fail_msg_f2 = ( - f'Unable to upload CSV file "{CSV_FILENAME2}" to table "{CSV_UPLOAD_TABLE}"' - ) - assert fail_msg_f2 in resp - - # replace table from file with different schema - resp = upload_csv(CSV_FILENAME2, CSV_UPLOAD_TABLE, extra={"if_exists": "replace"}) - success_msg_f2 = f'CSV file "{CSV_FILENAME2}" uploaded to table "{full_table_name}"' - assert success_msg_f2 in resp - - table = SupersetTestCase.get_table(name=CSV_UPLOAD_TABLE) - # make sure the new column name is reflected in the table metadata - assert "d" in table.column_names - - # ensure user is assigned as an owner - assert security_manager.find_user("admin") in table.owners - - # null values are set - upload_csv( - CSV_FILENAME2, - CSV_UPLOAD_TABLE, - extra={"null_values": '["", "john"]', "if_exists": "replace"}, - ) - # make sure that john and empty string are replaced with None - engine = test_db.get_sqla_engine() - data = engine.execute(f"SELECT * from {CSV_UPLOAD_TABLE}").fetchall() - assert data == [(None, 1, "x"), ("paul", 2, None)] - - # default null values - upload_csv(CSV_FILENAME2, CSV_UPLOAD_TABLE, extra={"if_exists": "replace"}) - # make sure that john and empty string are replaced with None - data = engine.execute(f"SELECT * from {CSV_UPLOAD_TABLE}").fetchall() - assert data == [("john", 1, "x"), ("paul", 2, None)] - - -@pytest.mark.usefixtures("setup_csv_upload") -@pytest.mark.usefixtures("create_excel_files") -@mock.patch("superset.db_engine_specs.hive.upload_to_s3", mock_upload_to_s3) -@mock.patch("superset.views.database.views.event_logger.log_with_context") -def test_import_excel(mock_event_logger): - if utils.backend() == "hive": - pytest.skip("Hive doesn't excel upload.") - - schema = utils.get_example_default_schema() - full_table_name = f"{schema}.{EXCEL_UPLOAD_TABLE}" if schema else EXCEL_UPLOAD_TABLE - test_db = get_upload_db() - - success_msg = f'Excel file "{EXCEL_FILENAME}" uploaded to table "{full_table_name}"' - - # initial upload with fail mode - resp = upload_excel(EXCEL_FILENAME, EXCEL_UPLOAD_TABLE) - assert success_msg in resp - mock_event_logger.assert_called_with( - action="successful_excel_upload", - database=test_db.name, - schema=schema, - table=EXCEL_UPLOAD_TABLE, - ) - - # ensure user is assigned as an owner - table = SupersetTestCase.get_table(name=EXCEL_UPLOAD_TABLE) - assert security_manager.find_user("admin") in table.owners - - # upload again with fail mode; should fail - fail_msg = f'Unable to upload Excel file "{EXCEL_FILENAME}" to table "{EXCEL_UPLOAD_TABLE}"' - resp = upload_excel(EXCEL_FILENAME, EXCEL_UPLOAD_TABLE) - assert fail_msg in resp - - if utils.backend() != "hive": - # upload again with append mode - resp = upload_excel( - EXCEL_FILENAME, EXCEL_UPLOAD_TABLE, extra={"if_exists": "append"} - ) - assert success_msg in resp - - # upload again with replace mode - resp = upload_excel( - EXCEL_FILENAME, EXCEL_UPLOAD_TABLE, extra={"if_exists": "replace"} - ) - assert success_msg in resp - mock_event_logger.assert_called_with( - action="successful_excel_upload", - database=test_db.name, - schema=schema, - table=EXCEL_UPLOAD_TABLE, - ) - - # make sure that john and empty string are replaced with None - data = ( - test_db.get_sqla_engine() - .execute(f"SELECT * from {EXCEL_UPLOAD_TABLE}") - .fetchall() - ) - assert data == [(0, "john", 1), (1, "paul", 2)] - - -@pytest.mark.usefixtures("setup_csv_upload") -@pytest.mark.usefixtures("create_columnar_files") -@mock.patch("superset.db_engine_specs.hive.upload_to_s3", mock_upload_to_s3) -@mock.patch("superset.views.database.views.event_logger.log_with_context") -def test_import_parquet(mock_event_logger): - if utils.backend() == "hive": - pytest.skip("Hive doesn't allow parquet upload.") - - schema = utils.get_example_default_schema() - full_table_name = ( - f"{schema}.{PARQUET_UPLOAD_TABLE}" if schema else PARQUET_UPLOAD_TABLE - ) - test_db = get_upload_db() - - success_msg_f1 = f'Columnar file "[\'{PARQUET_FILENAME1}\']" uploaded to table "{full_table_name}"' - - # initial upload with fail mode - resp = upload_columnar(PARQUET_FILENAME1, PARQUET_UPLOAD_TABLE) - assert success_msg_f1 in resp - - # upload again with fail mode; should fail - fail_msg = f'Unable to upload Columnar file "[\'{PARQUET_FILENAME1}\']" to table "{PARQUET_UPLOAD_TABLE}"' - resp = upload_columnar(PARQUET_FILENAME1, PARQUET_UPLOAD_TABLE) - assert fail_msg in resp - - if utils.backend() != "hive": - # upload again with append mode - resp = upload_columnar( - PARQUET_FILENAME1, PARQUET_UPLOAD_TABLE, extra={"if_exists": "append"} - ) - assert success_msg_f1 in resp - mock_event_logger.assert_called_with( - action="successful_columnar_upload", - database=test_db.name, - schema=schema, - table=PARQUET_UPLOAD_TABLE, - ) - - # upload again with replace mode and specific columns - resp = upload_columnar( - PARQUET_FILENAME1, - PARQUET_UPLOAD_TABLE, - extra={"if_exists": "replace", "usecols": '["a"]'}, - ) - assert success_msg_f1 in resp - - table = SupersetTestCase.get_table(name=PARQUET_UPLOAD_TABLE, schema=None) - # make sure only specified column name was read - assert "b" not in table.column_names - - # ensure user is assigned as an owner - assert security_manager.find_user("admin") in table.owners - - # upload again with replace mode - resp = upload_columnar( - PARQUET_FILENAME1, PARQUET_UPLOAD_TABLE, extra={"if_exists": "replace"} - ) - assert success_msg_f1 in resp - - data = ( - test_db.get_sqla_engine() - .execute(f"SELECT * from {PARQUET_UPLOAD_TABLE} ORDER BY b") - .fetchall() - ) - assert data == [("john", 1), ("paul", 2)] - - # replace table with zip file - resp = upload_columnar( - ZIP_FILENAME, PARQUET_UPLOAD_TABLE, extra={"if_exists": "replace"} - ) - success_msg_f2 = ( - f'Columnar file "[\'{ZIP_FILENAME}\']" uploaded to table "{full_table_name}"' - ) - assert success_msg_f2 in resp - - data = ( - test_db.get_sqla_engine() - .execute(f"SELECT * from {PARQUET_UPLOAD_TABLE} ORDER BY b") - .fetchall() - ) - assert data == [("john", 1), ("paul", 2), ("max", 3), ("bob", 4)] diff --git a/tests/integration_tests/dashboards/security/base_case.py b/tests/integration_tests/dashboards/security/base_case.py index 75e8772b59a4f..bbb5fad831166 100644 --- a/tests/integration_tests/dashboards/security/base_case.py +++ b/tests/integration_tests/dashboards/security/base_case.py @@ -16,6 +16,7 @@ # under the License. from typing import List, Optional +import pytest from flask import escape, Response from superset.models.dashboard import Dashboard @@ -32,31 +33,6 @@ def assert_dashboard_api_response( self.assert200(response) assert response.json["id"] == dashboard_to_access.id - def assert_dashboards_list_view_response( - self, - response: Response, - expected_counts: int, - expected_dashboards: Optional[List[Dashboard]] = None, - not_expected_dashboards: Optional[List[Dashboard]] = None, - ) -> None: - self.assert200(response) - response_html = response.data.decode("utf-8") - if expected_counts == 0: - assert "No records found" in response_html - else: - # # a way to parse number of dashboards returns - # in the list view as an html response - assert ( - "Record Count: {count}".format(count=str(expected_counts)) - in response_html - ) - expected_dashboards = expected_dashboards or [] - for dashboard in expected_dashboards: - assert dashboard.url in response_html - not_expected_dashboards = not_expected_dashboards or [] - for dashboard in not_expected_dashboards: - assert dashboard.url not in response_html - def assert_dashboards_api_response( self, response: Response, diff --git a/tests/integration_tests/dashboards/security/security_rbac_tests.py b/tests/integration_tests/dashboards/security/security_rbac_tests.py index 5a1f02f1e3243..62b0e1b4c2755 100644 --- a/tests/integration_tests/dashboards/security/security_rbac_tests.py +++ b/tests/integration_tests/dashboards/security/security_rbac_tests.py @@ -198,36 +198,6 @@ def test_get_dashboard_view__public_user_access_with_dashboard_permission(self): # post revoke_access_to_dashboard(dashboard_to_access, "Public") - def test_get_dashboards_list__admin_get_all_dashboards(self): - # arrange - create_dashboard_to_db( - owners=[], slices=[create_slice_to_db()], published=False - ) - dashboard_counts = count_dashboards() - - self.login("admin") - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response(response, dashboard_counts) - - def test_get_dashboards_list__owner_get_all_owned_dashboards(self): - # arrange - ( - not_owned_dashboards, - owned_dashboards, - ) = self._create_sample_dashboards_with_owner_access() - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response( - response, 2, owned_dashboards, not_owned_dashboards - ) - def _create_sample_dashboards_with_owner_access(self): username = random_str() new_role = f"role_{random_str()}" @@ -251,42 +221,6 @@ def _create_sample_dashboards_with_owner_access(self): self.login(username) return not_owned_dashboards, owned_dashboards - def test_get_dashboards_list__user_without_any_permissions_get_empty_list(self): - - # arrange - username = random_str() - new_role = f"role_{random_str()}" - self.create_user_with_roles(username, [new_role], should_create_roles=True) - - create_dashboard_to_db(published=True) - self.login(username) - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response(response, 0) - - def test_get_dashboards_list__user_get_only_published_permitted_dashboards(self): - # arrange - ( - new_role, - draft_dashboards, - published_dashboards, - ) = self._create_sample_only_published_dashboard_with_roles() - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response( - response, len(published_dashboards), published_dashboards, draft_dashboards, - ) - - # post - for dash in published_dashboards + draft_dashboards: - revoke_access_to_dashboard(dash, new_role) - def _create_sample_only_published_dashboard_with_roles(self): username = random_str() new_role = f"role_{random_str()}" @@ -304,49 +238,6 @@ def _create_sample_only_published_dashboard_with_roles(self): self.login(username) return new_role, draft_dashboards, published_dashboards - @pytest.mark.usefixtures("public_role_like_gamma") - def test_get_dashboards_list__public_user_without_any_permissions_get_empty_list( - self, - ): - create_dashboard_to_db(published=True) - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response(response, 0) - - @pytest.mark.usefixtures("public_role_like_gamma") - def test_get_dashboards_list__public_user_get_only_published_permitted_dashboards( - self, - ): - # arrange - published_dashboards = [ - create_dashboard_to_db(published=True), - create_dashboard_to_db(published=True), - ] - draft_dashboards = [ - create_dashboard_to_db(published=False), - create_dashboard_to_db(published=False), - ] - - for dash in published_dashboards + draft_dashboards: - grant_access_to_dashboard(dash, "Public") - - self.logout() - - # act - response = self.get_dashboards_list_response() - - # assert - self.assert_dashboards_list_view_response( - response, len(published_dashboards), published_dashboards, draft_dashboards, - ) - - # post - for dash in published_dashboards + draft_dashboards: - revoke_access_to_dashboard(dash, "Public") - def test_get_dashboards_api__admin_get_all_dashboards(self): # arrange create_dashboard_to_db( diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index 7626de677bf01..fe493a5504aed 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -290,12 +290,11 @@ def pg_test_query_parameter(query_parameter, expected_response): ) ) schema_values = [ - "admin_database", "information_schema", "public", ] expected_response = { - "count": 3, + "count": 2, "result": [{"text": val, "value": val} for val in schema_values], } self.login(username="admin") @@ -321,8 +320,10 @@ def pg_test_query_parameter(query_parameter, expected_response): pg_test_query_parameter( query_parameter, { - "count": 3, - "result": [{"text": "admin_database", "value": "admin_database"}], + "count": 2, + "result": [ + {"text": "information_schema", "value": "information_schema"} + ], }, ) diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index d54e400817e62..d8f608ecfc2b3 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -553,24 +553,6 @@ def test_gamma_user_schema_access_to_dashboards(self): self.assertIn("/superset/dashboard/world_health/", data) self.assertNotIn("/superset/dashboard/births/", data) - def test_gamma_user_schema_access_to_tables(self): - self.login(username="gamma") - data = str(self.client.get("tablemodelview/list/").data) - self.assertIn("wb_health_population", data) - self.assertNotIn("birth_names", data) - - @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") - def test_gamma_user_schema_access_to_charts(self): - self.login(username="gamma") - data = str(self.client.get("api/v1/chart/").data) - self.assertIn( - "Life Expectancy VS Rural %", data - ) # wb_health_population slice, has access - self.assertIn( - "Parallel Coordinates", data - ) # wb_health_population slice, has access - self.assertNotIn("Girl Name Cloud", data) # birth_names slice, no access - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("public_role_like_gamma") def test_public_sync_role_data_perms(self): diff --git a/tests/integration_tests/superset_test_config.py b/tests/integration_tests/superset_test_config.py index 7c862328294b5..983476490fe13 100644 --- a/tests/integration_tests/superset_test_config.py +++ b/tests/integration_tests/superset_test_config.py @@ -61,7 +61,6 @@ "KV_STORE": True, "SHARE_QUERIES_VIA_KV_STORE": True, "ENABLE_TEMPLATE_PROCESSING": True, - "ENABLE_REACT_CRUD_VIEWS": os.environ.get("ENABLE_REACT_CRUD_VIEWS", False), "ALERT_REPORTS": True, "DASHBOARD_NATIVE_FILTERS": True, } diff --git a/tox.ini b/tox.ini index 88a51278ec2ae..774aa681b6396 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,6 @@ setenv = SUPERSET_TESTENV = true SUPERSET_CONFIG = tests.integration_tests.superset_test_config SUPERSET_HOME = {envtmpdir} - ENABLE_REACT_CRUD_VIEWS = true commands = npm install -g npm@'>=6.5.0' pip install -e {toxinidir}/ @@ -70,7 +69,6 @@ setenv = SUPERSET_TESTENV = true SUPERSET_CONFIG = tests.integration_tests.superset_test_config SUPERSET_HOME = {envtmpdir} - ENABLE_REACT_CRUD_VIEWS = true commands = npm install -g npm@'>=6.5.0' pip install -e {toxinidir}/ @@ -84,7 +82,6 @@ setenv = SUPERSET_TESTENV = true SUPERSET_CONFIG = tests.integration_tests.superset_test_config SUPERSET_HOME = {envtmpdir} - ENABLE_REACT_CRUD_VIEWS = true commands = npm install -g npm@'>=6.5.0' pip install -e {toxinidir}/ @@ -98,7 +95,6 @@ setenv = SUPERSET_TESTENV = true SUPERSET_CONFIG = tests.integration_tests.superset_test_config SUPERSET_HOME = {envtmpdir} - ENABLE_REACT_CRUD_VIEWS = true commands = npm install -g npm@'>=6.5.0' pip install -e {toxinidir}/ @@ -112,7 +108,6 @@ setenv = SUPERSET_TESTENV = true SUPERSET_CONFIG = tests.integration_tests.superset_test_config SUPERSET_HOME = {envtmpdir} - ENABLE_REACT_CRUD_VIEWS = true commands = npm install -g npm@'>=6.5.0' pip install -e {toxinidir}/ From f341025d80aacf7345e7c20f8463231b9197ea58 Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Sat, 19 Mar 2022 00:08:06 +0200 Subject: [PATCH 22/29] feat: add support for comments in adhoc clauses (#19248) * feat: add support for comments in adhoc clauses * sanitize remaining freeform clauses * sanitize adhoc having in frontend * address review comment --- .../src/query/processFilters.ts | 12 ++++- .../test/query/processFilters.test.ts | 10 ++-- superset/common/query_object.py | 10 ++-- superset/connectors/sqla/models.py | 23 +++++--- superset/sql_parse.py | 21 ++++++-- superset/utils/core.py | 7 ++- superset/viz.py | 10 ++-- .../charts/data/api_tests.py | 22 ++++++++ tests/unit_tests/sql_parse_tests.py | 54 +++++++++---------- 9 files changed, 109 insertions(+), 60 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/query/processFilters.ts b/superset-frontend/packages/superset-ui-core/src/query/processFilters.ts index 8ead77c0fc34b..239f1c49afbe5 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/processFilters.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/processFilters.ts @@ -23,6 +23,14 @@ import { QueryObjectFilterClause } from './types/Query'; import { isSimpleAdhocFilter } from './types/Filter'; import convertFilter from './convertFilter'; +function sanitizeClause(clause: string): string { + let sanitizedClause = clause; + if (clause.includes('--')) { + sanitizedClause = `${clause}\n`; + } + return `(${sanitizedClause})`; +} + /** Logic formerly in viz.py's process_query_filters */ export default function processFilters( formData: Partial, @@ -60,9 +68,9 @@ export default function processFilters( }); // some filter-related fields need to go in `extras` - extras.having = freeformHaving.map(exp => `(${exp})`).join(' AND '); + extras.having = freeformHaving.map(sanitizeClause).join(' AND '); extras.having_druid = simpleHaving; - extras.where = freeformWhere.map(exp => `(${exp})`).join(' AND '); + extras.where = freeformWhere.map(sanitizeClause).join(' AND '); return { filters: simpleWhere, diff --git a/superset-frontend/packages/superset-ui-core/test/query/processFilters.test.ts b/superset-frontend/packages/superset-ui-core/test/query/processFilters.test.ts index 267b416493e35..151c0363f16f0 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/processFilters.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/processFilters.test.ts @@ -132,12 +132,12 @@ describe('processFilters', () => { { expressionType: 'SQL', clause: 'WHERE', - sqlExpression: 'tea = "jasmine"', + sqlExpression: "tea = 'jasmine'", }, { expressionType: 'SQL', clause: 'WHERE', - sqlExpression: 'cup = "large"', + sqlExpression: "cup = 'large' -- comment", }, { expressionType: 'SQL', @@ -147,13 +147,13 @@ describe('processFilters', () => { { expressionType: 'SQL', clause: 'HAVING', - sqlExpression: 'waitTime <= 180', + sqlExpression: 'waitTime <= 180 -- comment', }, ], }), ).toEqual({ extras: { - having: '(ice = 25 OR ice = 50) AND (waitTime <= 180)', + having: '(ice = 25 OR ice = 50) AND (waitTime <= 180 -- comment\n)', having_druid: [ { col: 'sweetness', @@ -166,7 +166,7 @@ describe('processFilters', () => { val: '50', }, ], - where: '(tea = "jasmine") AND (cup = "large")', + where: "(tea = 'jasmine') AND (cup = 'large' -- comment\n)", }, filters: [ { diff --git a/superset/common/query_object.py b/superset/common/query_object.py index fd988a36fac05..139dc27c580ef 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -30,7 +30,7 @@ QueryClauseValidationException, QueryObjectValidationError, ) -from superset.sql_parse import validate_filter_clause +from superset.sql_parse import sanitize_clause from superset.superset_typing import Column, Metric, OrderBy from superset.utils import pandas_postprocessing from superset.utils.core import ( @@ -272,7 +272,7 @@ def validate( try: self._validate_there_are_no_missing_series() self._validate_no_have_duplicate_labels() - self._validate_filters() + self._sanitize_filters() return None except QueryObjectValidationError as ex: if raise_exceptions: @@ -291,12 +291,14 @@ def _validate_no_have_duplicate_labels(self) -> None: ) ) - def _validate_filters(self) -> None: + def _sanitize_filters(self) -> None: for param in ("where", "having"): clause = self.extras.get(param) if clause: try: - validate_filter_clause(clause) + sanitized_clause = sanitize_clause(clause) + if sanitized_clause != clause: + self.extras[param] = sanitized_clause except QueryClauseValidationException as ex: raise QueryObjectValidationError(ex.message) from ex diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 23d3d326cd9a5..9261f97ae1b11 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -82,7 +82,10 @@ ) from superset.datasets.models import Dataset as NewDataset from superset.db_engine_specs.base import BaseEngineSpec, CTE_ALIAS, TimestampExpression -from superset.exceptions import QueryObjectValidationError +from superset.exceptions import ( + QueryClauseValidationException, + QueryObjectValidationError, +) from superset.jinja_context import ( BaseTemplateProcessor, ExtraCache, @@ -96,7 +99,7 @@ clone_model, QueryResult, ) -from superset.sql_parse import ParsedQuery +from superset.sql_parse import ParsedQuery, sanitize_clause from superset.superset_typing import ( AdhocColumn, AdhocMetric, @@ -887,6 +890,10 @@ def adhoc_metric_to_sqla( tp = self.get_template_processor() expression = tp.process_template(cast(str, metric["sqlExpression"])) validate_adhoc_subquery(expression) + try: + expression = sanitize_clause(expression) + except QueryClauseValidationException as ex: + raise QueryObjectValidationError(ex.message) from ex sqla_metric = literal_column(expression) else: raise QueryObjectValidationError("Adhoc metric expressionType is invalid") @@ -912,6 +919,10 @@ def adhoc_column_to_sqla( expression = template_processor.process_template(expression) if expression: validate_adhoc_subquery(expression) + try: + expression = sanitize_clause(expression) + except QueryClauseValidationException as ex: + raise QueryObjectValidationError(ex.message) from ex sqla_metric = literal_column(expression) return self.make_sqla_column_compatible(sqla_metric, label) @@ -1353,7 +1364,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma where = extras.get("where") if where: try: - where = template_processor.process_template(where) + where = template_processor.process_template(f"({where})") except TemplateError as ex: raise QueryObjectValidationError( _( @@ -1361,11 +1372,11 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma msg=ex.message, ) ) from ex - where_clause_and += [self.text(f"({where})")] + where_clause_and += [self.text(where)] having = extras.get("having") if having: try: - having = template_processor.process_template(having) + having = template_processor.process_template(f"({having})") except TemplateError as ex: raise QueryObjectValidationError( _( @@ -1373,7 +1384,7 @@ def get_sqla_query( # pylint: disable=too-many-arguments,too-many-locals,too-ma msg=ex.message, ) ) from ex - having_clause_and += [self.text(f"({having})")] + having_clause_and += [self.text(having)] if apply_fetch_values_predicate and self.fetch_values_predicate: qry = qry.where(self.get_fetch_values_predicate()) if granularity: diff --git a/superset/sql_parse.py b/superset/sql_parse.py index f5523bab71e8d..95361b39a6a27 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -32,6 +32,7 @@ Where, ) from sqlparse.tokens import ( + Comment, CTE, DDL, DML, @@ -441,25 +442,35 @@ def set_or_update_query_limit(self, new_limit: int, force: bool = False) -> str: return str_res -def validate_filter_clause(clause: str) -> None: - if sqlparse.format(clause, strip_comments=True) != sqlparse.format(clause): - raise QueryClauseValidationException("Filter clause contains comment") - +def sanitize_clause(clause: str) -> str: + # clause = sqlparse.format(clause, strip_comments=True) statements = sqlparse.parse(clause) if len(statements) != 1: - raise QueryClauseValidationException("Filter clause contains multiple queries") + raise QueryClauseValidationException("Clause contains multiple statements") open_parens = 0 + previous_token = None for token in statements[0]: + if token.value == "/" and previous_token and previous_token.value == "*": + raise QueryClauseValidationException("Closing unopened multiline comment") + if token.value == "*" and previous_token and previous_token.value == "/": + raise QueryClauseValidationException("Unclosed multiline comment") if token.value in (")", "("): open_parens += 1 if token.value == "(" else -1 if open_parens < 0: raise QueryClauseValidationException( "Closing unclosed parenthesis in filter clause" ) + previous_token = token if open_parens > 0: raise QueryClauseValidationException("Unclosed parenthesis in filter clause") + if previous_token and previous_token.ttype in Comment: + if previous_token.value[-1] != "\n": + clause = f"{clause}\n" + + return clause + class InsertRLSState(str, Enum): """ diff --git a/superset/utils/core.py b/superset/utils/core.py index 36d59333d2ed3..d2527a32c72cf 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -98,6 +98,7 @@ SupersetException, SupersetTimeoutException, ) +from superset.sql_parse import sanitize_clause from superset.superset_typing import ( AdhocColumn, AdhocMetric, @@ -1366,10 +1367,12 @@ def split_adhoc_filters_into_base_filters( # pylint: disable=invalid-name } ) elif expression_type == "SQL": + sql_expression = adhoc_filter.get("sqlExpression") + sql_expression = sanitize_clause(sql_expression) if clause == "WHERE": - sql_where_filters.append(adhoc_filter.get("sqlExpression")) + sql_where_filters.append(sql_expression) elif clause == "HAVING": - sql_having_filters.append(adhoc_filter.get("sqlExpression")) + sql_having_filters.append(sql_expression) form_data["where"] = " AND ".join( ["({})".format(sql) for sql in sql_where_filters] ) diff --git a/superset/viz.py b/superset/viz.py index 4998da12dcd57..7544af5078059 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -62,14 +62,13 @@ from superset.exceptions import ( CacheLoadError, NullValueException, - QueryClauseValidationException, QueryObjectValidationError, SpatialException, SupersetSecurityException, ) from superset.extensions import cache_manager, security_manager from superset.models.helpers import QueryResult -from superset.sql_parse import validate_filter_clause +from superset.sql_parse import sanitize_clause from superset.superset_typing import ( Column, Metric, @@ -391,10 +390,9 @@ def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals for param in ("where", "having"): clause = self.form_data.get(param) if clause: - try: - validate_filter_clause(clause) - except QueryClauseValidationException as ex: - raise QueryObjectValidationError(ex.message) from ex + sanitized_clause = sanitize_clause(clause) + if sanitized_clause != clause: + self.form_data[param] = sanitized_clause # extras are used to query elements specific to a datasource type # for instance the extra where clause that applies only to Tables diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py index bc8ec74feb0d6..c45c8d5064eaa 100644 --- a/tests/integration_tests/charts/data/api_tests.py +++ b/tests/integration_tests/charts/data/api_tests.py @@ -465,6 +465,28 @@ def test_with_invalid_where_parameter_closing_unclosed__400(self): assert rv.status_code == 400 + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_with_where_parameter_including_comment___200(self): + self.query_context_payload["queries"][0]["filters"] = [] + self.query_context_payload["queries"][0]["extras"]["where"] = "1 = 1 -- abc" + + rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data") + + assert rv.status_code == 200 + + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_with_orderby_parameter_with_second_query__400(self): + self.query_context_payload["queries"][0]["filters"] = [] + self.query_context_payload["queries"][0]["orderby"] = [ + [ + {"expressionType": "SQL", "sqlExpression": "sum__num; select 1, 1",}, + True, + ], + ] + rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data") + + assert rv.status_code == 400 + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_with_invalid_having_parameter_closing_and_comment__400(self): self.query_context_payload["queries"][0]["filters"] = [] diff --git a/tests/unit_tests/sql_parse_tests.py b/tests/unit_tests/sql_parse_tests.py index 886eb368e4aa4..75f099e52b6e1 100644 --- a/tests/unit_tests/sql_parse_tests.py +++ b/tests/unit_tests/sql_parse_tests.py @@ -30,9 +30,9 @@ insert_rls, matches_table_name, ParsedQuery, + sanitize_clause, strip_comments_from_sql, Table, - validate_filter_clause, ) @@ -1142,52 +1142,46 @@ def test_strip_comments_from_sql() -> None: ) -def test_validate_filter_clause_valid(): +def test_sanitize_clause_valid(): # regular clauses - assert validate_filter_clause("col = 1") is None - assert validate_filter_clause("1=\t\n1") is None - assert validate_filter_clause("(col = 1)") is None - assert validate_filter_clause("(col1 = 1) AND (col2 = 2)") is None + assert sanitize_clause("col = 1") == "col = 1" + assert sanitize_clause("1=\t\n1") == "1=\t\n1" + assert sanitize_clause("(col = 1)") == "(col = 1)" + assert sanitize_clause("(col1 = 1) AND (col2 = 2)") == "(col1 = 1) AND (col2 = 2)" + assert sanitize_clause("col = 'abc' -- comment") == "col = 'abc' -- comment\n" - # Valid literal values that appear to be invalid - assert validate_filter_clause("col = 'col1 = 1) AND (col2 = 2'") is None - assert validate_filter_clause("col = 'select 1; select 2'") is None - assert validate_filter_clause("col = 'abc -- comment'") is None - - -def test_validate_filter_clause_closing_unclosed(): - with pytest.raises(QueryClauseValidationException): - validate_filter_clause("col1 = 1) AND (col2 = 2)") - - -def test_validate_filter_clause_unclosed(): - with pytest.raises(QueryClauseValidationException): - validate_filter_clause("(col1 = 1) AND (col2 = 2") + # Valid literal values that at could be flagged as invalid by a naive query parser + assert ( + sanitize_clause("col = 'col1 = 1) AND (col2 = 2'") + == "col = 'col1 = 1) AND (col2 = 2'" + ) + assert sanitize_clause("col = 'select 1; select 2'") == "col = 'select 1; select 2'" + assert sanitize_clause("col = 'abc -- comment'") == "col = 'abc -- comment'" -def test_validate_filter_clause_closing_and_unclosed(): +def test_sanitize_clause_closing_unclosed(): with pytest.raises(QueryClauseValidationException): - validate_filter_clause("col1 = 1) AND (col2 = 2") + sanitize_clause("col1 = 1) AND (col2 = 2)") -def test_validate_filter_clause_closing_and_unclosed_nested(): +def test_sanitize_clause_unclosed(): with pytest.raises(QueryClauseValidationException): - validate_filter_clause("(col1 = 1)) AND ((col2 = 2)") + sanitize_clause("(col1 = 1) AND (col2 = 2") -def test_validate_filter_clause_multiple(): +def test_sanitize_clause_closing_and_unclosed(): with pytest.raises(QueryClauseValidationException): - validate_filter_clause("TRUE; SELECT 1") + sanitize_clause("col1 = 1) AND (col2 = 2") -def test_validate_filter_clause_comment(): +def test_sanitize_clause_closing_and_unclosed_nested(): with pytest.raises(QueryClauseValidationException): - validate_filter_clause("1 = 1 -- comment") + sanitize_clause("(col1 = 1)) AND ((col2 = 2)") -def test_validate_filter_clause_subquery_comment(): +def test_sanitize_clause_multiple(): with pytest.raises(QueryClauseValidationException): - validate_filter_clause("(1 = 1 -- comment\n)") + sanitize_clause("TRUE; SELECT 1") def test_sqlparse_issue_652(): From d645579cdd64b7fe7f9dde4a8da000dd7db51ce9 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Fri, 18 Mar 2022 16:01:27 -0700 Subject: [PATCH 23/29] chore!: update mutator to take kwargs (#19083) * update mutator to take kwargs * update updating.md * lint * test that the database name is properly passed in to the mutator --- UPDATING.md | 4 +++- superset/config.py | 11 +++++----- superset/connectors/sqla/models.py | 7 ++++++- superset/db_engine_specs/base.py | 7 ++++++- superset/sql_lab.py | 4 +++- superset/sql_validators/presto_db.py | 7 ++++++- tests/integration_tests/model_tests.py | 29 +++++++++++++++++++++++++- 7 files changed, 57 insertions(+), 12 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index 4272ecc56bb32..c28975387efe3 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -37,7 +37,9 @@ assists people when migrating to a new version. - [17984](/~https://github.com/apache/superset/pull/17984): Default Flask SECRET_KEY has changed for security reasons. You should always override with your own secret. Set `PREVIOUS_SECRET_KEY` (ex: PREVIOUS_SECRET_KEY = "\2\1thisismyscretkey\1\2\\e\\y\\y\\h") with your previous key and use `superset re-encrypt-secrets` to rotate you current secrets - [15254](/~https://github.com/apache/superset/pull/15254): Previously `QUERY_COST_FORMATTERS_BY_ENGINE`, `SQL_VALIDATORS_BY_ENGINE` and `SCHEDULED_QUERIES` were expected to be defined in the feature flag dictionary in the `config.py` file. These should now be defined as a top-level config, with the feature flag dictionary being reserved for boolean only values. - [17539](/~https://github.com/apache/superset/pull/17539): all Superset CLI commands (init, load_examples and etc) require setting the FLASK_APP environment variable (which is set by default when `.flaskenv` is loaded) -- [18970](/~https://github.com/apache/superset/pull/18970): Changes feature flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in config.py to True, thus disabling the feature from being shown in the client. +- [18970](/~https://github.com/apache/superset/pull/18970): Changes feature +flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in config.py to True, thus disabling the feature from being shown in the client. +- [19083](/~https://github.com/apache/superset/pull/19083): Updates the mutator function in the config file to take a sql argument and a list of kwargs. Any `SQL_QUERY_MUTATOR` config function overrides will need to be updated to match the new set of params. It is advised regardless of the dictionary args that you list in your function arguments, to keep **kwargs as the last argument to allow for any new kwargs to be passed in. - [19017](/~https://github.com/apache/superset/pull/19017): Removes Python 3.7 support. - [19142](/~https://github.com/apache/superset/pull/19142): Changes feature flag for versioned export(VERSIONED_EXPORT) to be true. - [19107](/~https://github.com/apache/superset/pull/19107): Feature flag `SQLLAB_BACKEND_PERSISTENCE` is now on by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`. diff --git a/superset/config.py b/superset/config.py index dc93ad7f5de17..86e35be718702 100644 --- a/superset/config.py +++ b/superset/config.py @@ -40,7 +40,6 @@ from flask_appbuilder.security.manager import AUTH_DB from pandas._libs.parsers import STR_NA_VALUES # pylint: disable=no-name-in-module from typing_extensions import Literal -from werkzeug.local import LocalProxy from superset.constants import CHANGE_ME_SECRET_KEY from superset.jinja_context import BaseTemplateProcessor @@ -1048,14 +1047,14 @@ def CSV_TO_HIVE_UPLOAD_DIRECTORY_FUNC( # pylint: disable=invalid-name # The use case is can be around adding some sort of comment header # with information such as the username and worker node information # -# def SQL_QUERY_MUTATOR(sql, user_name, security_manager, database): +# def SQL_QUERY_MUTATOR(sql, user_name=user_name, security_manager=security_manager, database=database): # dttm = datetime.now().isoformat() # return f"-- [SQL LAB] {username} {dttm}\n{sql}" +# For backward compatibility, you can unpack any of the above arguments in your +# function definition, but keep the **kwargs as the last argument to allow new args +# to be added later without any errors. def SQL_QUERY_MUTATOR( # pylint: disable=invalid-name,unused-argument - sql: str, - user_name: Optional[str], - security_manager: LocalProxy, - database: "Database", + sql: str, **kwargs: Any ) -> str: return sql diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 9261f97ae1b11..62ae8c9ebaf95 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -776,7 +776,12 @@ def mutate_query_from_config(self, sql: str) -> str: sql_query_mutator = config["SQL_QUERY_MUTATOR"] if sql_query_mutator: username = utils.get_username() - sql = sql_query_mutator(sql, username, security_manager, self.database) + sql = sql_query_mutator( + sql, + user_name=username, + security_manager=security_manager, + database=self.database, + ) return sql def get_template_processor(self, **kwargs: Any) -> BaseTemplateProcessor: diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index d7e457baa8c01..e867f200d91a8 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -1138,7 +1138,12 @@ def process_statement( sql = parsed_query.stripped() sql_query_mutator = current_app.config["SQL_QUERY_MUTATOR"] if sql_query_mutator: - sql = sql_query_mutator(sql, user_name, security_manager, database) + sql = sql_query_mutator( + sql, + user_name=user_name, + security_manager=security_manager, + database=database, + ) return sql diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 8fac419cf0ba6..613db963e31c1 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -225,7 +225,9 @@ def execute_sql_statement( # pylint: disable=too-many-arguments,too-many-locals sql = apply_limit_if_exists(database, increased_limit, query, sql) # Hook to allow environment-specific mutation (usually comments) to the SQL - sql = SQL_QUERY_MUTATOR(sql, user_name, security_manager, database) + sql = SQL_QUERY_MUTATOR( + sql, user_name=user_name, security_manager=security_manager, database=database + ) try: query.executed_sql = sql if log_query: diff --git a/superset/sql_validators/presto_db.py b/superset/sql_validators/presto_db.py index bf77f474a0004..6d0311f120efb 100644 --- a/superset/sql_validators/presto_db.py +++ b/superset/sql_validators/presto_db.py @@ -55,7 +55,12 @@ def validate_statement( # Hook to allow environment-specific mutation (usually comments) to the SQL sql_query_mutator = config["SQL_QUERY_MUTATOR"] if sql_query_mutator: - sql = sql_query_mutator(sql, user_name, security_manager, database) + sql = sql_query_mutator( + sql, + user_name=user_name, + security_manager=security_manager, + database=database, + ) # Transform the final statement to an explain call before sending it on # to presto to validate diff --git a/tests/integration_tests/model_tests.py b/tests/integration_tests/model_tests.py index 5ffa65e583ead..c6388601354d4 100644 --- a/tests/integration_tests/model_tests.py +++ b/tests/integration_tests/model_tests.py @@ -506,7 +506,7 @@ def test_sql_mutator(self): sql = tbl.get_query_str(query_obj) self.assertNotIn("-- COMMENT", sql) - def mutator(*args): + def mutator(*args, **kwargs): return "-- COMMENT\n" + args[0] app.config["SQL_QUERY_MUTATOR"] = mutator @@ -515,6 +515,33 @@ def mutator(*args): app.config["SQL_QUERY_MUTATOR"] = None + @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") + def test_sql_mutator_different_params(self): + tbl = self.get_table(name="birth_names") + query_obj = dict( + groupby=[], + metrics=None, + filter=[], + is_timeseries=False, + columns=["name"], + granularity=None, + from_dttm=None, + to_dttm=None, + extras={}, + ) + sql = tbl.get_query_str(query_obj) + self.assertNotIn("-- COMMENT", sql) + + def mutator(sql, database=None, **kwargs): + return "-- COMMENT\n--" + "\n" + str(database) + "\n" + sql + + app.config["SQL_QUERY_MUTATOR"] = mutator + mutated_sql = tbl.get_query_str(query_obj) + self.assertIn("-- COMMENT", mutated_sql) + self.assertIn(tbl.database.name, mutated_sql) + + app.config["SQL_QUERY_MUTATOR"] = None + def test_query_with_non_existent_metrics(self): tbl = self.get_table(name="birth_names") From e1d0b83885d89c50e96c6eef872beffe0de7cf50 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Sun, 20 Mar 2022 21:31:44 -0700 Subject: [PATCH 24/29] chore: update updating with druid no sql deprecation (#19262) * update updating with druid no sql deprecation * Update UPDATING.md Co-authored-by: Beto Dealmeida Co-authored-by: Beto Dealmeida --- UPDATING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPDATING.md b/UPDATING.md index c28975387efe3..953d2c34c400c 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -43,6 +43,7 @@ flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in conf - [19017](/~https://github.com/apache/superset/pull/19017): Removes Python 3.7 support. - [19142](/~https://github.com/apache/superset/pull/19142): Changes feature flag for versioned export(VERSIONED_EXPORT) to be true. - [19107](/~https://github.com/apache/superset/pull/19107): Feature flag `SQLLAB_BACKEND_PERSISTENCE` is now on by default, which enables persisting SQL Lab tabs in the backend instead of the browser's `localStorage`. +- [19262](/~https://github.com/apache/superset/pull/19262): As per SIPs 11 and 68, the native NoSQL Druid connector is deprecated as of 2.0 and will no longer be supported. Druid is still supported through SQLAlchemy via pydruid. ### Potential Downtime From dc575080d7e43d40b1734bb8f44fdc291cb95b11 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Mon, 21 Mar 2022 15:20:04 +0800 Subject: [PATCH 25/29] feat: improve color consistency (save all labels) (#19038) --- .gitignore | 2 + superset-frontend/package-lock.json | 26 +++- superset-frontend/package.json | 2 + .../src/shared-controls/index.tsx | 3 + .../packages/superset-ui-core/package.json | 1 + .../src/color/CategoricalColorScale.ts | 17 ++- .../src/color/SharedLabelColorSingleton.ts | 130 ++++++++++++++++++ .../superset-ui-core/src/color/index.ts | 4 + .../color/SharedLabelColorSingleton.test.ts | 110 +++++++++++++++ .../legacy-plugin-chart-chord/src/Chord.js | 6 +- .../src/transformProps.js | 3 +- .../src/CountryMap.js | 21 ++- .../src/transformProps.js | 10 +- .../src/Histogram.jsx | 3 +- .../src/transformProps.js | 2 + .../src/Partition.js | 3 +- .../src/transformProps.js | 2 + .../legacy-plugin-chart-rose/src/Rose.js | 9 +- .../src/transformProps.js | 2 + .../src/SankeyLoop.js | 4 +- .../src/transformProps.js | 3 +- .../legacy-plugin-chart-sankey/src/Sankey.js | 4 +- .../src/transformProps.js | 3 +- .../src/Sunburst.js | 9 +- .../src/transformProps.js | 4 +- .../src/Treemap.js | 3 +- .../src/transformProps.js | 3 +- .../src/WorldMap.js | 24 +++- .../src/controlPanel.ts | 4 + .../src/transformProps.js | 12 +- .../src/CategoricalDeckGLContainer.jsx | 4 +- .../legacy-preset-chart-nvd3/src/NVD3Vis.js | 5 +- .../src/transformProps.js | 2 + .../src/BoxPlot/transformProps.ts | 7 +- .../src/Funnel/transformProps.ts | 3 +- .../src/Gauge/transformProps.ts | 5 +- .../src/Graph/transformProps.ts | 3 +- .../plugin-chart-echarts/src/Graph/types.ts | 43 +++--- .../src/MixedTimeseries/transformProps.ts | 16 ++- .../src/Pie/transformProps.ts | 3 +- .../src/Radar/transformProps.ts | 3 +- .../src/Timeseries/transformProps.ts | 15 +- .../src/Timeseries/transformers.ts | 13 +- .../src/Treemap/transformProps.ts | 5 +- .../src/chart/WordCloud.tsx | 15 +- .../src/legacyPlugin/transformProps.ts | 2 + .../src/plugin/transformProps.ts | 3 +- .../src/components/Chart/Chart.jsx | 2 + .../src/components/Chart/ChartRenderer.jsx | 5 + .../src/dashboard/actions/dashboardInfo.ts | 27 +++- .../src/dashboard/actions/dashboardLayout.js | 6 +- .../src/dashboard/actions/dashboardState.js | 49 ++++++- .../dashboard/actions/dashboardState.test.js | 8 +- .../src/dashboard/actions/hydrate.js | 23 +--- .../src/dashboard/components/Header/index.jsx | 17 ++- .../components/PropertiesModal/index.tsx | 28 +++- .../components/gridComponents/Chart.jsx | 6 + .../components/gridComponents/ChartHolder.jsx | 8 ++ .../src/dashboard/containers/Chart.jsx | 3 + .../containers/DashboardComponent.jsx | 2 + .../dashboard/containers/DashboardPage.tsx | 21 ++- .../src/dashboard/reducers/dashboardState.js | 9 ++ .../dashboard/reducers/dashboardState.test.js | 9 +- .../charts/getFormDataWithExtraFilters.ts | 6 + .../components/ExploreChartHeader/index.jsx | 18 ++- superset/dashboards/dao.py | 1 + superset/dashboards/schemas.py | 1 + .../integration_tests/dashboards/api_tests.py | 2 +- 68 files changed, 690 insertions(+), 137 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts create mode 100644 superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts diff --git a/.gitignore b/.gitignore index 81c44731de5d6..a23cbb9ba5a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ release.json messages.mo docker/requirements-local.txt + +cache/ diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 24c235692a7eb..e54aa9df3ecda 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -136,6 +136,7 @@ "rison": "^0.1.1", "scroll-into-view-if-needed": "^2.2.28", "shortid": "^2.2.6", + "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", @@ -201,6 +202,7 @@ "@types/rison": "0.0.6", "@types/shortid": "^0.0.29", "@types/sinon": "^9.0.5", + "@types/tinycolor2": "^1.4.3", "@types/yargs": "12 - 15", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", @@ -22525,6 +22527,11 @@ "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" }, + "node_modules/@types/tinycolor2": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz", + "integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==" + }, "node_modules/@types/uglify-js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", @@ -53688,9 +53695,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "node_modules/tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==", "engines": { "node": "*" } @@ -58702,6 +58709,7 @@ "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@vx/responsive": "^0.0.199", "csstype": "^2.6.4", "d3-format": "^1.3.2", @@ -75807,6 +75815,7 @@ "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@vx/responsive": "^0.0.199", "csstype": "^2.6.4", "d3-format": "^1.3.2", @@ -77790,6 +77799,11 @@ "resolved": "https://registry.npmjs.org/@types/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-AQ6zewa0ucLJvtUi5HsErbOFKAcQfRLt9zFLlUOvcXBy2G36a+ZDpCHSGdzJVUD8aNURtIjh9aSjCStNMRCcRQ==" }, + "@types/tinycolor2": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz", + "integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==" + }, "@types/uglify-js": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", @@ -102080,9 +102094,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", + "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, "tinyqueue": { "version": "2.0.3", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index d8161d0d7d637..31a95ca672a02 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -196,6 +196,7 @@ "rison": "^0.1.1", "scroll-into-view-if-needed": "^2.2.28", "shortid": "^2.2.6", + "tinycolor2": "^1.4.2", "urijs": "^1.19.8", "use-immer": "^0.6.0", "use-query-params": "^1.1.9", @@ -261,6 +262,7 @@ "@types/rison": "0.0.6", "@types/shortid": "^0.0.29", "@types/sinon": "^9.0.5", + "@types/tinycolor2": "^1.4.3", "@types/yargs": "12 - 15", "@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/parser": "^5.3.0", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index aee3717938c9e..ff15a6f4a92da 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -205,6 +205,9 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = { renderTrigger: true, schemes: () => sequentialSchemeRegistry.getMap(), isLinear: true, + mapStateToProps: state => ({ + dashboardId: state?.form_data?.dashboardId, + }), }; const secondary_metric: SharedControlConfig<'MetricsControl'> = { diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 0b9713bc4346b..28937d298ad6c 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -42,6 +42,7 @@ "@types/math-expression-evaluator": "^1.2.1", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", + "@types/tinycolor2": "^1.4.3", "@types/fetch-mock": "^7.3.3", "@types/enzyme": "^3.10.5", "@types/prop-types": "^15.7.2", diff --git a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts index 63b2cb55f67cd..d34960dac0973 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/CategoricalColorScale.ts @@ -22,12 +22,12 @@ import { scaleOrdinal, ScaleOrdinal } from 'd3-scale'; import { ExtensibleFunction } from '../models'; import { ColorsLookup } from './types'; import stringifyAndTrim from './stringifyAndTrim'; +import getSharedLabelColor from './SharedLabelColorSingleton'; // Use type augmentation to correct the fact that // an instance of CategoricalScale is also a function - interface CategoricalColorScale { - (x: { toString(): string }): string; + (x: { toString(): string }, y?: number): string; } class CategoricalColorScale extends ExtensibleFunction { @@ -46,7 +46,7 @@ class CategoricalColorScale extends ExtensibleFunction { * (usually CategoricalColorNamespace) and supersede this.forcedColors */ constructor(colors: string[], parentForcedColors?: ColorsLookup) { - super((value: string) => this.getColor(value)); + super((value: string, sliceId?: number) => this.getColor(value, sliceId)); this.colors = colors; this.scale = scaleOrdinal<{ toString(): string }, string>(); @@ -55,20 +55,27 @@ class CategoricalColorScale extends ExtensibleFunction { this.forcedColors = {}; } - getColor(value?: string) { + getColor(value?: string, sliceId?: number) { const cleanedValue = stringifyAndTrim(value); + const sharedLabelColor = getSharedLabelColor(); + const parentColor = this.parentForcedColors && this.parentForcedColors[cleanedValue]; if (parentColor) { + sharedLabelColor.addSlice(cleanedValue, parentColor, sliceId); return parentColor; } const forcedColor = this.forcedColors[cleanedValue]; if (forcedColor) { + sharedLabelColor.addSlice(cleanedValue, forcedColor, sliceId); return forcedColor; } - return this.scale(cleanedValue); + const color = this.scale(cleanedValue); + sharedLabelColor.addSlice(cleanedValue, color, sliceId); + + return color; } /** diff --git a/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts new file mode 100644 index 0000000000000..227b565276a94 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/color/SharedLabelColorSingleton.ts @@ -0,0 +1,130 @@ +/* + * 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 tinycolor from 'tinycolor2'; +import { CategoricalColorNamespace } from '.'; +import makeSingleton from '../utils/makeSingleton'; + +export class SharedLabelColor { + sliceLabelColorMap: Record>; + + constructor() { + // { sliceId1: { label1: color1 }, sliceId2: { label2: color2 } } + this.sliceLabelColorMap = {}; + } + + getColorMap( + colorNamespace?: string, + colorScheme?: string, + updateColorScheme?: boolean, + ) { + if (colorScheme) { + const categoricalNamespace = + CategoricalColorNamespace.getNamespace(colorNamespace); + const colors = categoricalNamespace.getScale(colorScheme).range(); + const sharedLabels = this.getSharedLabels(); + const generatedColors: tinycolor.Instance[] = []; + let sharedLabelMap; + + if (sharedLabels.length) { + const multiple = Math.ceil(sharedLabels.length / colors.length); + const ext = 5; + const analogousColors = colors.map(color => { + const result = tinycolor(color).analogous(multiple + ext); + return result.slice(ext); + }); + + // [[A, AA, AAA], [B, BB, BBB]] => [A, B, AA, BB, AAA, BBB] + while (analogousColors[analogousColors.length - 1]?.length) { + analogousColors.forEach(colors => + generatedColors.push(colors.shift() as tinycolor.Instance), + ); + } + sharedLabelMap = sharedLabels.reduce( + (res, label, index) => ({ + ...res, + [label.toString()]: generatedColors[index]?.toHexString(), + }), + {}, + ); + } + + const labelMap = Object.keys(this.sliceLabelColorMap).reduce( + (res, sliceId) => { + const colorScale = categoricalNamespace.getScale(colorScheme); + return { + ...res, + ...Object.keys(this.sliceLabelColorMap[sliceId]).reduce( + (res, label) => ({ + ...res, + [label]: updateColorScheme + ? colorScale(label) + : this.sliceLabelColorMap[sliceId][label], + }), + {}, + ), + }; + }, + {}, + ); + + return { + ...labelMap, + ...sharedLabelMap, + }; + } + return undefined; + } + + addSlice(label: string, color: string, sliceId?: number) { + if (!sliceId) return; + this.sliceLabelColorMap[sliceId] = { + ...this.sliceLabelColorMap[sliceId], + [label]: color, + }; + } + + removeSlice(sliceId: number) { + delete this.sliceLabelColorMap[sliceId]; + } + + clear() { + this.sliceLabelColorMap = {}; + } + + getSharedLabels() { + const tempLabels = new Set(); + const result = new Set(); + Object.keys(this.sliceLabelColorMap).forEach(sliceId => { + const colorMap = this.sliceLabelColorMap[sliceId]; + Object.keys(colorMap).forEach(label => { + if (tempLabels.has(label) && !result.has(label)) { + result.add(label); + } else { + tempLabels.add(label); + } + }); + }); + return [...result]; + } +} + +const getInstance = makeSingleton(SharedLabelColor); + +export default getInstance; diff --git a/superset-frontend/packages/superset-ui-core/src/color/index.ts b/superset-frontend/packages/superset-ui-core/src/color/index.ts index 0f7ce6194c6e3..e1cde3ba3e2d5 100644 --- a/superset-frontend/packages/superset-ui-core/src/color/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/color/index.ts @@ -32,5 +32,9 @@ export * from './SequentialScheme'; export { default as ColorSchemeRegistry } from './ColorSchemeRegistry'; export * from './colorSchemes'; export * from './utils'; +export { + default as getSharedLabelColor, + SharedLabelColor, +} from './SharedLabelColorSingleton'; export const BRAND_COLOR = '#00A699'; diff --git a/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts new file mode 100644 index 0000000000000..560bd56e6805f --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/color/SharedLabelColorSingleton.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { + CategoricalScheme, + getCategoricalSchemeRegistry, + getSharedLabelColor, + SharedLabelColor, +} from '@superset-ui/core'; + +describe('SharedLabelColor', () => { + beforeAll(() => { + getCategoricalSchemeRegistry() + .registerValue( + 'testColors', + new CategoricalScheme({ + id: 'testColors', + colors: ['red', 'green', 'blue'], + }), + ) + .registerValue( + 'testColors2', + new CategoricalScheme({ + id: 'testColors2', + colors: ['yellow', 'green', 'blue'], + }), + ); + }); + + beforeEach(() => { + getSharedLabelColor().clear(); + }); + + it('has default value out-of-the-box', () => { + expect(getSharedLabelColor()).toBeInstanceOf(SharedLabelColor); + }); + + describe('.addSlice(value, color, sliceId)', () => { + it('should add to valueSliceMap when first adding label', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + expect(sharedLabelColor.sliceLabelColorMap).toHaveProperty('1', { + a: 'red', + }); + }); + + it('do nothing when sliceId is undefined', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red'); + expect(sharedLabelColor.sliceLabelColorMap).toEqual({}); + }); + }); + + describe('.remove(sliceId)', () => { + it('should remove sliceId', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.removeSlice(1); + expect(sharedLabelColor.sliceLabelColorMap).toEqual({}); + }); + }); + + describe('.getColorMap(namespace, scheme, updateColorScheme)', () => { + it('return undefined when scheme is undefined', () => { + const sharedLabelColor = getSharedLabelColor(); + const colorMap = sharedLabelColor.getColorMap(); + expect(colorMap).toBeUndefined(); + }); + + it('return undefined value if pass updateColorScheme', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('b', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors2', true); + expect(colorMap).toEqual({ a: 'yellow', b: 'yellow' }); + }); + + it('return color value if not pass updateColorScheme', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('b', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors'); + expect(colorMap).toEqual({ a: 'red', b: 'blue' }); + }); + + it('return color value if shared label exit', () => { + const sharedLabelColor = getSharedLabelColor(); + sharedLabelColor.addSlice('a', 'red', 1); + sharedLabelColor.addSlice('a', 'blue', 2); + const colorMap = sharedLabelColor.getColorMap('', 'testColors'); + expect(colorMap).not.toEqual({}); + }); + }); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js index d36083e6cb46b..d0aed798de916 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js @@ -36,7 +36,7 @@ const propTypes = { }; function Chord(element, props) { - const { data, width, height, numberFormat, colorScheme } = props; + const { data, width, height, numberFormat, colorScheme, sliceId } = props; element.innerHTML = ''; @@ -93,7 +93,7 @@ function Chord(element, props) { .append('path') .attr('id', (d, i) => `group${i}`) .attr('d', arc) - .style('fill', (d, i) => colorFn(nodes[i])); + .style('fill', (d, i) => colorFn(nodes[i], sliceId)); // Add a text label. const groupText = group.append('text').attr('x', 6).attr('dy', 15); @@ -121,7 +121,7 @@ function Chord(element, props) { .on('mouseover', d => { chord.classed('fade', p => p !== d); }) - .style('fill', d => colorFn(nodes[d.source.index])) + .style('fill', d => colorFn(nodes[d.source.index], sliceId)) .attr('d', path); // Add an elaborate mouseover title for each chord. diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js index 4c9d09517f606..7503ff4ea1ff7 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { yAxisFormat, colorScheme } = formData; + const { yAxisFormat, colorScheme, sliceId } = formData; return { colorScheme, @@ -26,5 +26,6 @@ export default function transformProps(chartProps) { height, numberFormat: yAxisFormat, width, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js index ef26569199918..d363e8a5980b5 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js @@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array'; import { getNumberFormatter, getSequentialSchemeRegistry, + CategoricalColorNamespace, } from '@superset-ui/core'; import countries, { countryOptions } from './countries'; import './CountryMap.css'; @@ -45,17 +46,29 @@ const propTypes = { const maps = {}; function CountryMap(element, props) { - const { data, width, height, country, linearColorScheme, numberFormat } = - props; + const { + data, + width, + height, + country, + linearColorScheme, + numberFormat, + colorScheme, + sliceId, + } = props; const container = element; const format = getNumberFormatter(numberFormat); - const colorScale = getSequentialSchemeRegistry() + const linearColorScale = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(data, v => v.metric)); + const colorScale = CategoricalColorNamespace.getScale(colorScheme); + const colorMap = {}; data.forEach(d => { - colorMap[d.country_id] = colorScale(d.metric); + colorMap[d.country_id] = colorScheme + ? colorScale(d.country_id, sliceId) + : linearColorScale(d.metric); }); const colorFn = d => colorMap[d.properties.ISO] || 'none'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js index 4120f621f1a04..8789c3d2f34fe 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js @@ -18,7 +18,13 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { linearColorScheme, numberFormat, selectCountry } = formData; + const { + linearColorScheme, + numberFormat, + selectCountry, + colorScheme, + sliceId, + } = formData; return { width, @@ -27,5 +33,7 @@ export default function transformProps(chartProps) { country: selectCountry ? String(selectCountry).toLowerCase() : null, linearColorScheme, numberFormat, + colorScheme, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx index 518272afdff2e..2c07267748614 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/Histogram.jsx @@ -71,13 +71,14 @@ class CustomHistogram extends React.PureComponent { xAxisLabel, yAxisLabel, showLegend, + sliceId, } = this.props; const colorFn = CategoricalColorNamespace.getScale(colorScheme); const keys = data.map(d => d.key); const colorScale = scaleOrdinal({ domain: keys, - range: keys.map(x => colorFn(x)), + range: keys.map(x => colorFn(x, sliceId)), }); return ( diff --git a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js index 4a5782c7172a5..1de223240498c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-histogram/src/transformProps.js @@ -27,6 +27,7 @@ export default function transformProps(chartProps) { xAxisLabel, yAxisLabel, showLegend, + sliceId, } = formData; return { @@ -41,5 +42,6 @@ export default function transformProps(chartProps) { xAxisLabel, yAxisLabel, showLegend, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js index 224ff85af7d18..5355530cd536a 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js @@ -119,6 +119,7 @@ function Icicle(element, props) { partitionThreshold, useRichTooltip, timeSeriesOption = 'not_time', + sliceId, } = props; const div = d3.select(element); @@ -385,7 +386,7 @@ function Icicle(element, props) { // Apply color scheme g.selectAll('rect').style('fill', d => { - d.color = colorFn(d.name); + d.color = colorFn(d.name, sliceId); return d.color; }); diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js index d69de4ed52f0f..da58cd6160850 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js @@ -30,6 +30,7 @@ export default function transformProps(chartProps) { partitionThreshold, richTooltip, timeSeriesOption, + sliceId, } = formData; const { verboseMap } = datasource; @@ -48,5 +49,6 @@ export default function transformProps(chartProps) { timeSeriesOption, useLogScale: logScale, useRichTooltip: richTooltip, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js index 7ad4fd508ea56..4d7ef2b8ed995 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js @@ -76,6 +76,7 @@ function Rose(element, props) { numberFormat, useRichTooltip, useAreaProportions, + sliceId, } = props; const div = d3.select(element); @@ -120,10 +121,10 @@ function Rose(element, props) { .map(v => ({ key: v.name, value: v.value, - color: colorFn(v.name), + color: colorFn(v.name, sliceId), highlight: v.id === d.arcId, })) - : [{ key: d.name, value: d.val, color: colorFn(d.name) }]; + : [{ key: d.name, value: d.val, color: colorFn(d.name, sliceId) }]; return { key: 'Date', @@ -132,7 +133,7 @@ function Rose(element, props) { }; } - legend.width(width).color(d => colorFn(d.key)); + legend.width(width).color(d => colorFn(d.key, sliceId)); legendWrap.datum(legendData(datum)).call(legend); tooltip.headerFormatter(timeFormat).valueFormatter(format); @@ -378,7 +379,7 @@ function Rose(element, props) { const arcs = ae .append('path') .attr('class', 'arc') - .attr('fill', d => colorFn(d.name)) + .attr('fill', d => colorFn(d.name, sliceId)) .attr('d', arc); function mousemove() { diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js index 1e5407b16d9d2..b907e40ecffab 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js @@ -24,6 +24,7 @@ export default function transformProps(chartProps) { numberFormat, richTooltip, roseAreaProportion, + sliceId, } = formData; return { @@ -35,5 +36,6 @@ export default function transformProps(chartProps) { numberFormat, useAreaProportions: roseAreaProportion, useRichTooltip: richTooltip, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js index bddf570026ba3..4d6f059fdeb10 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/SankeyLoop.js @@ -84,7 +84,7 @@ function computeGraph(links) { } function SankeyLoop(element, props) { - const { data, width, height, colorScheme } = props; + const { data, width, height, colorScheme, sliceId } = props; const color = CategoricalColorNamespace.getScale(colorScheme); const margin = { ...defaultMargin, ...props.margin }; const innerWidth = width - margin.left - margin.right; @@ -109,7 +109,7 @@ function SankeyLoop(element, props) { value / sValue, )})`, ) - .linkColor(d => color(d.source.name)); + .linkColor(d => color(d.source.name, sliceId)); const div = select(element); div.selectAll('*').remove(); diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js index b62639a87feec..76c0c220a7681 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData, margin } = chartProps; - const { colorScheme } = formData; + const { colorScheme, sliceId } = formData; return { width, @@ -26,5 +26,6 @@ export default function transformProps(chartProps) { data: queriesData[0].data, colorScheme, margin, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js index b847f754133bf..d8c8f61e441a8 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/Sankey.js @@ -44,7 +44,7 @@ const propTypes = { const formatNumber = getNumberFormatter(NumberFormats.FLOAT); function Sankey(element, props) { - const { data, width, height, colorScheme } = props; + const { data, width, height, colorScheme, sliceId } = props; const div = d3.select(element); div.classed(`superset-legacy-chart-sankey`, true); const margin = { @@ -219,7 +219,7 @@ function Sankey(element, props) { .attr('width', sankey.nodeWidth()) .style('fill', d => { const name = d.name || 'N/A'; - d.color = colorFn(name.replace(/ .*/, '')); + d.color = colorFn(name, sliceId); return d.color; }) diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js index 5297994fb9525..b8e9f05b284c3 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/transformProps.js @@ -20,7 +20,7 @@ import { getLabelFontSize } from './utils'; export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { colorScheme } = formData; + const { colorScheme, sliceId } = formData; return { width, @@ -28,5 +28,6 @@ export default function transformProps(chartProps) { data: queriesData[0].data, colorScheme, fontSize: getLabelFontSize(width), + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js index 75bebdaa14034..2a9cc56f51fc6 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/Sunburst.js @@ -170,6 +170,7 @@ function Sunburst(element, props) { linearColorScheme, metrics, numberFormat, + sliceId, } = props; const responsiveClass = getResponsiveContainerClass(width); const isSmallWidth = responsiveClass === 's'; @@ -287,7 +288,7 @@ function Sunburst(element, props) { .attr('points', breadcrumbPoints) .style('fill', d => colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ); @@ -300,7 +301,7 @@ function Sunburst(element, props) { // Make text white or black based on the lightness of the background const col = d3.hsl( colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ); @@ -489,7 +490,7 @@ function Sunburst(element, props) { // For efficiency, filter nodes to keep only those large enough to see. const nodes = partition.nodes(root).filter(d => d.dx > 0.005); // 0.005 radians = 0.29 degrees - if (metrics[0] !== metrics[1] && metrics[1]) { + if (metrics[0] !== metrics[1] && metrics[1] && !colorScheme) { colorByCategory = false; const ext = d3.extent(nodes, d => d.m2 / d.m1); linearColorScale = getSequentialSchemeRegistry() @@ -507,7 +508,7 @@ function Sunburst(element, props) { .attr('fill-rule', 'evenodd') .style('fill', d => colorByCategory - ? categoricalColorScale(d.name) + ? categoricalColorScale(d.name, sliceId) : linearColorScale(d.m2 / d.m1), ) .style('opacity', 1) diff --git a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js index 9952fc4992eb8..92c4d99f00e41 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sunburst/src/transformProps.js @@ -18,7 +18,8 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData, datasource } = chartProps; - const { colorScheme, linearColorScheme, metric, secondaryMetric } = formData; + const { colorScheme, linearColorScheme, metric, secondaryMetric, sliceId } = + formData; const returnProps = { width, @@ -27,6 +28,7 @@ export default function transformProps(chartProps) { colorScheme, linearColorScheme, metrics: [metric, secondaryMetric], + sliceId, }; if (datasource && datasource.metrics) { diff --git a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js index a155a050b360d..f218218ec8bbd 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js @@ -87,6 +87,7 @@ function Treemap(element, props) { numberFormat, colorScheme, treemapRatio, + sliceId, } = props; const div = d3Select(element); div.classed('superset-legacy-chart-treemap', true); @@ -138,7 +139,7 @@ function Treemap(element, props) { .attr('id', d => `rect-${d.data.name}`) .attr('width', d => d.x1 - d.x0) .attr('height', d => d.y1 - d.y0) - .style('fill', d => colorFn(d.depth)); + .style('fill', d => colorFn(d.depth, sliceId)); cell .append('clipPath') diff --git a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js index adba34c09b3dd..bbc577cb3db12 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-treemap/src/transformProps.js @@ -18,7 +18,7 @@ */ export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { colorScheme, treemapRatio } = formData; + const { colorScheme, treemapRatio, sliceId } = formData; let { numberFormat } = formData; if (!numberFormat && chartProps.datasource && chartProps.datasource.metrics) { @@ -39,5 +39,6 @@ export default function transformProps(chartProps) { colorScheme, numberFormat, treemapRatio, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js index c7253e10d0e68..0c81e98560166 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js @@ -23,6 +23,7 @@ import { extent as d3Extent } from 'd3-array'; import { getNumberFormatter, getSequentialSchemeRegistry, + CategoricalColorNamespace, } from '@superset-ui/core'; import Datamap from 'datamaps/dist/datamaps.world.min'; @@ -55,6 +56,8 @@ function WorldMap(element, props) { showBubbles, linearColorScheme, color, + colorScheme, + sliceId, } = props; const div = d3.select(element); div.classed('superset-legacy-chart-world-map', true); @@ -69,15 +72,24 @@ function WorldMap(element, props) { .domain([extRadius[0], extRadius[1]]) .range([1, maxBubbleSize]); - const colorScale = getSequentialSchemeRegistry() + const linearColorScale = getSequentialSchemeRegistry() .get(linearColorScheme) .createLinearScale(d3Extent(filteredData, d => d.m1)); - const processedData = filteredData.map(d => ({ - ...d, - radius: radiusScale(Math.sqrt(d.m2)), - fillColor: colorScale(d.m1), - })); + const colorScale = CategoricalColorNamespace.getScale(colorScheme); + + const processedData = filteredData.map(d => { + let color = linearColorScale(d.m1); + if (colorScheme) { + // use color scheme instead + color = colorScale(d.name, sliceId); + } + return { + ...d, + radius: radiusScale(Math.sqrt(d.m2)), + fillColor: color, + }; + }); const mapData = {}; processedData.forEach(d => { diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts index ec8aafc7b872a..91664290dcb02 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts @@ -106,6 +106,7 @@ const config: ControlPanelConfig = { }, ], ['color_picker'], + ['color_scheme'], ['linear_color_scheme'], ], }, @@ -126,6 +127,9 @@ const config: ControlPanelConfig = { color_picker: { label: t('Bubble Color'), }, + color_scheme: { + label: t('Categorical Color Scheme'), + }, linear_color_scheme: { label: t('Country Color Scheme'), }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js index 464dd53afa4fc..3838ebfa5c10a 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js @@ -20,8 +20,14 @@ import { rgb } from 'd3-color'; export default function transformProps(chartProps) { const { width, height, formData, queriesData } = chartProps; - const { maxBubbleSize, showBubbles, linearColorScheme, colorPicker } = - formData; + const { + maxBubbleSize, + showBubbles, + linearColorScheme, + colorPicker, + colorScheme, + sliceId, + } = formData; const { r, g, b } = colorPicker; return { @@ -32,5 +38,7 @@ export default function transformProps(chartProps) { showBubbles, linearColorScheme, color: rgb(r, g, b).hex(), + colorScheme, + sliceId, }; } diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx index 7523c2e4ba907..64bfc0244a8ad 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.jsx @@ -46,7 +46,7 @@ function getCategories(fd, data) { if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) { let color; if (fd.dimension) { - color = hexToRGB(colorFn(d.cat_color), c.a * 255); + color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255); } else { color = fixedColor; } @@ -212,7 +212,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent { return data.map(d => { let color; if (fd.dimension) { - color = hexToRGB(colorFn(d.cat_color), c.a * 255); + color = hexToRGB(colorFn(d.cat_color, fd.sliceId), c.a * 255); return { ...d, color }; } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js index 7f3d4d08d4fef..4d130d2139d0c 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js @@ -313,6 +313,7 @@ function nvd3Vis(element, props) { yAxis2ShowMinMax = false, yField, yIsLogScale, + sliceId, } = props; const isExplore = document.querySelector('#explorer-container') !== null; @@ -670,7 +671,9 @@ function nvd3Vis(element, props) { ); } else if (vizType !== 'bullet') { const colorFn = getScale(colorScheme); - chart.color(d => d.color || colorFn(cleanColorInput(d[colorKey]))); + chart.color( + d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId), + ); } if (isVizTypes(['line', 'area', 'bar', 'dist_bar']) && useRichTooltip) { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js index 454314c502f3a..7fc8669e6daf3 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js @@ -94,6 +94,7 @@ export default function transformProps(chartProps) { yAxisShowminmax, yAxis2Showminmax, yLogScale, + sliceId, } = formData; let { @@ -195,5 +196,6 @@ export default function transformProps(chartProps) { yAxis2ShowMinMax: yAxis2Showminmax, yField: y, yIsLogScale: yLogScale, + sliceId, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts index 8e89914b4d8a8..6f79ec6e274e1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts @@ -63,6 +63,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, } = formData as BoxPlotQueryFormData; const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const numberFormatter = getNumberFormatter(numberFormat); @@ -98,9 +99,9 @@ export default function transformProps( datum[`${metric}__outliers`], ], itemStyle: { - color: colorFn(groupbyLabel), + color: colorFn(groupbyLabel, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, - borderColor: colorFn(groupbyLabel), + borderColor: colorFn(groupbyLabel, sliceId), }, }; }); @@ -138,7 +139,7 @@ export default function transformProps( }, }, itemStyle: { - color: colorFn(groupbyLabel), + color: colorFn(groupbyLabel, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index 3f3d84816d28f..88a0fe75c215f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -103,6 +103,7 @@ export default function transformProps( showLabels, showLegend, emitFilter, + sliceId, }: EchartsFunnelFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_FUNNEL_FORM_DATA, @@ -145,7 +146,7 @@ export default function transformProps( value: datum[metricLabel], name, itemStyle: { - color: colorFn(name), + color: colorFn(name, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts index beecb475ace7a..0486ddf67298b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -107,6 +107,7 @@ export default function transformProps( intervalColorIndices, valueFormatter, emitFilter, + sliceId, }: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData }; const data = (queriesData[0]?.data || []) as DataRecord[]; const numberFormatter = getNumberFormatter(numberFormat); @@ -147,7 +148,7 @@ export default function transformProps( value: data_point[getMetricLabel(metric as QueryFormMetric)] as number, name, itemStyle: { - color: colorFn(index), + color: colorFn(index, sliceId), }, title: { offsetCenter: [ @@ -175,7 +176,7 @@ export default function transformProps( item = { ...item, itemStyle: { - color: colorFn(index), + color: colorFn(index, sliceId), opacity: OpacityEnum.SemiTransparent, }, detail: { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts index ea9c6f1524970..905ecbfa8ebdb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts @@ -184,6 +184,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { baseEdgeWidth, baseNodeSize, edgeSymbol, + sliceId, }: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; const metricLabel = getMetricLabel(metric); @@ -264,7 +265,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { type: 'graph', categories: categoryList.map(c => ({ name: c, - itemStyle: { color: colorFn(c) }, + itemStyle: { color: colorFn(c, sliceId) }, })), layout, force: { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts index 76be9bb1a4e24..9cb35c1304450 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { QueryFormData } from '@superset-ui/core'; import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; import { SeriesTooltipOption } from 'echarts/types/src/util/types'; import { @@ -27,32 +28,34 @@ import { export type EdgeSymbol = 'none' | 'circle' | 'arrow'; -export type EchartsGraphFormData = EchartsLegendFormData & { - source: string; - target: string; - sourceCategory?: string; - targetCategory?: string; - colorScheme?: string; - metric?: string; - layout?: 'none' | 'circular' | 'force'; - roam: boolean | 'scale' | 'move'; - draggable: boolean; - selectedMode?: boolean | 'multiple' | 'single'; - showSymbolThreshold: number; - repulsion: number; - gravity: number; - baseNodeSize: number; - baseEdgeWidth: number; - edgeLength: number; - edgeSymbol: string; - friction: number; -}; +export type EchartsGraphFormData = QueryFormData & + EchartsLegendFormData & { + source: string; + target: string; + sourceCategory?: string; + targetCategory?: string; + colorScheme?: string; + metric?: string; + layout?: 'none' | 'circular' | 'force'; + roam: boolean | 'scale' | 'move'; + draggable: boolean; + selectedMode?: boolean | 'multiple' | 'single'; + showSymbolThreshold: number; + repulsion: number; + gravity: number; + baseNodeSize: number; + baseEdgeWidth: number; + edgeLength: number; + edgeSymbol: string; + friction: number; + }; export type EChartGraphNode = Omit & { value: number; tooltip?: Pick; }; +// @ts-ignore export const DEFAULT_FORM_DATA: EchartsGraphFormData = { ...DEFAULT_LEGEND_FORM_DATA, source: '', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index c3d6e979a96a4..8ac08e39257b8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -128,6 +128,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); @@ -177,6 +178,7 @@ export default function transformProps( yAxisIndex, filterState, seriesKey: entry.name, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -195,6 +197,7 @@ export default function transformProps( seriesKey: primarySeries.has(entry.name as string) ? `${entry.name} (1)` : entry.name, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -203,7 +206,9 @@ export default function transformProps( .filter((layer: AnnotationLayer) => layer.show) .forEach((layer: AnnotationLayer) => { if (isFormulaAnnotationLayer(layer)) - series.push(transformFormulaAnnotation(layer, data1, colorScale)); + series.push( + transformFormulaAnnotation(layer, data1, colorScale, sliceId), + ); else if (isIntervalAnnotationLayer(layer)) { series.push( ...transformIntervalAnnotation( @@ -211,11 +216,18 @@ export default function transformProps( data1, annotationData, colorScale, + sliceId, ), ); } else if (isEventAnnotationLayer(layer)) { series.push( - ...transformEventAnnotation(layer, data1, annotationData, colorScale), + ...transformEventAnnotation( + layer, + data1, + annotationData, + colorScale, + sliceId, + ), ); } else if (isTimeseriesAnnotationLayer(layer)) { series.push( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index a70855fa432f7..237f4ae001f70 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -109,6 +109,7 @@ export default function transformProps( showLegend, showLabelsThreshold, emitFilter, + sliceId, }: EchartsPieFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_PIE_FORM_DATA, @@ -162,7 +163,7 @@ export default function transformProps( value: datum[metricLabel], name, itemStyle: { - color: colorFn(name), + color: colorFn(name, sliceId), opacity: isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts index cd981a21a9b7e..b668e340350a8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts @@ -91,6 +91,7 @@ export default function transformProps( showLegend, isCircle, columnConfig, + sliceId, }: EchartsRadarFormData = { ...DEFAULT_LEGEND_FORM_DATA, ...DEFAULT_RADAR_FORM_DATA, @@ -154,7 +155,7 @@ export default function transformProps( value: metricLabels.map(metricLabel => datum[metricLabel]), name: joinedName, itemStyle: { - color: colorFn(joinedName), + color: colorFn(joinedName, sliceId), opacity: isFiltered ? OpacityEnum.Transparent : OpacityEnum.NonTransparent, 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 b3a3d58c39958..1a2200db22097 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -125,6 +125,7 @@ export default function transformProps( xAxisTitleMargin, yAxisTitleMargin, yAxisTitlePosition, + sliceId, }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); @@ -198,6 +199,7 @@ export default function transformProps( showValueIndexes, thresholdValues, richTooltip, + sliceId, }); if (transformedSeries) series.push(transformedSeries); }); @@ -217,7 +219,9 @@ export default function transformProps( .filter((layer: AnnotationLayer) => layer.show) .forEach((layer: AnnotationLayer) => { if (isFormulaAnnotationLayer(layer)) - series.push(transformFormulaAnnotation(layer, data, colorScale)); + series.push( + transformFormulaAnnotation(layer, data, colorScale, sliceId), + ); else if (isIntervalAnnotationLayer(layer)) { series.push( ...transformIntervalAnnotation( @@ -225,11 +229,18 @@ export default function transformProps( data, annotationData, colorScale, + sliceId, ), ); } else if (isEventAnnotationLayer(layer)) { series.push( - ...transformEventAnnotation(layer, data, annotationData, colorScale), + ...transformEventAnnotation( + layer, + data, + annotationData, + colorScale, + sliceId, + ), ); } else if (isTimeseriesAnnotationLayer(layer)) { series.push( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index c357b2a40ee9d..7ce72695be2b5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -84,6 +84,7 @@ export function transformSeries( thresholdValues?: number[]; richTooltip?: boolean; seriesKey?: OptionName; + sliceId?: number; }, ): SeriesOption | undefined { const { name } = series; @@ -105,6 +106,7 @@ export function transformSeries( thresholdValues = [], richTooltip, seriesKey, + sliceId, } = opts; const contexts = seriesContexts[name || ''] || []; const hasForecast = @@ -151,7 +153,7 @@ export function transformSeries( } // forcing the colorScale to return a different color for same metrics across different queries const itemStyle = { - color: colorScale(seriesKey || forecastSeries.name), + color: colorScale(seriesKey || forecastSeries.name, sliceId), opacity, }; let emphasis = {}; @@ -244,13 +246,14 @@ export function transformFormulaAnnotation( layer: FormulaAnnotationLayer, data: TimeseriesDataRecord[], colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption { const { name, color, opacity, width, style } = layer; return { name, id: name, itemStyle: { - color: color || colorScale(name), + color: color || colorScale(name, sliceId), }, lineStyle: { opacity: parseAnnotationOpacity(opacity), @@ -269,6 +272,7 @@ export function transformIntervalAnnotation( data: TimeseriesDataRecord[], annotationData: AnnotationData, colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption[] { const series: SeriesOption[] = []; const annotations = extractRecordAnnotations(layer, annotationData); @@ -323,7 +327,7 @@ export function transformIntervalAnnotation( markArea: { silent: false, itemStyle: { - color: color || colorScale(name), + color: color || colorScale(name, sliceId), opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium), emphasis: { opacity: 0.8, @@ -342,6 +346,7 @@ export function transformEventAnnotation( data: TimeseriesDataRecord[], annotationData: AnnotationData, colorScale: CategoricalColorScale, + sliceId?: number, ): SeriesOption[] { const series: SeriesOption[] = []; const annotations = extractRecordAnnotations(layer, annotationData); @@ -359,7 +364,7 @@ export function transformEventAnnotation( const lineStyle: LineStyleOption & DefaultStatesMixin['emphasis'] = { width, type: style as ZRLineType, - color: color || colorScale(name), + color: color || colorScale(name, sliceId), opacity: parseAnnotationOpacity(opacity), emphasis: { width: width ? width + 1 : width, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts index 2face71250d1a..25f3910b92c81 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts @@ -127,6 +127,7 @@ export default function transformProps( showUpperLabels, dashboardId, emitFilter, + sliceId, }: EchartsTreemapFormData = { ...DEFAULT_TREEMAP_FORM_DATA, ...formData, @@ -223,7 +224,7 @@ export default function transformProps( colorSaturation: COLOR_SATURATION, itemStyle: { borderColor: BORDER_COLOR, - color: colorFn(`${child.name}`), + color: colorFn(`${child.name}`, sliceId), borderWidth: BORDER_WIDTH, gapWidth: GAP_WIDTH, }, @@ -259,7 +260,7 @@ export default function transformProps( show: false, }, itemStyle: { - color: CategoricalColorNamespace.getColor(), + color: '#1FA8C9', }, }, ]; diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx index 4a9683a6dd652..5d8ae0105e1af 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/chart/WordCloud.tsx @@ -25,7 +25,12 @@ import { DeriveEncoding, Encoder, } from 'encodable'; -import { SupersetThemeProps, withTheme, seedRandom } from '@superset-ui/core'; +import { + SupersetThemeProps, + withTheme, + seedRandom, + CategoricalColorScale, +} from '@superset-ui/core'; export const ROTATION = { flat: () => 0, @@ -58,6 +63,7 @@ export interface WordCloudProps extends WordCloudVisualProps { data: PlainObject[]; height: number; width: number; + sliceId: number; } export interface WordCloudState { @@ -210,12 +216,15 @@ class WordCloud extends React.PureComponent< render() { const { scaleFactor } = this.state; - const { width, height, encoding } = this.props; + const { width, height, encoding, sliceId } = this.props; const { words } = this.state; const encoder = this.createEncoder(encoding); encoder.channels.color.setDomainFromDataset(words); + const { getValueFromDatum } = encoder.channels.color; + const colorFn = encoder.channels.color.scale as CategoricalColorScale; + const viewBoxWidth = width * scaleFactor; const viewBoxHeight = height * scaleFactor; @@ -234,7 +243,7 @@ class WordCloud extends React.PureComponent< fontSize={`${w.size}px`} fontWeight={w.weight} fontFamily={w.font} - fill={encoder.channels.color.encodeDatum(w, '')} + fill={colorFn(getValueFromDatum(w) as string, sliceId)} textAnchor="middle" transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`} > diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts index 095714086bcc8..aec557d616d58 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/legacyPlugin/transformProps.ts @@ -43,6 +43,7 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { series, sizeFrom = 0, sizeTo, + sliceId, } = formData as LegacyWordCloudFormData; const metricLabel = getMetricLabel(metric); @@ -77,5 +78,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { height, rotation, width, + sliceId, }; } diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts index 02265298e97f7..4ed6f6405c9b6 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/transformProps.ts @@ -23,7 +23,7 @@ import { WordCloudFormData } from '../types'; export default function transformProps(chartProps: ChartProps): WordCloudProps { const { width, height, formData, queriesData } = chartProps; - const { encoding, rotation } = formData as WordCloudFormData; + const { encoding, rotation, sliceId } = formData as WordCloudFormData; return { data: queriesData[0].data, @@ -31,5 +31,6 @@ export default function transformProps(chartProps: ChartProps): WordCloudProps { height, rotation, width, + sliceId, }; } diff --git a/superset-frontend/src/components/Chart/Chart.jsx b/superset-frontend/src/components/Chart/Chart.jsx index 89fdd54546195..ab4fde84d2789 100644 --- a/superset-frontend/src/components/Chart/Chart.jsx +++ b/superset-frontend/src/components/Chart/Chart.jsx @@ -47,6 +47,7 @@ const propTypes = { // and merged with extra filter that current dashboard applying formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, width: PropTypes.number, height: PropTypes.number, setControlValue: PropTypes.func, @@ -70,6 +71,7 @@ const propTypes = { onFilterMenuOpen: PropTypes.func, onFilterMenuClose: PropTypes.func, ownState: PropTypes.object, + postTransformProps: PropTypes.func, }; const BLANK = {}; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index 3c634ea32df8a..b814b6fde6d36 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -31,6 +31,7 @@ const propTypes = { initialValues: PropTypes.object, formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, height: PropTypes.number, width: PropTypes.number, setControlValue: PropTypes.func, @@ -48,6 +49,7 @@ const propTypes = { onFilterMenuOpen: PropTypes.func, onFilterMenuClose: PropTypes.func, ownState: PropTypes.object, + postTransformProps: PropTypes.func, source: PropTypes.oneOf(['dashboard', 'explore']), }; @@ -107,6 +109,7 @@ class ChartRenderer extends React.Component { nextProps.width !== this.props.width || nextProps.triggerRender || nextProps.labelColors !== this.props.labelColors || + nextProps.sharedLabelColors !== this.props.sharedLabelColors || nextProps.formData.color_scheme !== this.props.formData.color_scheme || nextProps.cacheBusterProp !== this.props.cacheBusterProp ); @@ -192,6 +195,7 @@ class ChartRenderer extends React.Component { filterState, formData, queriesResponse, + postTransformProps, } = this.props; // It's bad practice to use unprefixed `vizType` as classnames for chart @@ -260,6 +264,7 @@ class ChartRenderer extends React.Component { onRenderSuccess={this.handleRenderSuccess} onRenderFailure={this.handleRenderFailure} noResults={noResultsComponent} + postTransformProps={postTransformProps} /> ); } diff --git a/superset-frontend/src/dashboard/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts index 7b1b0017baa24..9a769101cfdca 100644 --- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts +++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts @@ -23,6 +23,21 @@ import { ChartConfiguration, DashboardInfo } from '../reducers/types'; export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED'; +export function updateColorSchema( + metadata: Record, + labelColors: Record, +) { + const categoricalNamespace = CategoricalColorNamespace.getNamespace( + metadata?.color_namespace, + ); + const colorMap = isString(labelColors) + ? JSON.parse(labelColors) + : labelColors; + Object.keys(colorMap).forEach(label => { + categoricalNamespace.setColor(label, colorMap[label]); + }); +} + // updates partially changed dashboard info export function dashboardInfoChanged(newInfo: { metadata: any }) { const { metadata } = newInfo; @@ -33,14 +48,12 @@ export function dashboardInfoChanged(newInfo: { metadata: any }) { categoricalNamespace.resetColors(); + if (metadata?.shared_label_colors) { + updateColorSchema(metadata, metadata?.shared_label_colors); + } + if (metadata?.label_colors) { - const labelColors = metadata.label_colors; - const colorMap = isString(labelColors) - ? JSON.parse(labelColors) - : labelColors; - Object.keys(colorMap).forEach(label => { - categoricalNamespace.setColor(label, colorMap[label]); - }); + updateColorSchema(metadata, metadata?.label_colors); } return { type: DASHBOARD_INFO_UPDATED, newInfo }; diff --git a/superset-frontend/src/dashboard/actions/dashboardLayout.js b/superset-frontend/src/dashboard/actions/dashboardLayout.js index 1fe988849d627..e0cbe7aa00c77 100644 --- a/superset-frontend/src/dashboard/actions/dashboardLayout.js +++ b/superset-frontend/src/dashboard/actions/dashboardLayout.js @@ -47,17 +47,19 @@ function setUnsavedChangesAfterAction(action) { dispatch(result); } + const { dashboardLayout, dashboardState } = getState(); + const isComponentLevelEvent = result.type === UPDATE_COMPONENTS && result.payload && result.payload.nextComponents; // trigger dashboardFilters state update if dashboard layout is changed. if (!isComponentLevelEvent) { - const components = getState().dashboardLayout.present; + const components = dashboardLayout.present; dispatch(updateLayoutComponents(components)); } - if (!getState().dashboardState.hasUnsavedChanges) { + if (!dashboardState.hasUnsavedChanges) { dispatch(setUnsavedChanges(true)); } }; diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 0afe42e063f7f..839b5feb7a153 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -18,7 +18,12 @@ */ /* eslint camelcase: 0 */ import { ActionCreators as UndoActionCreators } from 'redux-undo'; -import { ensureIsArray, t, SupersetClient } from '@superset-ui/core'; +import { + ensureIsArray, + t, + SupersetClient, + getSharedLabelColor, +} from '@superset-ui/core'; import { addChart, removeChart, @@ -67,6 +72,11 @@ export function removeSlice(sliceId) { return { type: REMOVE_SLICE, sliceId }; } +export const RESET_SLICE = 'RESET_SLICE'; +export function resetSlice() { + return { type: RESET_SLICE }; +} + const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard'; export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR'; export function toggleFaveStar(isStarred) { @@ -232,6 +242,7 @@ export function saveDashboardRequest(data, id, saveType) { color_scheme: data.metadata?.color_scheme || '', expanded_slices: data.metadata?.expanded_slices || {}, label_colors: data.metadata?.label_colors || {}, + shared_label_colors: data.metadata?.shared_label_colors || {}, refresh_frequency: data.metadata?.refresh_frequency || 0, timed_refresh_immune_slices: data.metadata?.timed_refresh_immune_slices || [], @@ -495,6 +506,28 @@ export function addSliceToDashboard(id, component) { }; } +export function postAddSliceFromDashboard() { + return (dispatch, getState) => { + const { + dashboardInfo: { metadata }, + dashboardState, + } = getState(); + + if (dashboardState?.updateSlice && dashboardState?.editMode) { + metadata.shared_label_colors = getSharedLabelColor().getColorMap( + metadata?.color_namespace, + metadata?.color_scheme, + ); + dispatch( + dashboardInfoChanged({ + metadata, + }), + ); + dispatch(resetSlice()); + } + }; +} + export function removeSliceFromDashboard(id) { return (dispatch, getState) => { const sliceEntity = getState().sliceEntities.slices[id]; @@ -504,6 +537,20 @@ export function removeSliceFromDashboard(id) { dispatch(removeSlice(id)); dispatch(removeChart(id)); + + const { + dashboardInfo: { metadata }, + } = getState(); + getSharedLabelColor().removeSlice(id); + metadata.shared_label_colors = getSharedLabelColor().getColorMap( + metadata?.color_namespace, + metadata?.color_scheme, + ); + dispatch( + dashboardInfoChanged({ + metadata, + }), + ); }; } diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.js b/superset-frontend/src/dashboard/actions/dashboardState.test.js index 295dc8cfc1189..f5fa60c08d56d 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.js @@ -39,7 +39,11 @@ describe('dashboardState actions', () => { sliceIds: [filterId], hasUnsavedChanges: true, }, - dashboardInfo: {}, + dashboardInfo: { + metadata: { + color_scheme: 'supersetColors', + }, + }, sliceEntities, dashboardFilters: emptyFilters, dashboardLayout: { @@ -116,6 +120,6 @@ describe('dashboardState actions', () => { const removeFilter = dispatch.getCall(0).args[0]; removeFilter(dispatch, getState); - expect(dispatch.getCall(3).args[0].type).toBe(REMOVE_FILTER); + expect(dispatch.getCall(4).args[0].type).toBe(REMOVE_FILTER); }); }); diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 4c8a978e96389..f02d6f26484aa 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -17,12 +17,7 @@ * under the License. */ /* eslint-disable camelcase */ -import { isString } from 'lodash'; -import { - Behavior, - CategoricalColorNamespace, - getChartMetadataRegistry, -} from '@superset-ui/core'; +import { Behavior, getChartMetadataRegistry } from '@superset-ui/core'; import { chart } from 'src/components/Chart/chartReducer'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; @@ -59,6 +54,7 @@ import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants'; import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; import extractUrlParams from '../util/extractUrlParams'; import getNativeFilterConfig from '../util/filterboxMigrationHelper'; +import { updateColorSchema } from './dashboardInfo'; export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; @@ -92,19 +88,14 @@ export const hydrateDashboard = // } + if (metadata?.shared_label_colors) { + updateColorSchema(metadata, metadata?.shared_label_colors); + } + // Priming the color palette with user's label-color mapping provided in // the dashboard's JSON metadata if (metadata?.label_colors) { - const namespace = metadata.color_namespace; - const colorMap = isString(metadata.label_colors) - ? JSON.parse(metadata.label_colors) - : metadata.label_colors; - const categoricalNamespace = - CategoricalColorNamespace.getNamespace(namespace); - - Object.keys(colorMap).forEach(label => { - categoricalNamespace.setColor(label, colorMap[label]); - }); + updateColorSchema(metadata, metadata?.label_colors); } // dashboard layout diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index a67061832ddaf..89b1b9bee673c 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -20,7 +20,7 @@ import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; -import { styled, t } from '@superset-ui/core'; +import { styled, t, getSharedLabelColor } from '@superset-ui/core'; import ButtonGroup from 'src/components/ButtonGroup'; import { @@ -356,6 +356,15 @@ class Header extends React.PureComponent { ? currentRefreshFrequency : dashboardInfo.metadata?.refresh_frequency; + const currentColorScheme = + dashboardInfo?.metadata?.color_scheme || colorScheme; + const currentColorNamespace = + dashboardInfo?.metadata?.color_namespace || colorNamespace; + const currentSharedLabelColors = getSharedLabelColor().getColorMap( + currentColorNamespace, + currentColorScheme, + ); + const data = { certified_by: dashboardInfo.certified_by, certification_details: dashboardInfo.certification_details, @@ -367,11 +376,11 @@ class Header extends React.PureComponent { slug, metadata: { ...dashboardInfo?.metadata, - color_namespace: - dashboardInfo?.metadata?.color_namespace || colorNamespace, - color_scheme: dashboardInfo?.metadata?.color_scheme || colorScheme, + color_namespace: currentColorNamespace, + color_scheme: currentColorScheme, positions, refresh_frequency: refreshFrequency, + shared_label_colors: currentSharedLabelColors, }, }; diff --git a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx index a18cb40ead87b..67c86cb1fc7de 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx @@ -29,6 +29,7 @@ import { SupersetClient, getCategoricalSchemeRegistry, ensureIsArray, + getSharedLabelColor, } from '@superset-ui/core'; import Modal from 'src/components/Modal'; @@ -169,7 +170,11 @@ const PropertiesModal = ({ if (metadata?.positions) { delete metadata.positions; } - setJsonMetadata(metadata ? jsonStringify(metadata) : ''); + const metaDataCopy = { ...metadata }; + if (metaDataCopy?.shared_label_colors) { + delete metaDataCopy.shared_label_colors; + } + setJsonMetadata(metaDataCopy ? jsonStringify(metaDataCopy) : ''); }, [form], ); @@ -282,12 +287,25 @@ const PropertiesModal = ({ form.getFieldsValue(); let currentColorScheme = colorScheme; let colorNamespace = ''; + let currentJsonMetadata = jsonMetadata; // color scheme in json metadata has precedence over selection - if (jsonMetadata?.length) { - const metadata = JSON.parse(jsonMetadata); + if (currentJsonMetadata?.length) { + const metadata = JSON.parse(currentJsonMetadata); currentColorScheme = metadata?.color_scheme || colorScheme; colorNamespace = metadata?.color_namespace || ''; + + // filter shared_label_color from user input + if (metadata?.shared_label_colors) { + delete metadata.shared_label_colors; + } + const colorMap = getSharedLabelColor().getColorMap( + colorNamespace, + currentColorScheme, + true, + ); + metadata.shared_label_colors = colorMap; + currentJsonMetadata = jsonStringify(metadata); } onColorSchemeChange(currentColorScheme, { @@ -304,7 +322,7 @@ const PropertiesModal = ({ id: dashboardId, title, slug, - jsonMetadata, + jsonMetadata: currentJsonMetadata, owners, colorScheme: currentColorScheme, colorNamespace, @@ -323,7 +341,7 @@ const PropertiesModal = ({ body: JSON.stringify({ dashboard_title: title, slug: slug || null, - json_metadata: jsonMetadata || null, + json_metadata: currentJsonMetadata || null, owners: (owners || []).map(o => o.id), certified_by: certifiedBy || null, certification_details: diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 239bb508deb06..821b311b48fd2 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -56,6 +56,7 @@ const propTypes = { chart: chartPropShape.isRequired, formData: PropTypes.object.isRequired, labelColors: PropTypes.object, + sharedLabelColors: PropTypes.object, datasource: PropTypes.object, slice: slicePropShape.isRequired, sliceName: PropTypes.string.isRequired, @@ -81,6 +82,7 @@ const propTypes = { addDangerToast: PropTypes.func.isRequired, ownState: PropTypes.object, filterState: PropTypes.object, + postTransformProps: PropTypes.func, }; const defaultProps = { @@ -319,6 +321,7 @@ export default class Chart extends React.Component { filters, formData, labelColors, + sharedLabelColors, updateSliceName, sliceName, toggleExpandSlice, @@ -334,6 +337,7 @@ export default class Chart extends React.Component { handleToggleFullSize, isFullSize, filterboxMigrationState, + postTransformProps, } = this.props; const { width } = this.state; @@ -449,6 +453,7 @@ export default class Chart extends React.Component { initialValues={initialValues} formData={formData} labelColors={labelColors} + sharedLabelColors={sharedLabelColors} ownState={ownState} filterState={filterState} queriesResponse={chart.queriesResponse} @@ -457,6 +462,7 @@ export default class Chart extends React.Component { vizType={slice.viz_type} isDeactivatedViz={isDeactivatedViz} filterboxMigrationState={filterboxMigrationState} + postTransformProps={postTransformProps} /> diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index 465d646e7bd16..95fb967c777b2 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -69,6 +69,7 @@ const propTypes = { updateComponents: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, setFullSizeChartId: PropTypes.func.isRequired, + postAddSliceFromDashboard: PropTypes.func, }; const defaultProps = { @@ -197,6 +198,7 @@ class ChartHolder extends React.Component { this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this); + this.handlePostTransformProps = this.handlePostTransformProps.bind(this); } componentDidMount() { @@ -251,6 +253,11 @@ class ChartHolder extends React.Component { setFullSizeChartId(isFullSize ? null : chartId); } + handlePostTransformProps(props) { + this.props.postAddSliceFromDashboard(); + return props; + } + render() { const { isFocused } = this.state; const { @@ -364,6 +371,7 @@ class ChartHolder extends React.Component { isComponentVisible={isComponentVisible} handleToggleFullSize={this.handleToggleFullSize} isFullSize={isFullSize} + postTransformProps={this.handlePostTransformProps} /> {editMode && ( diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 06d3f56e34fae..96e053e8ed60f 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -62,6 +62,7 @@ function mapStateToProps( PLACEHOLDER_DATASOURCE; const { colorScheme, colorNamespace } = dashboardState; const labelColors = dashboardInfo?.metadata?.label_colors || {}; + const sharedLabelColors = dashboardInfo?.metadata?.shared_label_colors || {}; // note: this method caches filters if possible to prevent render cascades const formData = getFormDataWithExtraFilters({ layout: dashboardLayout.present, @@ -76,6 +77,7 @@ function mapStateToProps( nativeFilters, dataMask, labelColors, + sharedLabelColors, }); formData.dashboardId = dashboardInfo.id; @@ -84,6 +86,7 @@ function mapStateToProps( chart, datasource, labelColors, + sharedLabelColors, slice: sliceEntities.slices[id], timeout: dashboardInfo.common.conf.SUPERSET_WEBSERVER_TIMEOUT, filters: getActiveFilters() || EMPTY_OBJECT, diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index 08b7ed9f82d90..23298d8bf96a4 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -37,6 +37,7 @@ import { setDirectPathToChild, setActiveTabs, setFullSizeChartId, + postAddSliceFromDashboard, } from 'src/dashboard/actions/dashboardState'; const propTypes = { @@ -111,6 +112,7 @@ function mapDispatchToProps(dispatch) { setFullSizeChartId, setActiveTabs, logEvent, + postAddSliceFromDashboard, }, dispatch, ); diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index e5fff328724d5..cd6772527509e 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -17,7 +17,14 @@ * under the License. */ import React, { FC, useRef, useEffect, useState } from 'react'; -import { FeatureFlag, isFeatureEnabled, t, useTheme } from '@superset-ui/core'; +import { + CategoricalColorNamespace, + FeatureFlag, + getSharedLabelColor, + isFeatureEnabled, + t, + useTheme, +} from '@superset-ui/core'; import { useDispatch, useSelector } from 'react-redux'; import { Global } from '@emotion/react'; import { useParams } from 'react-router-dom'; @@ -222,6 +229,18 @@ const DashboardPage: FC = () => { return () => {}; }, [css]); + useEffect( + () => () => { + // clean up label color + const categoricalNamespace = CategoricalColorNamespace.getNamespace( + metadata?.color_namespace, + ); + categoricalNamespace.resetColors(); + getSharedLabelColor().clear(); + }, + [metadata?.color_namespace], + ); + useEffect(() => { if (datasetsApiError) { addDangerToast( diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 64c794af93318..21d70b4f53bb9 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -39,6 +39,7 @@ import { UNSET_FOCUSED_FILTER_FIELD, SET_ACTIVE_TABS, SET_FULL_SIZE_CHART_ID, + RESET_SLICE, ON_FILTERS_REFRESH, ON_FILTERS_REFRESH_SUCCESS, } from '../actions/dashboardState'; @@ -58,6 +59,7 @@ export default function dashboardStateReducer(state = {}, action) { return { ...state, sliceIds: Array.from(updatedSliceIds), + updateSlice: true, }; }, [REMOVE_SLICE]() { @@ -70,6 +72,12 @@ export default function dashboardStateReducer(state = {}, action) { sliceIds: Array.from(updatedSliceIds), }; }, + [RESET_SLICE]() { + return { + ...state, + updateSlice: false, + }; + }, [TOGGLE_FAVE_STAR]() { return { ...state, isStarred: action.isStarred }; }, @@ -116,6 +124,7 @@ export default function dashboardStateReducer(state = {}, action) { maxUndoHistoryExceeded: false, editMode: false, updatedColorScheme: false, + updateSlice: false, // server-side returns last_modified_time for latest change lastModifiedTime: action.lastModifiedTime, }; diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.test.js b/superset-frontend/src/dashboard/reducers/dashboardState.test.js index 39798ecf139e5..de3ecf72ff3ec 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.test.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.test.js @@ -28,6 +28,7 @@ import { TOGGLE_EXPAND_SLICE, TOGGLE_FAVE_STAR, UNSET_FOCUSED_FILTER_FIELD, + RESET_SLICE, } from 'src/dashboard/actions/dashboardState'; import dashboardStateReducer from 'src/dashboard/reducers/dashboardState'; @@ -43,7 +44,7 @@ describe('dashboardState reducer', () => { { sliceIds: [1] }, { type: ADD_SLICE, slice: { slice_id: 2 } }, ), - ).toEqual({ sliceIds: [1, 2] }); + ).toEqual({ sliceIds: [1, 2], updateSlice: true }); }); it('should remove a slice', () => { @@ -55,6 +56,12 @@ describe('dashboardState reducer', () => { ).toEqual({ sliceIds: [1], filters: {} }); }); + it('should reset updateSlice', () => { + expect( + dashboardStateReducer({ updateSlice: true }, { type: RESET_SLICE }), + ).toEqual({ updateSlice: false }); + }); + it('should toggle fav star', () => { expect( dashboardStateReducer( diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 90022a9dce396..54e0417b27718 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -46,6 +46,7 @@ export interface GetFormDataWithExtraFiltersArguments { dataMask: DataMaskStateWithId; nativeFilters: NativeFiltersState; labelColors?: Record; + sharedLabelColors?: Record; } // this function merge chart's formData with dashboard filters value, @@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({ layout, dataMask, labelColors, + sharedLabelColors, }: GetFormDataWithExtraFiltersArguments) { // if dashboard metadata + filters have not changed, use cache if possible const cachedFormData = cachedFormdataByChart[sliceId]; @@ -77,6 +79,9 @@ export default function getFormDataWithExtraFilters({ areObjectsEqual(cachedFormData?.label_colors, labelColors, { ignoreUndefined: true, }) && + areObjectsEqual(cachedFormData?.shared_label_colors, sharedLabelColors, { + ignoreUndefined: true, + }) && !!cachedFormData && areObjectsEqual(cachedFormData?.dataMask, dataMask, { ignoreUndefined: true, @@ -108,6 +113,7 @@ export default function getFormDataWithExtraFilters({ const formData = { ...chart.formData, label_colors: labelColors, + shared_label_colors: sharedLabelColors, ...(colorScheme && { color_scheme: colorScheme }), extra_filters: getEffectiveExtraFilters(filters), ...extraData, diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx index bb1fde9c30e73..21605c553dfcd 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx @@ -156,13 +156,23 @@ export class ExploreChartHeader extends React.PureComponent { if (dashboard && dashboard.json_metadata) { // setting the chart to use the dashboard custom label colors if any - const labelColors = - JSON.parse(dashboard.json_metadata).label_colors || {}; + const metadata = JSON.parse(dashboard.json_metadata); + const sharedLabelColors = metadata.shared_label_colors || {}; + const customLabelColors = metadata.label_colors || {}; + const mergedLabelColors = { + ...sharedLabelColors, + ...customLabelColors, + }; + const categoricalNamespace = CategoricalColorNamespace.getNamespace(); - Object.keys(labelColors).forEach(label => { - categoricalNamespace.setColor(label, labelColors[label]); + Object.keys(mergedLabelColors).forEach(label => { + categoricalNamespace.setColor( + label, + mergedLabelColors[label], + metadata.color_scheme, + ); }); } } diff --git a/superset/dashboards/dao.py b/superset/dashboards/dao.py index 0443763cb370f..ce6e30f8d4beb 100644 --- a/superset/dashboards/dao.py +++ b/superset/dashboards/dao.py @@ -265,6 +265,7 @@ def set_dash_metadata( # pylint: disable=too-many-locals md["refresh_frequency"] = data.get("refresh_frequency", 0) md["color_scheme"] = data.get("color_scheme", "") md["label_colors"] = data.get("label_colors", {}) + md["shared_label_colors"] = data.get("shared_label_colors", {}) dashboard.json_metadata = json.dumps(md) diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py index b1831fdcbbe70..661c61e1c2483 100644 --- a/superset/dashboards/schemas.py +++ b/superset/dashboards/schemas.py @@ -128,6 +128,7 @@ class DashboardJSONMetadataSchema(Schema): color_namespace = fields.Str(allow_none=True) positions = fields.Dict(allow_none=True) label_colors = fields.Dict() + shared_label_colors = fields.Dict() # used for v0 import/export import_time = fields.Integer() remote_id = fields.Integer() diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py index 2ed627a257247..8669da99f4a9e 100644 --- a/tests/integration_tests/dashboards/api_tests.py +++ b/tests/integration_tests/dashboards/api_tests.py @@ -72,7 +72,7 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi "slug": "slug1_changed", "position_json": '{"b": "B"}', "css": "css_changed", - "json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}}', + "json_metadata": '{"refresh_frequency": 30, "timed_refresh_immune_slices": [], "expanded_slices": {}, "color_scheme": "", "label_colors": {}, "shared_label_colors": {}}', "published": False, } From 783168e13fd06794507807f782956b7a41e2ab8f Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Mon, 21 Mar 2022 13:09:38 +0000 Subject: [PATCH 26/29] chore: remove deprecated celery cli (#19273) * chore: remove deprecated celery cli * remove configs and UPDATING --- UPDATING.md | 1 + superset/cli/celery.py | 80 ------------------------------------------ superset/config.py | 2 -- 3 files changed, 1 insertion(+), 82 deletions(-) delete mode 100755 superset/cli/celery.py diff --git a/UPDATING.md b/UPDATING.md index 953d2c34c400c..64e22761335be 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -29,6 +29,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [19273](/~https://github.com/apache/superset/pull/19273): The `SUPERSET_CELERY_WORKERS` and `SUPERSET_WORKERS` config keys has been removed. Configure celery directly using `CELERY_CONFIG` on Superset - [19231](/~https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case. - [17556](/~https://github.com/apache/superset/pull/17556): Bumps mysqlclient from v1 to v2 - [19113](/~https://github.com/apache/superset/pull/19113): The `ENABLE_JAVASCRIPT_CONTROLS` setting has moved from app config to a feature flag. Any deployments who overrode this setting will now need to override the feature flag from here onward. diff --git a/superset/cli/celery.py b/superset/cli/celery.py deleted file mode 100755 index a0373573e8825..0000000000000 --- a/superset/cli/celery.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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 logging -from subprocess import Popen - -import click -from colorama import Fore -from flask.cli import with_appcontext - -from superset import app -from superset.extensions import celery_app - -logger = logging.getLogger(__name__) - - -@click.command() -@with_appcontext -@click.option( - "--workers", "-w", type=int, help="Number of celery server workers to fire up", -) -def worker(workers: int) -> None: - """Starts a Superset worker for async SQL query execution.""" - logger.info( - "The 'superset worker' command is deprecated. Please use the 'celery " - "worker' command instead." - ) - if workers: - celery_app.conf.update(CELERYD_CONCURRENCY=workers) - elif app.config["SUPERSET_CELERY_WORKERS"]: - celery_app.conf.update( - CELERYD_CONCURRENCY=app.config["SUPERSET_CELERY_WORKERS"] - ) - - local_worker = celery_app.Worker(optimization="fair") - local_worker.start() - - -@click.command() -@with_appcontext -@click.option( - "-p", "--port", default="5555", help="Port on which to start the Flower process", -) -@click.option( - "-a", "--address", default="localhost", help="Address on which to run the service", -) -def flower(port: int, address: str) -> None: - """Runs a Celery Flower web server - - Celery Flower is a UI to monitor the Celery operation on a given - broker""" - broker_url = celery_app.conf.BROKER_URL - cmd = ( - "celery flower " - f"--broker={broker_url} " - f"--port={port} " - f"--address={address} " - ) - logger.info( - "The 'superset flower' command is deprecated. Please use the 'celery " - "flower' command instead." - ) - print(Fore.GREEN + "Starting a Celery Flower instance") - print(Fore.BLUE + "-=" * 40) - print(Fore.YELLOW + cmd) - print(Fore.BLUE + "-=" * 40) - Popen(cmd, shell=True).wait() # pylint: disable=consider-using-with diff --git a/superset/config.py b/superset/config.py index 86e35be718702..c8c0a0053141b 100644 --- a/superset/config.py +++ b/superset/config.py @@ -140,8 +140,6 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: SAMPLES_ROW_LIMIT = 1000 # max rows retrieved by filter select auto complete FILTER_SELECT_ROW_LIMIT = 10000 -SUPERSET_WORKERS = 2 # deprecated -SUPERSET_CELERY_WORKERS = 32 # deprecated SUPERSET_WEBSERVER_PROTOCOL = "http" SUPERSET_WEBSERVER_ADDRESS = "0.0.0.0" From d215cbcdc8df85e5c5600cffc8bc7c337f45dee9 Mon Sep 17 00:00:00 2001 From: prassanna-helixsense-com <78132595+prassanna-helixsense-com@users.noreply.github.com> Date: Mon, 21 Mar 2022 19:14:29 +0530 Subject: [PATCH 27/29] Update README.md (#19270) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ecc921e58a4b..40ef7bb77bc2e 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ A modern, enterprise-ready business intelligence web application. ## Why Superset? -Superset is a modern data exploration and data visualization platform. Superset can replace or augment proprietary business intelligence tools for many teams. +Superset is a modern data exploration and data visualization platform. Superset can replace or augment proprietary business intelligence tools for many teams. Superset integrates well with a variety of data sources. Superset provides: From af91a136701401ab52e68fa5946d618f0d78e530 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Mon, 21 Mar 2022 14:57:38 +0000 Subject: [PATCH 28/29] chore: remove PUBLIC_ROLE_LIKE_GAMMA deprecated config key (#19274) --- UPDATING.md | 1 + superset/security/manager.py | 6 ------ superset/views/core.py | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index 64e22761335be..f9ffadfae7185 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -29,6 +29,7 @@ assists people when migrating to a new version. ### Breaking Changes +- [19274](/~https://github.com/apache/superset/pull/19274): The `PUBLIC_ROLE_LIKE_GAMMA` config key has been removed, set `PUBLIC_ROLE_LIKE` = "Gamma" to have the same functionality. - [19273](/~https://github.com/apache/superset/pull/19273): The `SUPERSET_CELERY_WORKERS` and `SUPERSET_WORKERS` config keys has been removed. Configure celery directly using `CELERY_CONFIG` on Superset - [19231](/~https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (permanently enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case. - [17556](/~https://github.com/apache/superset/pull/17556): Bumps mysqlclient from v1 to v2 diff --git a/superset/security/manager.py b/superset/security/manager.py index eb068c81fbb11..275c77a41cd20 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -726,12 +726,6 @@ def sync_role_definitions(self) -> None: self.auth_role_public, merge=True, ) - if current_app.config.get("PUBLIC_ROLE_LIKE_GAMMA", False): - logger.warning( - "The config `PUBLIC_ROLE_LIKE_GAMMA` is deprecated and will be removed " - "in Superset 1.0. Please use `PUBLIC_ROLE_LIKE` instead." - ) - self.copy_role("Gamma", self.auth_role_public, merge=True) self.create_missing_perms() diff --git a/superset/views/core.py b/superset/views/core.py index 50a56569c547c..5959584f1c2d3 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2792,7 +2792,7 @@ def show_traceback(self) -> FlaskResponse: # pylint: disable=no-self-use def welcome(self) -> FlaskResponse: """Personalized welcome page""" if not g.user or not g.user.get_id(): - if conf.get("PUBLIC_ROLE_LIKE_GAMMA", False) or conf["PUBLIC_ROLE_LIKE"]: + if conf["PUBLIC_ROLE_LIKE"]: return self.render_template("superset/public_welcome.html") return redirect(appbuilder.get_url_for_login) From c07a707eaba3bb4db3061728a3bcc8636e43ffb1 Mon Sep 17 00:00:00 2001 From: PApostol <50751110+PApostol@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:01:57 +0000 Subject: [PATCH 29/29] Various docstring fixes (#18221) --- superset/utils/mock_data.py | 3 ++- superset/views/utils.py | 1 + tests/integration_tests/fixtures/query_context.py | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/superset/utils/mock_data.py b/superset/utils/mock_data.py index e51bb39e20ff2..1c6515804b7fa 100644 --- a/superset/utils/mock_data.py +++ b/superset/utils/mock_data.py @@ -176,7 +176,7 @@ def add_data( If the table already exists `columns` can be `None`. :param Optional[List[ColumnInfo]] columns: list of column names and types to create - :param int run_nows: how many rows to generate and insert + :param int num_rows: how many rows to generate and insert :param str table_name: name of table, will be created if it doesn't exist :param bool append: if the table already exists, append data or replace? """ @@ -239,6 +239,7 @@ def add_sample_rows( ) -> Iterator[Model]: """ Add entities of a given model. + :param Session session: an SQLAlchemy session :param Model model: a Superset/FAB model :param int count: how many entities to generate and insert """ diff --git a/superset/views/utils.py b/superset/views/utils.py index 62639174f647e..19c9a2eaf05af 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -279,6 +279,7 @@ def apply_display_max_row_limit( metadata. :param sql_results: The results of a sql query from sql_lab.get_sql_results + :param rows: The number of rows to apply a limit to :returns: The mutated sql_results structure """ diff --git a/tests/integration_tests/fixtures/query_context.py b/tests/integration_tests/fixtures/query_context.py index e8a3118bf5db0..00a3036e01c25 100644 --- a/tests/integration_tests/fixtures/query_context.py +++ b/tests/integration_tests/fixtures/query_context.py @@ -37,8 +37,6 @@ def get_query_context( generated by the "Boy Name Cloud" chart in the examples. :param query_name: name of an example query, which is always in the format of `datasource_name[:test_case_name]`, where `:test_case_name` is optional. - :param datasource_id: id of datasource to query. - :param datasource_type: type of datasource to query. :param add_postprocessing_operations: Add post-processing operations to QueryObject :param add_time_offsets: Add time offsets to QueryObject(advanced analytics) :param form_data: chart metadata