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/.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/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/CONTRIBUTING.md b/CONTRIBUTING.md index 3a5e42b3aa4b1..84a948511ab15 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 @@ -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/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/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index 69f8f8a5a691d..77e381c7275cc 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 @@ -64,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 8d17dab043d87..c28975387efe3 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -29,6 +29,8 @@ 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. - [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. @@ -37,8 +39,10 @@ assists people when migrating to a new version. - [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. +- [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`. ### Potential Downtime @@ -60,6 +64,17 @@ flag for the legacy datasource editor (DISABLE_LEGACY_DATASOURCE_EDITOR) in conf - [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 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/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/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/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: 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/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-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index a5de8eff91f71..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 @@ -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'> = { @@ -444,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/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/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..03d44078344f7 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', @@ -32,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/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-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/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/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/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 ( ( - + 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/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/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/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', () => { 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: { 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/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, 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-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx index ba92d46b61e61..255dc99da7796 100644 --- a/superset-frontend/src/views/routes.tsx +++ b/superset-frontend/src/views/routes.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import React, { lazy } from 'react'; // not lazy loaded since this is the home page. @@ -182,7 +181,6 @@ const frontEndRoutes = routes ); export function isFrontendRoute(path?: string) { - if (!isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS)) return false; if (path) { const basePath = path.split(/[?#]/)[0]; // strip out query params and link bookmarks return !!frontEndRoutes[basePath]; 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/config.py b/superset/config.py index efb5178679d4a..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 @@ -391,13 +390,8 @@ 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 - # (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 @@ -420,8 +414,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, @@ -443,6 +435,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. @@ -1054,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/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/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 99cbc50997559..62ae8c9ebaf95 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -78,10 +78,14 @@ 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 -from superset.exceptions import QueryObjectValidationError +from superset.exceptions import ( + QueryClauseValidationException, + QueryObjectValidationError, +) from superset.jinja_context import ( BaseTemplateProcessor, ExtraCache, @@ -95,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, @@ -772,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: @@ -885,6 +894,11 @@ 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) + 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") @@ -908,6 +922,12 @@ 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) + 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) @@ -1166,6 +1186,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 +1199,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 @@ -1347,7 +1369,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( _( @@ -1355,11 +1377,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( _( @@ -1367,7 +1389,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: @@ -1389,6 +1411,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/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/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/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/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/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_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/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/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/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 5ec525b9cac73..0e17f46f16f07 100644 --- a/superset/views/sql_lab.py +++ b/superset/views/sql_lab.py @@ -20,8 +20,9 @@ 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 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 @@ -78,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: @@ -228,6 +226,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(), diff --git a/superset/viz.py b/superset/viz.py index 7c0f8e134875b..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 @@ -1842,7 +1840,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) 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/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/model_tests.py b/tests/integration_tests/model_tests.py index 6371a06123ae8..c6388601354d4 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, @@ -504,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 @@ -513,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") @@ -578,6 +607,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() 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/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/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/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} diff --git a/tests/unit_tests/sql_parse_tests.py b/tests/unit_tests/sql_parse_tests.py index aa811bdef757e..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(): @@ -1208,6 +1202,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: 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}/