From 99085d583ddadf8e092f6648195cde29efef6976 Mon Sep 17 00:00:00 2001 From: Antonio Rivero Martinez <38889534+Antonio-RiveroMartnez@users.noreply.github.com> Date: Wed, 27 Jul 2022 15:24:47 -0300 Subject: [PATCH 01/68] fix(viz): Header scrolling for Time Table in dashboard (#20874) * TimeTable: - Increase the z-index so the sparkline doesn't overlap when scrolling in the dashboard * Time TAble: - Remove testing label --- superset-frontend/src/visualizations/TimeTable/TimeTable.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/visualizations/TimeTable/TimeTable.jsx b/superset-frontend/src/visualizations/TimeTable/TimeTable.jsx index 9dd76ffc1565d..f6eb4b28b2880 100644 --- a/superset-frontend/src/visualizations/TimeTable/TimeTable.jsx +++ b/superset-frontend/src/visualizations/TimeTable/TimeTable.jsx @@ -98,12 +98,13 @@ const defaultProps = { url: '', }; +// @z-index-above-dashboard-charts + 1 = 11 const TimeTableStyles = styled.div` height: ${props => props.height}px; overflow: auto; th { - z-index: 1; // to cover sparkline + z-index: 11 !important; // to cover sparkline } `; From 234c44626df4b80e11711213335099ae909624df Mon Sep 17 00:00:00 2001 From: Antonio Rivero Martinez <38889534+Antonio-RiveroMartnez@users.noreply.github.com> Date: Wed, 27 Jul 2022 15:32:34 -0300 Subject: [PATCH 02/68] TableChart: (#20833) - Handle resize properly when only the scrollbar is changing so it doesn't get stuck into an infinite loop. --- .../plugin-chart-table/src/TableChart.tsx | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index f56381bb96efd..8acc06199f66e 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties, useCallback, useMemo, useState } from 'react'; +import React, { + CSSProperties, + useCallback, + useLayoutEffect, + useMemo, + useState, +} from 'react'; import { ColumnInstance, ColumnWithLooseAccessor, @@ -50,9 +56,15 @@ import Styles from './Styles'; import { formatColumnValue } from './utils/formatValue'; import { PAGE_SIZE_OPTIONS } from './consts'; import { updateExternalFormData } from './DataTable/utils/externalAPIs'; +import getScrollBarSize from './DataTable/utils/getScrollBarSize'; type ValueRange = [number, number]; +interface TableSize { + width: number; + height: number; +} + /** * Return sortType based on data type */ @@ -198,7 +210,10 @@ export default function TableChart( value => getTimeFormatterForGranularity(timeGrain)(value), [timeGrain], ); - + const [tableSize, setTableSize] = useState({ + width: 0, + height: 0, + }); // keep track of whether column order changed, so that column widths can too const [columnOrderToggle, setColumnOrderToggle] = useState(false); @@ -526,6 +541,41 @@ export default function TableChart( [setDataMask], ); + const handleSizeChange = useCallback( + ({ width, height }: { width: number; height: number }) => { + setTableSize({ width, height }); + }, + [], + ); + + useLayoutEffect(() => { + // After initial load the table should resize only when the new sizes + // Are not only scrollbar updates, otherwise, the table would twicth + const scrollBarSize = getScrollBarSize(); + const { width: tableWidth, height: tableHeight } = tableSize; + // Table is increasing its original size + if ( + width - tableWidth > scrollBarSize || + height - tableHeight > scrollBarSize + ) { + handleSizeChange({ + width: width - scrollBarSize, + height: height - scrollBarSize, + }); + } else if ( + tableWidth - width > scrollBarSize || + tableHeight - height > scrollBarSize + ) { + // Table is decreasing its original size + handleSizeChange({ + width, + height, + }); + } + }, [width, height, handleSizeChange, tableSize]); + + const { width: widthFromState, height: heightFromState } = tableSize; + return ( @@ -536,8 +586,8 @@ export default function TableChart( pageSize={pageSize} serverPaginationData={serverPaginationData} pageSizeOptions={pageSizeOptions} - width={width} - height={height} + width={widthFromState} + height={heightFromState} serverPagination={serverPagination} onServerPaginationChange={handleServerPaginationChange} onColumnOrderChange={() => setColumnOrderToggle(!columnOrderToggle)} From 07ce33d142039559557af68cbaa7af43d503f5dc Mon Sep 17 00:00:00 2001 From: Antonio Rivero Martinez <38889534+Antonio-RiveroMartnez@users.noreply.github.com> Date: Wed, 27 Jul 2022 15:33:13 -0300 Subject: [PATCH 03/68] Reports: (#20753) - dashboardInfo might be an empty object, in which case we must use the chart info instead so the resourceId is not undefined --- superset-frontend/src/reports/actions/reports.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/reports/actions/reports.js b/superset-frontend/src/reports/actions/reports.js index 6fbd8ca4ec0f2..96a435ec5da3a 100644 --- a/superset-frontend/src/reports/actions/reports.js +++ b/superset-frontend/src/reports/actions/reports.js @@ -23,6 +23,7 @@ import { addDangerToast, addSuccessToast, } from 'src/components/MessageToasts/actions'; +import { isEmpty } from 'lodash'; export const SET_REPORT = 'SET_REPORT'; export function setReport(report, resourceId, creationMethod, filterField) { @@ -76,7 +77,7 @@ export function fetchUISpecificReport({ const structureFetchAction = (dispatch, getState) => { const state = getState(); const { user, dashboardInfo, charts, explore } = state; - if (dashboardInfo) { + if (!isEmpty(dashboardInfo)) { dispatch( fetchUISpecificReport({ userId: user.userId, @@ -89,7 +90,7 @@ const structureFetchAction = (dispatch, getState) => { const [chartArr] = Object.keys(charts); dispatch( fetchUISpecificReport({ - userId: explore.user.userId, + userId: explore.user?.userId || user?.userId, filterField: 'chart_id', creationMethod: 'charts', resourceId: charts[chartArr].id, From c77ea1bb6aa22fac72ed3e386cbd7172cf7d7541 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:37:29 -0600 Subject: [PATCH 04/68] chore(deps): bump terser from 5.9.0 to 5.14.2 in /docs (#20786) Bumps [terser](https://github.com/terser/terser) from 5.9.0 to 5.14.2. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 89 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 2c788f210c475..367ec2363c993 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3014,15 +3014,37 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" @@ -3032,6 +3054,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -3840,15 +3870,10 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.0.4, acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== - -acorn@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== address@^1.0.1, address@^1.1.2: version "1.1.2" @@ -4438,7 +4463,7 @@ btoa@^1.2.1: buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-indexof@^1.0.0: @@ -4790,7 +4815,7 @@ comma-separated-tokens@^1.0.0: commander@^2.20.0: version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^5.1.0: @@ -10264,9 +10289,9 @@ source-map-js@^1.0.2: integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-support@~0.5.20: - version "0.5.20" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10281,11 +10306,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz" @@ -10618,23 +10638,14 @@ terser-webpack-plugin@^5.3.1: source-map "^0.6.1" terser "^5.7.2" -terser@^5.10.0: - version "5.12.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" - integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== +terser@^5.10.0, terser@^5.7.2: + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== dependencies: + "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.20" - -terser@^5.7.2: - version "5.9.0" - resolved "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz" - integrity sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ== - dependencies: - commander "^2.20.0" - source-map "~0.7.2" source-map-support "~0.5.20" text-table@^0.2.0: From ab415fddd9428101b4b4ca1c3681d9ac1719e585 Mon Sep 17 00:00:00 2001 From: Umair Pathan Abro <59310384+umair-abro@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:38:15 +0500 Subject: [PATCH 05/68] chore: adding Bazaar Technologies to Superset Users (#20669) * adding Bazaar Technologies to Superset Users At Bazaar, we are working to digitize Pakistan's retail sector. Superset is the main BI tool that powers the Bazaar Analytics Platform (aka Buraq). Buraq handles petabytes of data from a diverse set of data stores from Hive, S3 Lakes, RDS and Redshift. Superset allows us to make our business critical dashboards, that drives real time insights and data driven decisions to helping us to provide best in class customer experience. * Update RESOURCES/INTHEWILD.md Co-authored-by: Evan Rusackas * Update INTHEWILD.md Co-authored-by: Evan Rusackas Co-authored-by: Srini Kadamati --- RESOURCES/INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index 5a5b722a06acf..4a14790ed5b64 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -48,6 +48,7 @@ Join our growing community! ### E-Commerce - [AiHello](https://www.aihello.com) [@ganeshkrishnan1] +- [Bazaar Technologies](https://www.bazaartech.com) [@umair-abro] - [Dragonpass](https://www.dragonpass.com.cn/) [@zhxjdwh] - [Fanatics](https://www.fanatics.com) [@coderfender] - [Fordeal](http://www.fordeal.com) [@Renkai] From 8b354b4b6999e5a2474fb94ffab6b8428c4d4e6e Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Wed, 27 Jul 2022 15:40:59 -0300 Subject: [PATCH 06/68] fix: Published Dashboard without charts don't show up for non admin users (#20638) --- superset/dashboards/filters.py | 2 +- superset/sql_parse.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/dashboards/filters.py b/superset/dashboards/filters.py index f765bc8ffd5f9..8b4b8fe5269de 100644 --- a/superset/dashboards/filters.py +++ b/superset/dashboards/filters.py @@ -111,7 +111,7 @@ def apply(self, query: Query, value: Any) -> Query: datasource_perm_query = ( db.session.query(Dashboard.id) - .join(Dashboard.slices) + .join(Dashboard.slices, isouter=True) .filter( and_( Dashboard.published.is_(True), diff --git a/superset/sql_parse.py b/superset/sql_parse.py index d377986f56573..ab2f04417249c 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -494,7 +494,7 @@ class InsertRLSState(str, Enum): def has_table_query(token_list: TokenList) -> bool: """ - Return if a stament has a query reading from a table. + Return if a statement has a query reading from a table. >>> has_table_query(sqlparse.parse("COUNT(*)")[0]) False From 383313b105b0e82bea0f38cc971630eded5affe0 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Wed, 27 Jul 2022 15:41:17 -0300 Subject: [PATCH 07/68] fix(sql lab): Syntax errors should return with 422 status (#20491) * fix(sql lab): Syntax errors should return with 422 status * refactor --- superset/exceptions.py | 8 ++++++++ superset/sqllab/command.py | 25 ++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/superset/exceptions.py b/superset/exceptions.py index 07bedfa2db568..153d7439eb790 100644 --- a/superset/exceptions.py +++ b/superset/exceptions.py @@ -115,6 +115,14 @@ def __init__( self.status = status +class SupersetSyntaxErrorException(SupersetErrorsException): + status = 422 + error_type = SupersetErrorType.SYNTAX_ERROR + + def __init__(self, errors: List[SupersetError]) -> None: + super().__init__(errors) + + class SupersetTimeoutException(SupersetErrorFromParamsException): status = 408 diff --git a/superset/sqllab/command.py b/superset/sqllab/command.py index ce41eb6de230f..0aeab754ca54c 100644 --- a/superset/sqllab/command.py +++ b/superset/sqllab/command.py @@ -25,8 +25,13 @@ from superset.commands.base import BaseCommand from superset.common.db_query_status import QueryStatus from superset.dao.exceptions import DAOCreateFailedError -from superset.errors import SupersetErrorType -from superset.exceptions import SupersetErrorsException, SupersetGenericErrorException +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.exceptions import ( + SupersetErrorsException, + SupersetException, + SupersetGenericErrorException, + SupersetSyntaxErrorException, +) from superset.models.core import Database from superset.models.sql_lab import Query from superset.sqllab.command_status import SqlJsonExecutionStatus @@ -110,7 +115,21 @@ def run( # pylint: disable=too-many-statements,useless-suppression "status": status, "payload": self._execution_context_convertor.serialize_payload(), } - except (SqlLabException, SupersetErrorsException) as ex: + except SupersetErrorsException as ex: + if all(ex.error_type == SupersetErrorType.SYNTAX_ERROR for ex in ex.errors): + raise SupersetSyntaxErrorException(ex.errors) from ex + raise ex + except SupersetException as ex: + if ex.error_type == SupersetErrorType.SYNTAX_ERROR: + raise SupersetSyntaxErrorException( + [ + SupersetError( + message=ex.message, + error_type=ex.error_type, + level=ErrorLevel.ERROR, + ) + ] + ) from ex raise ex except Exception as ex: raise SqlLabException(self._execution_context, exception=ex) from ex From 718bc3062e99cc44afbb57f786b5ca228c5b13fb Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Thu, 28 Jul 2022 08:15:43 +0800 Subject: [PATCH 08/68] fix: invalid metric should raise an exception (#20882) --- superset/dashboards/api.py | 8 ++++++-- superset/models/sql_lab.py | 4 +++- superset/utils/core.py | 10 ++++++---- .../fixtures/deck_geojson_form_data.json | 2 +- .../fixtures/deck_path_form_data.json | 2 +- tests/integration_tests/viz_tests.py | 16 +++++----------- tests/unit_tests/core_tests.py | 12 ++++++++++++ 7 files changed, 34 insertions(+), 20 deletions(-) diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 5fb59a7d1dfbe..460cfcb0eaa30 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -383,8 +383,12 @@ def get_datasets(self, id_or_slug: str) -> Response: self.dashboard_dataset_schema.dump(dataset) for dataset in datasets ] return self.response(200, result=result) - except TypeError: - return self.response_400(message=gettext("Dataset schema is invalid.")) + except (TypeError, ValueError) as err: + return self.response_400( + message=gettext( + "Dataset schema is invalid, caused by: %(error)s", error=str(err) + ) + ) except DashboardAccessDeniedError: return self.response_403() except DashboardNotFoundError: diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index e7f61964e4034..4449b3dfa1c60 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -61,7 +61,9 @@ logger = logging.getLogger(__name__) -class Query(Model, ExtraJSONMixin, ExploreMixin): # pylint: disable=abstract-method +class Query( + Model, ExtraJSONMixin, ExploreMixin +): # pylint: disable=abstract-method,too-many-public-methods """ORM model for SQL query Now that SQL Lab support multi-statement execution, an entry in this diff --git a/superset/utils/core.py b/superset/utils/core.py index ba7237b77bbf0..6ce5a8a831c0c 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1294,7 +1294,7 @@ def get_metric_name( sql_expression = metric.get("sqlExpression") if sql_expression: return sql_expression - elif expression_type == "SIMPLE": + if expression_type == "SIMPLE": column: AdhocMetricColumn = metric.get("column") or {} column_name = column.get("column_name") aggregate = metric.get("aggregate") @@ -1302,10 +1302,12 @@ def get_metric_name( return f"{aggregate}({column_name})" if column_name: return column_name - raise ValueError(__("Invalid metric object")) - verbose_map = verbose_map or {} - return verbose_map.get(metric, metric) # type: ignore + if isinstance(metric, str): + verbose_map = verbose_map or {} + return verbose_map.get(metric, metric) + + raise ValueError(__("Invalid metric object: %(metric)s", metric=str(metric))) def get_column_names( diff --git a/tests/integration_tests/fixtures/deck_geojson_form_data.json b/tests/integration_tests/fixtures/deck_geojson_form_data.json index 422197a2855c7..e8258c7d443a1 100644 --- a/tests/integration_tests/fixtures/deck_geojson_form_data.json +++ b/tests/integration_tests/fixtures/deck_geojson_form_data.json @@ -43,5 +43,5 @@ "granularity_sqla": null, "autozoom": true, "url_params": {}, - "size": 100 + "size": "100" } diff --git a/tests/integration_tests/fixtures/deck_path_form_data.json b/tests/integration_tests/fixtures/deck_path_form_data.json index 39cc2007f85b4..ac2e404d83fb4 100644 --- a/tests/integration_tests/fixtures/deck_path_form_data.json +++ b/tests/integration_tests/fixtures/deck_path_form_data.json @@ -45,5 +45,5 @@ "granularity_sqla": null, "autozoom": true, "url_params": {}, - "size": 100 + "size": "100" } diff --git a/tests/integration_tests/viz_tests.py b/tests/integration_tests/viz_tests.py index 6a8bda3df954b..137e2a474c344 100644 --- a/tests/integration_tests/viz_tests.py +++ b/tests/integration_tests/viz_tests.py @@ -716,7 +716,7 @@ def test_get_data_transforms_dataframe(self): self.assertEqual(data, expected) def test_get_data_empty_null_keys(self): - form_data = {"groupby": [], "metrics": ["", None]} + form_data = {"groupby": [], "metrics": [""]} datasource = self.get_datasource_mock() # Test data raw = {} @@ -739,19 +739,13 @@ def test_get_data_empty_null_keys(self): "group": "All", } ], - "NULL": [ - { - "values": [ - {"x": 100, "y": 10}, - {"x": 200, "y": 20}, - {"x": 300, "y": 30}, - ], - "group": "All", - } - ], } self.assertEqual(data, expected) + form_data = {"groupby": [], "metrics": [None]} + with self.assertRaises(ValueError): + viz.viz_types["paired_ttest"](datasource, form_data) + class TestPartitionViz(SupersetTestCase): @patch("superset.viz.BaseViz.query_obj") diff --git a/tests/unit_tests/core_tests.py b/tests/unit_tests/core_tests.py index f7a0047157bb8..bd151011a48f6 100644 --- a/tests/unit_tests/core_tests.py +++ b/tests/unit_tests/core_tests.py @@ -105,6 +105,18 @@ def test_get_metric_name_invalid_metric(): with pytest.raises(ValueError): get_metric_name(metric) + metric = deepcopy(SQL_ADHOC_METRIC) + del metric["expressionType"] + with pytest.raises(ValueError): + get_metric_name(metric) + + with pytest.raises(ValueError): + get_metric_name(None) + with pytest.raises(ValueError): + get_metric_name(0) + with pytest.raises(ValueError): + get_metric_name({}) + def test_get_metric_names(): assert get_metric_names( From fe919741632e677ff14cdd35f886c8b2a9f0d4de Mon Sep 17 00:00:00 2001 From: cccs-RyanK <102618419+cccs-RyanK@users.noreply.github.com> Date: Thu, 28 Jul 2022 09:09:37 -0400 Subject: [PATCH 09/68] chore: Remove unecessary code from async and sync select components (#20690) * Created AsyncSelect Component Changed files to reference AsyncSelect if needed * modified import of AsyncSelect, removed async tests and prefixes from select tests * fixed various import and lint warnings * fixing lint errors * fixed frontend test errors * fixed alertreportmodel tests * removed accidental import * fixed lint errors * updated async select * removed code from select component * fixed select test * fixed async label value and select initial values * cleaned up async test * fixed lint errors * minor fixes to sync select component * removed unecessary variables and fixed linting * fixed npm test errors * fixed linting issues * fixed showSearch and storybook * fixed linting --- .../src/components/DatabaseSelector/index.tsx | 1 - .../components/ListView/Filters/Select.tsx | 42 ++- .../components/Select/AsyncSelect.test.tsx | 150 +++++++--- .../src/components/Select/AsyncSelect.tsx | 73 ++--- .../src/components/Select/Select.stories.tsx | 28 +- .../src/components/Select/Select.test.tsx | 27 +- .../src/components/Select/Select.tsx | 280 +----------------- .../src/components/TableSelector/index.tsx | 1 - .../src/filters/components/GroupBy/types.ts | 3 +- .../filters/components/TimeColumn/types.ts | 3 +- .../src/filters/components/TimeGrain/types.ts | 3 +- 11 files changed, 214 insertions(+), 397 deletions(-) diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index e972e95b002f1..1df7f78a3bea9 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -302,7 +302,6 @@ export default function DatabaseSelector({ disabled={!currentDb || readOnly} header={{t('Schema')}} labelInValue - lazyLoading={false} loading={loadingSchemas} name="select-schema" placeholder={t('Select schema or type schema name')} diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx b/superset-frontend/src/components/ListView/Filters/Select.tsx index 525061fd27411..ecda25a81ff63 100644 --- a/superset-frontend/src/components/ListView/Filters/Select.tsx +++ b/superset-frontend/src/components/ListView/Filters/Select.tsx @@ -26,6 +26,7 @@ import { t } from '@superset-ui/core'; import { Select } from 'src/components'; import { Filter, SelectOption } from 'src/components/ListView/types'; import { FormLabel } from 'src/components/Form'; +import AsyncSelect from 'src/components/Select/AsyncSelect'; import { FilterContainer, BaseFilter, FilterHandler } from './Base'; interface SelectFilterProps extends BaseFilter { @@ -86,19 +87,34 @@ function SelectFilter( return ( - {Header}} + labelInValue + onChange={onChange} + onClear={onClear} + options={selects} + placeholder={t('Select or type a value')} + showSearch + value={selectedOption} + /> + )} ); } diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index dc6eff35d9426..8a50002c866f8 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -60,10 +60,16 @@ const loadOptions = async (search: string, page: number, pageSize: number) => { const start = page * pageSize; const deleteCount = start + pageSize < totalCount ? pageSize : totalCount - start; - const data = OPTIONS.filter(option => option.label.match(search)).splice( - start, - deleteCount, - ); + const searchValue = search.trim().toLowerCase(); + const optionFilterProps = ['label', 'value', 'gender']; + const data = OPTIONS.filter(option => + optionFilterProps.some(prop => { + const optionProp = option?.[prop] + ? String(option[prop]).trim().toLowerCase() + : ''; + return optionProp.includes(searchValue); + }), + ).splice(start, deleteCount); return { data, totalCount: OPTIONS.length, @@ -74,7 +80,7 @@ const defaultProps = { allowClear: true, ariaLabel: ARIA_LABEL, labelInValue: true, - options: OPTIONS, + options: loadOptions, pageSize: 10, showSearch: true, }; @@ -129,17 +135,31 @@ test('displays a header', async () => { expect(screen.getByText(headerText)).toBeInTheDocument(); }); -test('adds a new option if the value is not in the options', async () => { - const { rerender } = render( - , +test('adds a new option if the value is not in the options, when options are empty', async () => { + const loadOptions = jest.fn(async () => ({ data: [], totalCount: 0 })); + render( + , ); await open(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); + options.forEach((option, i) => + expect(option).toHaveTextContent(OPTIONS[i].label), + ); +}); - rerender( - , +test('adds a new option if the value is not in the options, when options have values', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[1]], + totalCount: 1, + })); + render( + , ); await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + expect(await findSelectOption(OPTIONS[1].label)).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options).toHaveLength(2); options.forEach((option, i) => @@ -147,6 +167,20 @@ test('adds a new option if the value is not in the options', async () => { ); }); +test('does not add a new option if the value is already in the options', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0]], + totalCount: 1, + })); + render( + , + ); + await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); +}); + test('inverts the selection', async () => { render(); await open(); @@ -155,8 +189,11 @@ test('inverts the selection', async () => { }); test('sort the options by label if no sort comparator is provided', async () => { - const unsortedOptions = [...OPTIONS].sort(() => Math.random()); - render(); + const loadUnsortedOptions = jest.fn(async () => ({ + data: [...OPTIONS].sort(() => Math.random()), + totalCount: 2, + })); + render(); await open(); const options = await findAllSelectOptions(); options.forEach((option, key) => @@ -250,20 +287,23 @@ test('searches for label or value', async () => { render(); const search = option.value; await type(search.toString()); + expect(await findSelectOption(option.label)).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options.length).toBe(1); expect(options[0]).toHaveTextContent(option.label); }); test('search order exact and startWith match first', async () => { - render(); + render(); + await open(); await type('Her'); + expect(await findSelectOption('Guilherme')).toBeInTheDocument(); const options = await findAllSelectOptions(); expect(options.length).toBe(4); - expect(options[0]?.textContent).toEqual('Her'); - expect(options[1]?.textContent).toEqual('Herme'); - expect(options[2]?.textContent).toEqual('Cher'); - expect(options[3]?.textContent).toEqual('Guilherme'); + expect(options[0]).toHaveTextContent('Her'); + expect(options[1]).toHaveTextContent('Herme'); + expect(options[2]).toHaveTextContent('Cher'); + expect(options[3]).toHaveTextContent('Guilherme'); }); test('ignores case when searching', async () => { @@ -273,17 +313,16 @@ test('ignores case when searching', async () => { }); test('same case should be ranked to the top', async () => { - render( - , - ); + const loadOptions = jest.fn(async () => ({ + data: [ + { value: 'Cac' }, + { value: 'abac' }, + { value: 'acbc' }, + { value: 'CAc' }, + ], + totalCount: 4, + })); + render(); await type('Ac'); const options = await findAllSelectOptions(); expect(options.length).toBe(4); @@ -294,7 +333,7 @@ test('same case should be ranked to the top', async () => { }); test('ignores special keys when searching', async () => { - render(); + render(); await type('{shift}'); expect(screen.queryByText(LOADING)).not.toBeInTheDocument(); }); @@ -303,11 +342,16 @@ test('searches for custom fields', async () => { render( , ); + await open(); await type('Liam'); + // Liam is on the second page. need to wait to fetch options + expect(await findSelectOption('Liam')).toBeInTheDocument(); let options = await findAllSelectOptions(); expect(options.length).toBe(1); expect(options[0]).toHaveTextContent('Liam'); await type('Female'); + // Olivia is on the second page. need to wait to fetch options + expect(await findSelectOption('Olivia')).toBeInTheDocument(); options = await findAllSelectOptions(); expect(options.length).toBe(6); expect(options[0]).toHaveTextContent('Ava'); @@ -317,7 +361,7 @@ test('searches for custom fields', async () => { expect(options[4]).toHaveTextContent('Nikole'); expect(options[5]).toHaveTextContent('Olivia'); await type('1'); - expect(screen.getByText(NO_DATA)).toBeInTheDocument(); + expect(await screen.findByText(NO_DATA)).toBeInTheDocument(); }); test('removes duplicated values', async () => { @@ -332,12 +376,15 @@ test('removes duplicated values', async () => { }); test('renders a custom label', async () => { - const options = [ - { label: 'John', value: 1, customLabel:

John

}, - { label: 'Liam', value: 2, customLabel:

Liam

}, - { label: 'Olivia', value: 3, customLabel:

Olivia

}, - ]; - render(); + const loadOptions = jest.fn(async () => ({ + data: [ + { label: 'John', value: 1, customLabel:

John

}, + { label: 'Liam', value: 2, customLabel:

Liam

}, + { label: 'Olivia', value: 3, customLabel:

Olivia

}, + ], + totalCount: 3, + })); + render(); await open(); expect(screen.getByRole('heading', { name: 'John' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Liam' })).toBeInTheDocument(); @@ -345,12 +392,15 @@ test('renders a custom label', async () => { }); test('searches for a word with a custom label', async () => { - const options = [ - { label: 'John', value: 1, customLabel:

John

}, - { label: 'Liam', value: 2, customLabel:

Liam

}, - { label: 'Olivia', value: 3, customLabel:

Olivia

}, - ]; - render(); + const loadOptions = jest.fn(async () => ({ + data: [ + { label: 'John', value: 1, customLabel:

John

}, + { label: 'Liam', value: 2, customLabel:

Liam

}, + { label: 'Olivia', value: 3, customLabel:

Olivia

}, + ], + totalCount: 3, + })); + render(); await type('Liam'); const selectOptions = await findAllSelectOptions(); expect(selectOptions.length).toBe(1); @@ -391,7 +441,11 @@ test('does not add a new option if allowNewOptions is false', async () => { }); test('adds the null option when selected in single mode', async () => { - render(); + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0], NULL_OPTION], + totalCount: 2, + })); + render(); await open(); userEvent.click(await findSelectOption(NULL_OPTION.label)); const values = await findAllSelectValues(); @@ -399,12 +453,12 @@ test('adds the null option when selected in single mode', async () => { }); test('adds the null option when selected in multiple mode', async () => { + const loadOptions = jest.fn(async () => ({ + data: [OPTIONS[0], NULL_OPTION], + totalCount: 2, + })); render( - , + , ); await open(); userEvent.click(await findSelectOption(OPTIONS[0].label)); diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index 98f146f15f2ac..b95f2d8f0d1f6 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -55,7 +55,6 @@ type PickedSelectProps = Pick< | 'autoFocus' | 'disabled' | 'filterOption' - | 'labelInValue' | 'loading' | 'notFoundContent' | 'onChange' @@ -129,11 +128,10 @@ export interface AsyncSelectProps extends PickedSelectProps { optionFilterProps?: string[]; /** * It defines the options of the Select. - * The options can be static, an array of options. - * The options can also be async, a promise that returns + * The options are async, a promise that returns * an array of options. */ - options: OptionsType | OptionsPagePromise; + options: OptionsPagePromise; /** * It defines how many results should be included * in the query response. @@ -299,7 +297,6 @@ const AsyncSelect = ( filterOption = true, header = null, invertSelection = false, - labelInValue = false, lazyLoading = true, loading, mode = 'single', @@ -322,9 +319,7 @@ const AsyncSelect = ( }: AsyncSelectProps, ref: RefObject, ) => { - const isAsync = typeof options === 'function'; const isSingleMode = mode === 'single'; - const shouldShowSearch = isAsync || allowNewOptions ? true : showSearch; const [selectValue, setSelectValue] = useState(value); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(loading); @@ -360,8 +355,8 @@ const AsyncSelect = ( sortSelectedFirst(a, b) || // Only apply the custom sorter in async mode because we should // preserve the options order as much as possible. - (isAsync ? sortComparator(a, b, '') : 0), - [isAsync, sortComparator, sortSelectedFirst], + sortComparator(a, b, ''), + [sortComparator, sortSelectedFirst], ); const initialOptions = useMemo( @@ -528,7 +523,6 @@ const AsyncSelect = ( setSelectOptions(newOptions); } if ( - isAsync && !allValuesLoaded && loadingEnabled && !fetchedQueries.current.has(getQueryCacheKey(searchValue, 0, pageSize)) @@ -546,7 +540,7 @@ const AsyncSelect = ( vScroll.scrollTop > (vScroll.scrollHeight - vScroll.offsetHeight) * 0.7; const hasMoreData = page * pageSize + pageSize < totalCount; - if (!isLoading && isAsync && hasMoreData && thresholdReached) { + if (!isLoading && hasMoreData && thresholdReached) { const newPage = page + 1; fetchPage(inputValue, newPage); } @@ -575,30 +569,26 @@ const AsyncSelect = ( const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => { setIsDropdownVisible(isDropdownVisible); - if (isAsync) { - // loading is enabled when dropdown is open, - // disabled when dropdown is closed - if (loadingEnabled !== isDropdownVisible) { - setLoadingEnabled(isDropdownVisible); - } - // when closing dropdown, always reset loading state - if (!isDropdownVisible && isLoading) { - // delay is for the animation of closing the dropdown - // so the dropdown doesn't flash between "Loading..." and "No data" - // before closing. - setTimeout(() => { - setIsLoading(false); - }, 250); - } + // loading is enabled when dropdown is open, + // disabled when dropdown is closed + if (loadingEnabled !== isDropdownVisible) { + setLoadingEnabled(isDropdownVisible); + } + // when closing dropdown, always reset loading state + if (!isDropdownVisible && isLoading) { + // delay is for the animation of closing the dropdown + // so the dropdown doesn't flash between "Loading..." and "No data" + // before closing. + setTimeout(() => { + setIsLoading(false); + }, 250); } // if no search input value, force sort options because it won't be sorted by // `filterSort`. if (isDropdownVisible && !inputValue && selectOptions.length > 1) { - const sortedOptions = isAsync - ? selectOptions.slice().sort(sortComparatorForNoSearch) - : // if not in async mode, revert to the original select options - // (with selected options still sorted to the top) - initialOptionsSorted; + const sortedOptions = selectOptions + .slice() + .sort(sortComparatorForNoSearch); if (!isEqual(sortedOptions, selectOptions)) { setSelectOptions(sortedOptions); } @@ -627,7 +617,7 @@ const AsyncSelect = ( if (isLoading) { return ; } - if (shouldShowSearch && isDropdownVisible) { + if (showSearch && isDropdownVisible) { return ; } return ; @@ -660,7 +650,7 @@ const AsyncSelect = ( ); useEffect(() => { - if (isAsync && loadingEnabled && allowFetch) { + if (loadingEnabled && allowFetch) { // trigger fetch every time inputValue changes if (inputValue) { debouncedFetchPage(inputValue, 0); @@ -668,14 +658,7 @@ const AsyncSelect = ( fetchPage('', 0); } } - }, [ - isAsync, - loadingEnabled, - fetchPage, - allowFetch, - inputValue, - debouncedFetchPage, - ]); + }, [loadingEnabled, fetchPage, allowFetch, inputValue, debouncedFetchPage]); useEffect(() => { if (loading !== undefined && loading !== isLoading) { @@ -706,20 +689,20 @@ const AsyncSelect = ( getPopupContainer={ getPopupContainer || (triggerNode => triggerNode.parentNode) } - labelInValue={isAsync || labelInValue} + labelInValue maxTagCount={MAX_TAG_COUNT} mode={mappedMode} notFoundContent={isLoading ? t('Loading...') : notFoundContent} onDeselect={handleOnDeselect} onDropdownVisibleChange={handleOnDropdownVisibleChange} - onPopupScroll={isAsync ? handlePagination : undefined} - onSearch={shouldShowSearch ? handleOnSearch : undefined} + onPopupScroll={handlePagination} + onSearch={showSearch ? handleOnSearch : undefined} onSelect={handleOnSelect} onClear={handleClear} onChange={onChange} options={hasCustomLabels ? undefined : fullSelectOptions} placeholder={placeholder} - showSearch={shouldShowSearch} + showSearch={showSearch} showArrow tokenSeparators={tokenSeparators || TOKEN_SEPARATORS} value={selectValue} diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx index efcd91c0c38fc..b75e1ff28bd00 100644 --- a/superset-frontend/src/components/Select/Select.stories.tsx +++ b/superset-frontend/src/components/Select/Select.stories.tsx @@ -16,11 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useState, useCallback, useRef } from 'react'; +import React, { + ReactNode, + useState, + useCallback, + useRef, + useMemo, +} from 'react'; import Button from 'src/components/Button'; import ControlHeader from 'src/explore/components/ControlHeader'; -import AsyncSelect, { AsyncSelectProps, AsyncSelectRef } from './AsyncSelect'; -import Select, { SelectProps, OptionsTypePage, OptionsType } from './Select'; +import AsyncSelect, { + AsyncSelectProps, + AsyncSelectRef, + OptionsTypePage, +} from './AsyncSelect'; + +import Select, { SelectProps, OptionsType } from './Select'; export default { title: 'Select', @@ -452,6 +463,11 @@ export const AsynchronousSelect = ({ reject(new Error('Error while fetching the names from the server')); }); + const initialValue = useMemo( + () => ({ label: 'Valentina', value: 'Valentina' }), + [], + ); + return ( <>
{ expect(screen.getByText(headerText)).toBeInTheDocument(); }); -test('adds a new option if the value is not in the options', async () => { - const { rerender } = render( - ); await open(); expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); + options.forEach((option, i) => + expect(option).toHaveTextContent(OPTIONS[i].label), + ); +}); - rerender( +test('adds a new option if the value is not in the options, when options have values', async () => { + render( , + ); + await open(); + expect(await findSelectOption(OPTIONS[0].label)).toBeInTheDocument(); + const options = await findAllSelectOptions(); + expect(options).toHaveLength(1); +}); + test('inverts the selection', async () => { render( + + {this.props.datasource?.type === 'query' && ( + + + + + )} + + - - -