Compare commits

...

32 Commits

Author SHA1 Message Date
Michael S. Molina
2dc29cee9a chore: Adds 3.1.0 RC2 data to CHANGELOG.md and UPDATING.md 2023-12-11 14:15:23 -03:00
cwegener
be81aaa31a fix: bump pyarrow constraints (CVE-2023-47248) (#26187)
(cherry picked from commit 2ac28927a3)
2023-12-11 13:28:46 -03:00
Jeremy
8185ac3e33 chore: lock the databend-sqlalchemy version (#26082)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
(cherry picked from commit fca5b829df)
2023-12-11 13:26:55 -03:00
Michael S. Molina
463989dbc9 chore: Moves xAxisLabelRotation to shared controls (#26212)
(cherry picked from commit 005cf5947b)
2023-12-11 13:26:55 -03:00
Kamil Gabryjelski
38b8b03f90 fix: Use page.locator in Playwright reports (#26224)
(cherry picked from commit dbed64a2c6)
2023-12-11 13:26:55 -03:00
Ville Brofeldt
d0961d0ed8 fix(plugin-chart-echarts): support truncated numeric x-axis (#26215)
Co-authored-by: Michael S. Molina <michael.s.molina@gmail.com>
(cherry picked from commit 07e5fe8a66)
2023-12-08 11:14:18 -03:00
Vitor Avila
b699df7030 fix(chart-filter): Avoid column denormalization if not enabled (#26199)
(cherry picked from commit 05d7060d83)
2023-12-08 11:14:18 -03:00
Ville Brofeldt
c2612d8c26 fix: support custom links in markdown (#26211)
(cherry picked from commit d2adc858cb)
2023-12-08 11:14:18 -03:00
ʈᵃᵢ
ec0a338aa3 fix(dashboard): use textContent to render hidden title (#26189)
(cherry picked from commit 88fb342887)
2023-12-08 11:14:18 -03:00
Michael S. Molina
6fa75b7047 chore: Adds note about numerical x-axis (#26208)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
(cherry picked from commit b4a35e624b)
2023-12-08 11:14:18 -03:00
JUST.in DO IT
77c73b63db fix(sqllab): flaky json explore modal due to over-rendering (#26156)
(cherry picked from commit f30f685eb5)
2023-12-08 11:14:18 -03:00
Michael S. Molina
fb50819fcd fix: Includes 90° x-axis label rotation (#26207)
(cherry picked from commit 39c6488463)
2023-12-08 11:14:18 -03:00
Jack Fragassi
5c24c580dd chore: Lower giveup log level for retried functions to warning (#26188)
(cherry picked from commit bf5b18ccb1)
2023-12-08 11:14:17 -03:00
Suma Goud B
2104a9a853 fix(init-job): Fix envFrom for init job in helm chart (#26157)
(cherry picked from commit 4d4b19e8ba)
2023-12-08 11:14:17 -03:00
Jack Fragassi
0925d75dfa fix(embedded): Hide sensitive payload data from guest users (#25878)
(cherry picked from commit 386d4e0541)
2023-12-08 11:14:17 -03:00
JUST.in DO IT
96c0497fa9 fix(menu): Styling active menu in SPA navigation (#25533)
(cherry picked from commit 86304ab171)
2023-12-08 11:14:17 -03:00
Ville Brofeldt
5bcd3ef17e chore: harmonize and clean up list views (#25961)
(cherry picked from commit 0b477e3f7c)
2023-12-04 18:05:22 -03:00
Michael S. Molina
5ec1edc876 chore: Clean up the examples dashboards (#26158)
(cherry picked from commit 3ab27c6ec9)
2023-12-04 18:05:22 -03:00
John Bodley
aaa50c4b4a fix: Migration order due to cherry which went astray (#26160)
(cherry picked from commit 8644b1a319)
2023-12-04 18:05:22 -03:00
Ross Mabbett
880086c750 fix(Alerts/Reports): allow use of ";" separator in slack recipient entry (#25894)
Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com>
(cherry picked from commit b7a9c220e1)
2023-12-04 18:05:21 -03:00
JUST.in DO IT
d0aa34bf79 fix(sqllab): table preview has gone (#25977)
(cherry picked from commit cdbbd83705)
2023-12-04 18:05:21 -03:00
Vitor Avila
77332bfb38 fix(database-import): Support importing a DB connection with a version set (#26116)
(cherry picked from commit c033ca959d)
2023-12-04 18:05:21 -03:00
Beto Dealmeida
ceac19fa2f fix: set label on adhoc column should persist (#26154)
(cherry picked from commit b2ea97a984)
2023-12-04 18:05:21 -03:00
Michael S. Molina
79d5975028 feat: Adds legacy time support for Waterfall chart (#26136)
(cherry picked from commit f405ba033e)
2023-12-04 18:05:21 -03:00
Tom Murphy
4a4f9983df feat(helm): Add option to deploy extra containers to remaining deployments (#26123)
(cherry picked from commit 4f00404805)
2023-12-04 18:05:21 -03:00
Beto Dealmeida
26e59662fb fix(annotations): time grain column (#26140)
(cherry picked from commit cff473f825)
2023-12-04 18:05:21 -03:00
Kamil Gabryjelski
fad4616d2f chore: Rename SET_ACTIVE_TABS action, add a new action (#26147)
(cherry picked from commit d00c17dde2)
2023-12-04 18:05:21 -03:00
Hugh A. Miles II
2c3bf2895f chore(tags): Allow for lookup via ids vs. name in the API (#25996)
(cherry picked from commit ee308fbc64)
2023-12-04 18:05:21 -03:00
Daniel Vaz Gaspar
93319696de fix: remove default secret key from helm (#23916)
(cherry picked from commit 6a5a765689)
2023-12-04 18:05:21 -03:00
Gnought
e382d0dd28 chore(deps): bump pillow deps (#25931)
(cherry picked from commit a27a0df1a4)
2023-12-04 18:05:21 -03:00
Beto Dealmeida
f4fd0e19e2 fix: alias column when fetching values (#26120)
(cherry picked from commit 7223633da6)
2023-12-04 18:05:20 -03:00
Michael S. Molina
c8844bdd5e chore: Adds 3.1.0 data to CHANGELOG.md and UPDATING.md 2023-11-28 11:50:54 -03:00
152 changed files with 3443 additions and 2616 deletions

View File

@@ -19,6 +19,8 @@ under the License.
## Change Log
- [3.1.0](#310-mon-dec-11-162753-2023-0000)
- [3.0.3](#303-fri-dec-8-054009-2023--0800)
- [3.0.2](#302-mon-nov-20-073838-2023--0500)
- [3.0.1](#301-tue-oct-13-103221-2023--0700)
- [3.0.0](#300-thu-aug-24-133627-2023--0600)
@@ -33,6 +35,400 @@ under the License.
- [1.4.2](#142-sat-mar-19-000806-2022-0200)
- [1.4.1](#141)
### 3.1.0 (Mon Dec 11 16:27:53 2023 +0000)
**Database Migrations**
- [#26160](https://github.com/apache/superset/pull/26160) fix: Migration order due to cherry which went astray (@john-bodley)
- [#24776](https://github.com/apache/superset/pull/24776) chore(sqlalchemy): Remove erroneous SQLAlchemy ORM session.merge operations (@john-bodley)
- [#25819](https://github.com/apache/superset/pull/25819) chore: Singularize tag models (@john-bodley)
- [#25911](https://github.com/apache/superset/pull/25911) chore: remove deprecated functions in SQLAlchemy (@gnought)
- [#25304](https://github.com/apache/superset/pull/25304) feat: Adds CLI commands to execute viz migrations (@michael-s-molina)
- [#25204](https://github.com/apache/superset/pull/25204) feat(datasource): Checkbox for always filtering main dttm in datasource (@Always-prog)
- [#24832](https://github.com/apache/superset/pull/24832) fix: Alembic migration head (@john-bodley)
- [#24701](https://github.com/apache/superset/pull/24701) feat(Tags): Allow users to favorite Tags on CRUD Listview page (@hughhhh)
- [#24755](https://github.com/apache/superset/pull/24755) feat: Add line width unit control in deckgl Polygon and Path (@kgabryje)
- [#24700](https://github.com/apache/superset/pull/24700) chore: Update pylint to 2.17.4 (@EugeneTorap)
**Features**
- [#26136](https://github.com/apache/superset/pull/26136) feat: Adds legacy time support for Waterfall chart (@michael-s-molina)
- [#26123](https://github.com/apache/superset/pull/26123) feat(helm): Add option to deploy extra containers to remaining deployments (@bluemalkin)
- [#24714](https://github.com/apache/superset/pull/24714) feat: Add Apache Doris support (@liujiwen-up)
- [#26033](https://github.com/apache/superset/pull/26033) feat: Add Bubble chart migration logic (@michael-s-molina)
- [#25921](https://github.com/apache/superset/pull/25921) feat(metadb): handle decimals (@betodealmeida)
- [#24539](https://github.com/apache/superset/pull/24539) feat(sqllab): non-blocking persistence mode (@justinpark)
- [#25861](https://github.com/apache/superset/pull/25861) feat(sqllab): Show duration as separate column in Query History view (@sebastianliebscher)
- [#25809](https://github.com/apache/superset/pull/25809) feat(sqllab): TRINO_EXPAND_ROWS: expand columns from ROWs (@giftig)
- [#25952](https://github.com/apache/superset/pull/25952) feat: Add Area chart migration and tweaks the Timeseries chart migration (@michael-s-molina)
- [#25950](https://github.com/apache/superset/pull/25950) feat(explore): dataset macro: dttm filter context (@giftig)
- [#23973](https://github.com/apache/superset/pull/23973) feat: Adds Line chart migration logic (@michael-s-molina)
- [#20323](https://github.com/apache/superset/pull/20323) feat: safer insert RLS (@betodealmeida)
- [#25882](https://github.com/apache/superset/pull/25882) feat: method for dynamic `allows_alias_in_select` (@betodealmeida)
- [#25855](https://github.com/apache/superset/pull/25855) feat(sqllab): Dynamic query limit dropdown (@giftig)
- [#25344](https://github.com/apache/superset/pull/25344) feat(sqllab): Format sql (@justinpark)
- [#25557](https://github.com/apache/superset/pull/25557) feat: Improves the Waterfall chart (@michael-s-molina)
- [#23308](https://github.com/apache/superset/pull/23308) feat: support databend for superset (@hantmac)
- [#25795](https://github.com/apache/superset/pull/25795) feat: support server-side sessions (@dpgaspar)
- [#25783](https://github.com/apache/superset/pull/25783) feat(helm): Add option to deploy extra containers to init job (@bluemalkin)
- [#25696](https://github.com/apache/superset/pull/25696) feat(Export as PDF - rasterized): Adding rasterized pdf functionality to dashboard (@fisjac)
- [#25676](https://github.com/apache/superset/pull/25676) feat: add France's regions to country map visualization (@dmeaux)
- [#25569](https://github.com/apache/superset/pull/25569) feat: add database and schema names to dataset option (@soniagtm)
- [#25666](https://github.com/apache/superset/pull/25666) feat: Funnel/tooltip-customization (@CorbinBullard)
- [#25683](https://github.com/apache/superset/pull/25683) feat: Add week time grain for Elasticsearch datasets (@mikelv92)
- [#25423](https://github.com/apache/superset/pull/25423) feat(sqllab): ResultTable extension (@justinpark)
- [#25542](https://github.com/apache/superset/pull/25542) feat(sqllab): Add keyboard shortcut helper (@justinpark)
- [#25565](https://github.com/apache/superset/pull/25565) feat: migrate to docker compose v2 (@mdeshmu)
- [#24154](https://github.com/apache/superset/pull/24154) feat: Add Deck.gl Contour Layer (@Mattc1221)
- [#17906](https://github.com/apache/superset/pull/17906) feat(plugin-chart-echarts): Echarts Waterfall (@stephenLYZ)
- [#22107](https://github.com/apache/superset/pull/22107) feat: Adds the ECharts Bubble chart (@mayurnewase)
- [#25151](https://github.com/apache/superset/pull/25151) feat(sqllab): SPA migration (@justinpark)
- [#25247](https://github.com/apache/superset/pull/25247) feat: Implement using Playwright for taking screenshots in reports (@kgabryje)
- [#25303](https://github.com/apache/superset/pull/25303) feat: generic marshmallow error component (@betodealmeida)
- [#25377](https://github.com/apache/superset/pull/25377) feat(docker): Use docker buildx and Add ARM builds for dockerize and websocket (@alekseyolg)
- [#25343](https://github.com/apache/superset/pull/25343) feat: Adds Sunburst chart migration logic (@michael-s-molina)
- [#25345](https://github.com/apache/superset/pull/25345) feat(sqllab): extra logging when chart is downloaded (@zephyring)
- [#25280](https://github.com/apache/superset/pull/25280) feat(helm): Support HPA for supersetNode and supersetWorker (@tenkian4)
- [#25309](https://github.com/apache/superset/pull/25309) feat(tag): fast follow for Tags flatten api + update client with generator + some bug fixes (@hughhhh)
- [#24964](https://github.com/apache/superset/pull/24964) feat: Tags ListView Page (@hughhhh)
- [#24787](https://github.com/apache/superset/pull/24787) feat(sqllab): Show sql in the current result (@justinpark)
- [#25105](https://github.com/apache/superset/pull/25105) feat: removing renderCard from Tags/index.tsc to remove cardview from Tags ListView (@fisjac)
- [#25089](https://github.com/apache/superset/pull/25089) feat(docker): refactor docker images (@alekseyolg)
- [#24839](https://github.com/apache/superset/pull/24839) feat: Update Tags CRUD API (@hughhhh)
- [#25065](https://github.com/apache/superset/pull/25065) feat: adding Scarf pixels to gather telemetry on readme and website (@rusackas)
- [#14225](https://github.com/apache/superset/pull/14225) feat: a native SQLAlchemy dialect for Superset (@betodealmeida)
- [#25001](https://github.com/apache/superset/pull/25001) feat: Moves Profile to Single Page App (SPA) (@michael-s-molina)
- [#24983](https://github.com/apache/superset/pull/24983) feat(sqllab): Add /sqllab endpoint to the v1 api (@justinpark)
- [#24918](https://github.com/apache/superset/pull/24918) feat: command to test DB engine specs (@betodealmeida)
- [#24921](https://github.com/apache/superset/pull/24921) feat(gsheets): file upload (@betodealmeida)
- [#24934](https://github.com/apache/superset/pull/24934) feat: add MotherDuck DB engine spec (@betodealmeida)
- [#24909](https://github.com/apache/superset/pull/24909) feat: improve SQLite DB engine spec (@betodealmeida)
- [#24870](https://github.com/apache/superset/pull/24870) feat(chart): Added Central Asia countries to countries map (@Zoynels)
- [#24702](https://github.com/apache/superset/pull/24702) feat: add empty state for Tags (@hughhhh)
- [#24768](https://github.com/apache/superset/pull/24768) feat: add pandas performance dependencies (@sebastianliebscher)
- [#24618](https://github.com/apache/superset/pull/24618) feat(csv-upload): Configurable max filesize (@giftig)
- [#24580](https://github.com/apache/superset/pull/24580) feat(database): Database Filtering via custom configuration (@Antonio-RiveroMartnez)
**Fixes**
- [#26187](https://github.com/apache/superset/pull/26187) fix: bump pyarrow constraints (CVE-2023-47248) (@cwegener)
- [#26224](https://github.com/apache/superset/pull/26224) fix: Use page.locator in Playwright reports (@kgabryje)
- [#26156](https://github.com/apache/superset/pull/26156) fix(sqllab): flaky json explore modal due to over-rendering (@justinpark)
- [#25533](https://github.com/apache/superset/pull/25533) fix(menu): Styling active menu in SPA navigation (@justinpark)
- [#25977](https://github.com/apache/superset/pull/25977) fix(sqllab): table preview has gone (@justinpark)
- [#26066](https://github.com/apache/superset/pull/26066) fix: move driver import to method (@giftig)
- [#25934](https://github.com/apache/superset/pull/25934) fix(tag): update state to clear form on success (@hughhhh)
- [#25941](https://github.com/apache/superset/pull/25941) fix(sqllab): Allow router navigation to explore (@justinpark)
- [#25875](https://github.com/apache/superset/pull/25875) fix(typo): replace 'datasouce_id' with 'datasource_id' in openapi.json (@nero5700)
- [#25856](https://github.com/apache/superset/pull/25856) fix(tagging): change key from name to id for tagToSelectOption (@lilykuang)
- [#25831](https://github.com/apache/superset/pull/25831) fix: add validation on tag name to have name + onDelete refresh list view (@hughhhh)
- [#25851](https://github.com/apache/superset/pull/25851) fix: databend png pic (@hantmac)
- [#25803](https://github.com/apache/superset/pull/25803) fix(helm): Fix init extra containers (@bluemalkin)
- [#25739](https://github.com/apache/superset/pull/25739) fix(README): mismatched picture tags (@andy-clapson)
- [#25727](https://github.com/apache/superset/pull/25727) fix(metadb): handle durations (@betodealmeida)
- [#25718](https://github.com/apache/superset/pull/25718) fix(driver): bumping DuckDB to 0.9.2 (@rusackas)
- [#25603](https://github.com/apache/superset/pull/25603) fix(tags): +n tags for listview (@hughhhh)
- [#25578](https://github.com/apache/superset/pull/25578) fix(tags): Polish + Better messaging for skipped tags with bad permissions (@hughhhh)
- [#25582](https://github.com/apache/superset/pull/25582) fix(sqllab): Allow opening of SQL Lab in new browser tab (@justinpark)
- [#25615](https://github.com/apache/superset/pull/25615) fix(test-db): engine params (@betodealmeida)
- [#25532](https://github.com/apache/superset/pull/25532) fix: Breaking change in MachineAuthProvider constructor (@kgabryje)
- [#25547](https://github.com/apache/superset/pull/25547) fix: Make `host.docker.internal` available on linux (@sebastianliebscher)
- [#25536](https://github.com/apache/superset/pull/25536) fix: Tags Page ListView size to 10 (@hughhhh)
- [#25525](https://github.com/apache/superset/pull/25525) fix(test-db): removed attribute (@betodealmeida)
- [#25473](https://github.com/apache/superset/pull/25473) fix(tags): Update loading + pagination for Tags Page (@hughhhh)
- [#25470](https://github.com/apache/superset/pull/25470) fix(tags): fix clears delete on Tags Modal (@hughhhh)
- [#25496](https://github.com/apache/superset/pull/25496) fix: Tags Polish II (@hughhhh)
- [#24927](https://github.com/apache/superset/pull/24927) fix(Indian Map Changes): fixed-Indian-map-border (@Yaswanth-Perumalla)
- [#25403](https://github.com/apache/superset/pull/25403) fix: Tags Page Polish (@hughhhh)
- [#25306](https://github.com/apache/superset/pull/25306) fix(sqllab): misplaced limit warning alert (@justinpark)
- [#25361](https://github.com/apache/superset/pull/25361) fix: update helm chart app version (@hugosjoberg)
- [#25308](https://github.com/apache/superset/pull/25308) fix(sqllab): invalid persisted tab state (@justinpark)
- [#25216](https://github.com/apache/superset/pull/25216) fix(docs): Fixing a typo in README.md (@yousoph)
- [#25152](https://github.com/apache/superset/pull/25152) fix(sqllab): invalid reducer key name (@justinpark)
- [#25124](https://github.com/apache/superset/pull/25124) fix: Partially reverts #25007 (@michael-s-molina)
- [#25067](https://github.com/apache/superset/pull/25067) fix: small fixes for the meta DB (@betodealmeida)
- [#24963](https://github.com/apache/superset/pull/24963) fix(gsheets): add column names on file upload (@betodealmeida)
- [#24955](https://github.com/apache/superset/pull/24955) fix: timezone issue in Pandas 2 (@betodealmeida)
- [#24952](https://github.com/apache/superset/pull/24952) fix: `to_datetime` in Pandas 2 (@betodealmeida)
- [#24871](https://github.com/apache/superset/pull/24871) fix: Ignores hot update files when generating the manifest (@michael-s-molina)
- [#24868](https://github.com/apache/superset/pull/24868) fix: Ignores ResizeObserver errors in development mode (@michael-s-molina)
**Others**
- [#26082](https://github.com/apache/superset/pull/26082) chore: lock the databend-sqlalchemy version (@hantmac)
- [#26212](https://github.com/apache/superset/pull/26212) chore: Moves xAxisLabelRotation to shared controls (@michael-s-molina)
- [#26188](https://github.com/apache/superset/pull/26188) chore: Lower giveup log level for retried functions to warning (@jfrag1)
- [#25961](https://github.com/apache/superset/pull/25961) chore: harmonize and clean up list views (@villebro)
- [#26147](https://github.com/apache/superset/pull/26147) chore: Rename SET_ACTIVE_TABS action, add a new action (@kgabryje)
- [#25996](https://github.com/apache/superset/pull/25996) chore(tags): Allow for lookup via ids vs. name in the API (@hughhhh)
- [#26058](https://github.com/apache/superset/pull/26058) chore: Adds the 3.1.0 Release Notes (@michael-s-molina)
- [#26000](https://github.com/apache/superset/pull/26000) docs(databases): Update pinot.mdx to incorporate username and password based connection. (@raamri)
- [#26075](https://github.com/apache/superset/pull/26075) chore: Adds 3.0.2 data to CHANGELOG.md (@michael-s-molina)
- [#25850](https://github.com/apache/superset/pull/25850) chore(command): Organize Commands according to SIP-92 (@john-bodley)
- [#26073](https://github.com/apache/superset/pull/26073) chore: Updates Announce template to include CHANGELOG.md and UPDATING.md files (@michael-s-molina)
- [#26064](https://github.com/apache/superset/pull/26064) build(deps-dev): bump @types/node from 20.9.3 to 20.9.4 in /superset-websocket (@dependabot[bot])
- [#26063](https://github.com/apache/superset/pull/26063) build(deps): bump @types/lodash from 4.14.201 to 4.14.202 in /superset-websocket (@dependabot[bot])
- [#25844](https://github.com/apache/superset/pull/25844) chore: Allow only iterables for BaseDAO.delete() (@john-bodley)
- [#25917](https://github.com/apache/superset/pull/25917) docs: update security policy and contributing (@dpgaspar)
- [#24773](https://github.com/apache/superset/pull/24773) chore(connector): Cleanup base models and views according to SIP-92 (@john-bodley)
- [#26039](https://github.com/apache/superset/pull/26039) docs(intro): fix a single broken link (BugHerd #97) (@sfirke)
- [#26049](https://github.com/apache/superset/pull/26049) build(deps-dev): bump @types/node from 20.9.1 to 20.9.3 in /superset-websocket (@dependabot[bot])
- [#26048](https://github.com/apache/superset/pull/26048) build(deps-dev): bump @types/ws from 8.5.9 to 8.5.10 in /superset-websocket (@dependabot[bot])
- [#26043](https://github.com/apache/superset/pull/26043) chore: bump shillelagh (@betodealmeida)
- [#26004](https://github.com/apache/superset/pull/26004) chore: Allow external extensions to include their own package.json files (@kgabryje)
- [#26044](https://github.com/apache/superset/pull/26044) docs(BH#109): Athena URI spec fix (@rusackas)
- [#26025](https://github.com/apache/superset/pull/26025) build(deps-dev): bump eslint from 8.53.0 to 8.54.0 in /superset-websocket (@dependabot[bot])
- [#26013](https://github.com/apache/superset/pull/26013) chore: cleanup unused code in pandas 2.0+ (@gnought)
- [#26012](https://github.com/apache/superset/pull/26012) build(deps-dev): bump @types/node from 20.9.0 to 20.9.1 in /superset-websocket (@dependabot[bot])
- [#26009](https://github.com/apache/superset/pull/26009) chore: Remove unnecessary autoflush from tagging and key/value workflows (@john-bodley)
- [#25551](https://github.com/apache/superset/pull/25551) docs: handling "System limit for number of file watchers reached" error (@nitish-samsung-jha)
- [#25986](https://github.com/apache/superset/pull/25986) chore: Remove more redundant code in utils/core (@sebastianliebscher)
- [#24485](https://github.com/apache/superset/pull/24485) style: Transition of Navbar from dark to light and vice-versa is now smooth (@git-init-priyanshu)
- [#25059](https://github.com/apache/superset/pull/25059) docs: add Tentacle to users list (@jdclarke5)
- [#25968](https://github.com/apache/superset/pull/25968) chore: Add entry point for SliceHeader frontend extension (@kgabryje)
- [#25891](https://github.com/apache/superset/pull/25891) chore: support different JWT CSRF cookie names (@dpgaspar)
- [#25953](https://github.com/apache/superset/pull/25953) build(deps-dev): bump axios from 0.25.0 to 1.6.0 in /superset-embedded-sdk (@dependabot[bot])
- [#25927](https://github.com/apache/superset/pull/25927) build(deps-dev): bump @types/jsonwebtoken from 9.0.4 to 9.0.5 in /superset-websocket (@dependabot[bot])
- [#25929](https://github.com/apache/superset/pull/25929) build(deps-dev): bump @types/uuid from 9.0.6 to 9.0.7 in /superset-websocket (@dependabot[bot])
- [#25958](https://github.com/apache/superset/pull/25958) test: Reduce flaky integration tests triggered by `test_get_tag` (@sebastianliebscher)
- [#25948](https://github.com/apache/superset/pull/25948) chore: Simplify views/base (@sebastianliebscher)
- [#25951](https://github.com/apache/superset/pull/25951) build(deps): bump axios from 1.4.0 to 1.6.1 in /superset-frontend (@dependabot[bot])
- [#25881](https://github.com/apache/superset/pull/25881) chore(issue template): attempting to fix two entries/links (@rusackas)
- [#25926](https://github.com/apache/superset/pull/25926) chore: removing unused chartMetadata field (@rusackas)
- [#25928](https://github.com/apache/superset/pull/25928) build(deps-dev): bump @types/node from 20.8.10 to 20.9.0 in /superset-websocket (@dependabot[bot])
- [#25885](https://github.com/apache/superset/pull/25885) docs: Remove Python 3.8 from CONTRIBUTING.md (@koushik-rout-samsung)
- [#25900](https://github.com/apache/superset/pull/25900) chore: Simplify utils/cache by using default argument values (@sebastianliebscher)
- [#25912](https://github.com/apache/superset/pull/25912) chore: remove unused functions in utils/core (@sebastianliebscher)
- [#25907](https://github.com/apache/superset/pull/25907) build(deps): bump @types/lodash from 4.14.200 to 4.14.201 in /superset-websocket (@dependabot[bot])
- [#25906](https://github.com/apache/superset/pull/25906) build(deps-dev): bump @types/ws from 8.5.7 to 8.5.9 in /superset-websocket (@dependabot[bot])
- [#25905](https://github.com/apache/superset/pull/25905) build(deps-dev): bump @types/cookie from 0.5.3 to 0.5.4 in /superset-websocket (@dependabot[bot])
- [#25262](https://github.com/apache/superset/pull/25262) chore: add more migration tests (@eschutho)
- [#25886](https://github.com/apache/superset/pull/25886) build(deps): bump cookie from 0.5.0 to 0.6.0 in /superset-websocket (@dependabot[bot])
- [#25714](https://github.com/apache/superset/pull/25714) chore: Update INTHEWILD.md (@codek)
- [#25867](https://github.com/apache/superset/pull/25867) build(deps-dev): bump eslint from 8.52.0 to 8.53.0 in /superset-websocket (@dependabot[bot])
- [#25852](https://github.com/apache/superset/pull/25852) chore: Updates Databend image extension reference in README.md (@michael-s-molina)
- [#25531](https://github.com/apache/superset/pull/25531) docs: Update location of `async_query_manager.py` (@emmanuel-ferdman)
- [#25817](https://github.com/apache/superset/pull/25817) chore(docker-compose): more host network specifiers (@giftig)
- [#25812](https://github.com/apache/superset/pull/25812) chore: Removes border of the color picker control (@michael-s-molina)
- [#25826](https://github.com/apache/superset/pull/25826) chore(websocket): Adding support for redis username in websocket server (@craig-rueda)
- [#25822](https://github.com/apache/superset/pull/25822) chore: Update sip.md to have a better call to action (@rusackas)
- [#25823](https://github.com/apache/superset/pull/25823) chore(issues): config.yaml added with feature request link to open a discussion (@rusackas)
- [#25816](https://github.com/apache/superset/pull/25816) build(deps-dev): bump @types/node from 20.8.7 to 20.8.10 in /superset-websocket (@dependabot[bot])
- [#25530](https://github.com/apache/superset/pull/25530) docs: Add Cyberhaven to Users list (@ghost)
- [#25314](https://github.com/apache/superset/pull/25314) chore(celery): Cleanup config and async query specifications (@john-bodley)
- [#25778](https://github.com/apache/superset/pull/25778) build(deps): bump browserify-sign from 4.2.1 to 4.2.2 in /superset-frontend (@dependabot[bot])
- [#24046](https://github.com/apache/superset/pull/24046) chore(security): Make get_database_perm/get_dataset_perm return optional (@john-bodley)
- [#25765](https://github.com/apache/superset/pull/25765) chore: Add config options for Playwright wait_until and default timeout (@kgabryje)
- [#25721](https://github.com/apache/superset/pull/25721) style(readme): reformatted (@bipinct)
- [#25737](https://github.com/apache/superset/pull/25737) chore: bump pymssql version (@gnought)
- [#25521](https://github.com/apache/superset/pull/25521) chore(websocket): [WIP] Making JWT algos configurable (@craig-rueda)
- [#25735](https://github.com/apache/superset/pull/25735) build(deps-dev): bump eslint from 8.51.0 to 8.52.0 in /superset-websocket (@dependabot[bot])
- [#25726](https://github.com/apache/superset/pull/25726) chore: updated base DAO find_by_id to return generic type (@zephyring)
- [#25717](https://github.com/apache/superset/pull/25717) refactor: use DATE_TRUNC for Elasticsearch time grain (@mikelv92)
- [#25709](https://github.com/apache/superset/pull/25709) chore: helm chart: bump appVersion to 3.0.1 (@mdavidsen)
- [#25577](https://github.com/apache/superset/pull/25577) chore: Change the format for sha512 sum for releases (@sebastianliebscher)
- [#25689](https://github.com/apache/superset/pull/25689) build(deps-dev): bump @types/cookie from 0.5.1 to 0.5.3 in /superset-websocket (@dependabot[bot])
- [#25701](https://github.com/apache/superset/pull/25701) build(deps-dev): bump @types/uuid from 9.0.4 to 9.0.6 in /superset-websocket (@dependabot[bot])
- [#25710](https://github.com/apache/superset/pull/25710) docs(README): Fix typo (@RahulK4102)
- [#25700](https://github.com/apache/superset/pull/25700) build(deps-dev): bump @types/node from 20.8.6 to 20.8.7 in /superset-websocket (@dependabot[bot])
- [#25322](https://github.com/apache/superset/pull/25322) chore: add latest docker tag (@eschutho)
- [#25691](https://github.com/apache/superset/pull/25691) chore: Adds 3.0.1 data to CHANGELOG.md (@michael-s-molina)
- [#25688](https://github.com/apache/superset/pull/25688) build(deps-dev): bump @types/jsonwebtoken from 9.0.3 to 9.0.4 in /superset-websocket (@dependabot[bot])
- [#25543](https://github.com/apache/superset/pull/25543) chore: Cleanup hostNamesConfig.js (@john-bodley)
- [#25654](https://github.com/apache/superset/pull/25654) docs: make project-specific security page more prominent (@raboof)
- [#25667](https://github.com/apache/superset/pull/25667) chore: sync lock files (@villebro)
- [#25661](https://github.com/apache/superset/pull/25661) build(deps-dev): bump @babel/traverse from 7.16.0 to 7.23.2 in /superset-websocket (@dependabot[bot])
- [#25653](https://github.com/apache/superset/pull/25653) build(deps-dev): bump @types/node from 20.8.5 to 20.8.6 in /superset-websocket (@dependabot[bot])
- [#25645](https://github.com/apache/superset/pull/25645) chore: bump pip-tools (@villebro)
- [#25537](https://github.com/apache/superset/pull/25537) docs: invert logo color for dark theme in README (@Sea-n)
- [#25629](https://github.com/apache/superset/pull/25629) chore: adding resource links to readme (@rusackas)
- [#25638](https://github.com/apache/superset/pull/25638) build(ci): Provide diff for pre-commit failures (@jsoref)
- [#25632](https://github.com/apache/superset/pull/25632) build(deps-dev): bump @types/node from 20.8.4 to 20.8.5 in /superset-websocket (@dependabot[bot])
- [#25455](https://github.com/apache/superset/pull/25455) chore(helm): spelling: initialize (@jsoref)
- [#25567](https://github.com/apache/superset/pull/25567) docs: BugHerd Tasks 88, 89, 90, 91 (@mdeshmu)
- [#25602](https://github.com/apache/superset/pull/25602) chore(feature?): Bump `scarf-js` to 1.3.0 to get more telemetry data (@rusackas)
- [#25502](https://github.com/apache/superset/pull/25502) build(deps): bump postcss from 8.3.11 to 8.4.31 in /docs (@dependabot[bot])
- [#19056](https://github.com/apache/superset/pull/19056) docs: Add timezone information (@john-bodley)
- [#25340](https://github.com/apache/superset/pull/25340) refactor: Issue #25040; Refactored sync_role_definition function in order to reduce number of query. (@suicide11)
- [#25606](https://github.com/apache/superset/pull/25606) build(deps-dev): bump @types/ws from 8.5.6 to 8.5.7 in /superset-websocket (@dependabot[bot])
- [#25585](https://github.com/apache/superset/pull/25585) build(deps): bump winston from 3.10.0 to 3.11.0 in /superset-websocket (@dependabot[bot])
- [#25584](https://github.com/apache/superset/pull/25584) build(deps-dev): bump @types/node from 20.8.2 to 20.8.4 in /superset-websocket (@dependabot[bot])
- [#25566](https://github.com/apache/superset/pull/25566) chore: Update pylint to 2.17.7 (@EugeneTorap)
- [#25574](https://github.com/apache/superset/pull/25574) build(deps-dev): bump eslint from 8.49.0 to 8.51.0 in /superset-websocket (@dependabot[bot])
- [#25228](https://github.com/apache/superset/pull/25228) chore(sqllab): Typescript for SqlEditor component (@justinpark)
- [#25507](https://github.com/apache/superset/pull/25507) chore(tags): don't allow users to create new tags from property dropdowns (@hughhhh)
- [#25504](https://github.com/apache/superset/pull/25504) chore(tags): move tags column in dashboard and chart list (@lilykuang)
- [#24481](https://github.com/apache/superset/pull/24481) docs: fix for domain sharding results in failed requests with "Missing Authorization Header" (@ved-kashyap-samsung)
- [#25508](https://github.com/apache/superset/pull/25508) build(deps): bump ws and @types/ws in /superset-websocket (@dependabot[bot])
- [#25498](https://github.com/apache/superset/pull/25498) build(deps-dev): bump @types/node from 20.6.0 to 20.8.2 in /superset-websocket (@dependabot[bot])
- [#25480](https://github.com/apache/superset/pull/25480) docs: define localhost for docker (@mdeshmu)
- [#25479](https://github.com/apache/superset/pull/25479) docs: update docker compose instructions (@mdeshmu)
- [#25482](https://github.com/apache/superset/pull/25482) docs: add a FAQ about asset recovery from UI (@mdeshmu)
- [#25477](https://github.com/apache/superset/pull/25477) docs: add https & ldap instructions (@mdeshmu)
- [#25466](https://github.com/apache/superset/pull/25466) chore(async): Initial Refactoring of Global Async Queries (@craig-rueda)
- [#25120](https://github.com/apache/superset/pull/25120) build(deps-dev): bump prettier from 3.0.2 to 3.0.3 in /superset-websocket (@dependabot[bot])
- [#25325](https://github.com/apache/superset/pull/25325) build(deps-dev): bump @types/jsonwebtoken from 9.0.2 to 9.0.3 in /superset-websocket (@dependabot[bot])
- [#25435](https://github.com/apache/superset/pull/25435) docs(FAQ): remove reference to filter box, add Q&A re: usage analytics (@sfirke)
- [#25438](https://github.com/apache/superset/pull/25438) chore: Update Explore tooltip copy (@yousoph)
- [#25465](https://github.com/apache/superset/pull/25465) chore(misc): Typos in config.py (@JZ6)
- [#25457](https://github.com/apache/superset/pull/25457) chore(backend): Spelling (@jsoref)
- [#25456](https://github.com/apache/superset/pull/25456) chore(misc): Spelling (@jsoref)
- [#25453](https://github.com/apache/superset/pull/25453) chore(docs): Spelling (@jsoref)
- [#25441](https://github.com/apache/superset/pull/25441) build(deps): bump get-func-name from 2.0.0 to 2.0.2 in /superset-frontend/cypress-base (@dependabot[bot])
- [#25276](https://github.com/apache/superset/pull/25276) chore: cryptography version bump (@lilykuang)
- [#25332](https://github.com/apache/superset/pull/25332) docs: update docker-compose (@nytai)
- [#25362](https://github.com/apache/superset/pull/25362) chore: upgrade node to most recent 16.x (@villebro)
- [#25360](https://github.com/apache/superset/pull/25360) chore: Adds 3.0 data to CHANGELOG and UPDATING (@michael-s-molina)
- [#25346](https://github.com/apache/superset/pull/25346) chore(async): Making create app configurable (@craig-rueda)
- [#24928](https://github.com/apache/superset/pull/24928) docs: jwks_uri addition to OAUTH provider (@kravi21)
- [#25312](https://github.com/apache/superset/pull/25312) docs: add snowflake-sqlalchemy in ./docker/requirements-local.txt (@janhavitripurwar)
- [#25324](https://github.com/apache/superset/pull/25324) docs: add ReadyTech to INTHEWILD.md (@jbat)
- [#25313](https://github.com/apache/superset/pull/25313) chore: bump gunicorn to v21 (@villebro)
- [#25311](https://github.com/apache/superset/pull/25311) build(deps-dev): bump @types/uuid from 9.0.3 to 9.0.4 in /superset-websocket (@dependabot[bot])
- [#25274](https://github.com/apache/superset/pull/25274) chore(sqllab): Migrate tests to typescript (@justinpark)
- [#25291](https://github.com/apache/superset/pull/25291) chore: changing one word (disablement -> disabling) (@rusackas)
- [#25287](https://github.com/apache/superset/pull/25287) build(docker): bump geckodriver and firefox to latest (@alekseyolg)
- [#25293](https://github.com/apache/superset/pull/25293) build(deps): bump ws from 8.13.0 to 8.14.1 in /superset-websocket (@dependabot[bot])
- [#25296](https://github.com/apache/superset/pull/25296) docs: rewrite superset docker localhost prose (@jsoref)
- [#25279](https://github.com/apache/superset/pull/25279) build(deps): bump uuid from 9.0.0 to 9.0.1 in /superset-websocket (@dependabot[bot])
- [#25263](https://github.com/apache/superset/pull/25263) build(deps-dev): bump eslint from 8.48.0 to 8.49.0 in /superset-websocket (@dependabot[bot])
- [#25253](https://github.com/apache/superset/pull/25253) build(deps-dev): bump @types/node from 20.5.7 to 20.6.0 in /superset-websocket (@dependabot[bot])
- [#20631](https://github.com/apache/superset/pull/20631) refactor: Remove obsolete HiveEngineSpec.fetch_logs method (@john-bodley)
- [#25226](https://github.com/apache/superset/pull/25226) chore(read_csv): remove deprecated argument (@betodealmeida)
- [#25177](https://github.com/apache/superset/pull/25177) chore: Convert deckgl class components to functional (@kgabryje)
- [#24992](https://github.com/apache/superset/pull/24992) docs(FAQ): add answer re: necessary specs, copy-edit existing answer (@sfirke)
- [#25165](https://github.com/apache/superset/pull/25165) chore: back port 2.1.1 doc changes (@eschutho)
- [#25206](https://github.com/apache/superset/pull/25206) docs: add CVEs for 2.1.1 (@dpgaspar)
- [#25200](https://github.com/apache/superset/pull/25200) docs: fix wrong type in PREFERRED_DATABASES example (@cmontemuino)
- [#25160](https://github.com/apache/superset/pull/25160) chore: fix broken link to Celery worker docs (@wAVeckx)
- [#25142](https://github.com/apache/superset/pull/25142) build(deps-dev): bump @types/uuid from 9.0.2 to 9.0.3 in /superset-websocket (@dependabot[bot])
- [#25141](https://github.com/apache/superset/pull/25141) build(deps): bump jsonwebtoken from 9.0.1 to 9.0.2 in /superset-websocket (@dependabot[bot])
- [#25140](https://github.com/apache/superset/pull/25140) build(deps): bump jsonwebtoken from 9.0.1 to 9.0.2 in /superset-websocket/utils/client-ws-app (@dependabot[bot])
- [#25088](https://github.com/apache/superset/pull/25088) chore: consolidate sqllab store into SPA store (@justinpark)
- [#25121](https://github.com/apache/superset/pull/25121) chore: move TypedDict from typing_extensions to typing (@sebastianliebscher)
- [#24896](https://github.com/apache/superset/pull/24896) chore: use contextlib.surpress instead of passing on error (@sebastianliebscher)
- [#25098](https://github.com/apache/superset/pull/25098) build(deps-dev): bump eslint from 8.47.0 to 8.48.0 in /superset-websocket (@dependabot[bot])
- [#25097](https://github.com/apache/superset/pull/25097) build(deps-dev): bump @types/node from 20.5.6 to 20.5.7 in /superset-websocket (@dependabot[bot])
- [#24933](https://github.com/apache/superset/pull/24933) chore: Refactor deck.gl plugins to Typescript (@kgabryje)
- [#24980](https://github.com/apache/superset/pull/24980) chore: Update docs for docker-compose installation (@hughhhh)
- [#24771](https://github.com/apache/superset/pull/24771) docs(docker-compose): add missing parenthesis (@sfirke)
- [#25082](https://github.com/apache/superset/pull/25082) build(deps-dev): bump @types/node from 20.5.1 to 20.5.6 in /superset-websocket (@dependabot[bot])
- [#25080](https://github.com/apache/superset/pull/25080) chore(reports): add metrics to report schedule and log prune (@villebro)
- [#25047](https://github.com/apache/superset/pull/25047) chore(sqllab): typescript for getInitialState (@justinpark)
- [#25030](https://github.com/apache/superset/pull/25030) build(deps): Bump PyHive (@mdeshmu)
- [#24872](https://github.com/apache/superset/pull/24872) test(cypress): Fail Cypress on Console errors (@rusackas)
- [#25046](https://github.com/apache/superset/pull/25046) chore: Organizes the files of the ReportModal feature (@michael-s-molina)
- [#25045](https://github.com/apache/superset/pull/25045) chore(tests): Adding missing **init**.py files to various test packages (@craig-rueda)
- [#25038](https://github.com/apache/superset/pull/25038) build(deps-dev): bump @types/node from 20.5.0 to 20.5.1 in /superset-websocket (@dependabot[bot])
- [#25010](https://github.com/apache/superset/pull/25010) chore(sqllab): Relocate user in SqlLab to root (@justinpark)
- [#25034](https://github.com/apache/superset/pull/25034) docs: fix line break in Apache Druid page (@giuliotal)
- [#24994](https://github.com/apache/superset/pull/24994) chore: rename `get_iterable` (@betodealmeida)
- [#25007](https://github.com/apache/superset/pull/25007) chore: Removes Saved Query old code (@michael-s-molina)
- [#24894](https://github.com/apache/superset/pull/24894) chore: Update DAOs to use singular deletion method instead of bulk (@jfrag1)
- [#25005](https://github.com/apache/superset/pull/25005) chore: Removes src/modules top folder (@michael-s-molina)
- [#24998](https://github.com/apache/superset/pull/24998) build(deps-dev): bump prettier from 3.0.1 to 3.0.2 in /superset-websocket (@dependabot[bot])
- [#24941](https://github.com/apache/superset/pull/24941) chore(dashboard import/export): include additional fields to export/import commands (@Vitor-Avila)
- [#24967](https://github.com/apache/superset/pull/24967) chore(dao): Remove redundant convenience methods (@john-bodley)
- [#24973](https://github.com/apache/superset/pull/24973) build(deps-dev): bump @types/node from 20.4.9 to 20.5.0 in /superset-websocket (@dependabot[bot])
- [#24972](https://github.com/apache/superset/pull/24972) build(deps-dev): bump eslint from 8.46.0 to 8.47.0 in /superset-websocket (@dependabot[bot])
- [#24936](https://github.com/apache/superset/pull/24936) chore(sqllab): Relocate get bootstrap data logic (@justinpark)
- [#24467](https://github.com/apache/superset/pull/24467) chore(dao): Replace save/overwrite with create/update respectively (@john-bodley)
- [#24962](https://github.com/apache/superset/pull/24962) docs: Add wattbewerb to users list (@hbruch)
- [#24961](https://github.com/apache/superset/pull/24961) chore: Add Automattic to the list of users and contributors (@Khrol)
- [#24958](https://github.com/apache/superset/pull/24958) build(deps): bump tough-cookie and @cypress/request in /superset-frontend/cypress-base (@dependabot[bot])
- [#24920](https://github.com/apache/superset/pull/24920) docs: Fixing Superset typo in docker-compose local installation guide (@TannerBarcelos)
- [#24924](https://github.com/apache/superset/pull/24924) build(deps-dev): bump @types/node from 20.4.8 to 20.4.9 in /superset-websocket (@dependabot[bot])
- [#24915](https://github.com/apache/superset/pull/24915) docs: fix tip box in "Installing From Scratch" page (@giuliotal)
- [#24878](https://github.com/apache/superset/pull/24878) build(deps-dev): bump prettier from 2.8.8 to 3.0.1 in /superset-websocket (@dependabot[bot])
- [#24900](https://github.com/apache/superset/pull/24900) build(deps-dev): bump eslint-config-prettier from 8.10.0 to 9.0.0 in /superset-websocket (@dependabot[bot])
- [#24901](https://github.com/apache/superset/pull/24901) build(deps-dev): bump @types/node from 20.4.7 to 20.4.8 in /superset-websocket (@dependabot[bot])
- [#24888](https://github.com/apache/superset/pull/24888) build(deps-dev): bump @types/node from 20.4.6 to 20.4.7 in /superset-websocket (@dependabot[bot])
- [#24880](https://github.com/apache/superset/pull/24880) build(deps-dev): bump @types/node from 20.4.5 to 20.4.6 in /superset-websocket (@dependabot[bot])
- [#24879](https://github.com/apache/superset/pull/24879) build(deps-dev): bump eslint-config-prettier from 8.8.0 to 8.10.0 in /superset-websocket (@dependabot[bot])
- [#24873](https://github.com/apache/superset/pull/24873) docs(native-filters): Remove outdated statement (@john-bodley)
- [#24657](https://github.com/apache/superset/pull/24657) chore: Bump cryptography (@suryadev99)
- [#24842](https://github.com/apache/superset/pull/24842) build(deps-dev): bump eslint from 8.45.0 to 8.46.0 in /superset-websocket (@dependabot[bot])
- [#24838](https://github.com/apache/superset/pull/24838) chore(api): clean up API spec (@sebastianliebscher)
- [#24834](https://github.com/apache/superset/pull/24834) docs(Kubernetes): Fix typos, clarify language re: Scarf (@sfirke)
- [#24819](https://github.com/apache/superset/pull/24819) chore: remove get_columns_description duplication (@betodealmeida)
- [#24817](https://github.com/apache/superset/pull/24817) docs: Adding a couple links to contributing page (@rusackas)
- [#24820](https://github.com/apache/superset/pull/24820) docs: fixing stack overflow link (@rusackas)
- [#24809](https://github.com/apache/superset/pull/24809) build(deps-dev): bump @types/node from 20.4.4 to 20.4.5 in /superset-websocket (@dependabot[bot])
- [#19959](https://github.com/apache/superset/pull/19959) docs(K8s): Add instructions for loading the examples (@charris-msft)
- [#24147](https://github.com/apache/superset/pull/24147) chore: bump postgresql in docker-compose and github workflows (@sebastianliebscher)
- [#24779](https://github.com/apache/superset/pull/24779) build(deps-dev): bump @types/node from 20.4.2 to 20.4.4 in /superset-websocket (@dependabot[bot])
- [#24751](https://github.com/apache/superset/pull/24751) docs: update AWS Athena and Redshift docs (@mdeshmu)
- [#24461](https://github.com/apache/superset/pull/24461) docs(docker-compose): note the risk of running a Docker Postgres volume in production (@sfirke)
- [#24705](https://github.com/apache/superset/pull/24705) chore(deps): bump pandas >=2.0 (@sebastianliebscher)
- [#24732](https://github.com/apache/superset/pull/24732) build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 in /superset-websocket (@dependabot[bot])
- [#24715](https://github.com/apache/superset/pull/24715) chore: update deprecated arguments in schema (@sebastianliebscher)
- [#24733](https://github.com/apache/superset/pull/24733) build(deps): bump word-wrap from 1.2.3 to 1.2.4 in /superset-frontend/cypress-base (@dependabot[bot])
- [#24735](https://github.com/apache/superset/pull/24735) build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 in /superset-frontend (@dependabot[bot])
- [#24734](https://github.com/apache/superset/pull/24734) build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 in /superset-embedded-sdk (@dependabot[bot])
- [#24712](https://github.com/apache/superset/pull/24712) build(deps-dev): bump eslint from 8.44.0 to 8.45.0 in /superset-websocket (@dependabot[bot])
- [#24669](https://github.com/apache/superset/pull/24669) chore: remove obsolete fetchExploreJson function (@john-bodley)
- [#24682](https://github.com/apache/superset/pull/24682) build(deps-dev): bump @types/node from 20.4.1 to 20.4.2 in /superset-websocket (@dependabot[bot])
- [#24674](https://github.com/apache/superset/pull/24674) docs: Fix typo in Rockset docs (@gadhagod)
- [#24672](https://github.com/apache/superset/pull/24672) build(deps-dev): bump @types/node from 20.4.0 to 20.4.1 in /superset-websocket (@dependabot[bot])
- [#24673](https://github.com/apache/superset/pull/24673) build(deps-dev): bump @typescript-eslint/parser from 5.61.0 to 5.62.0 in /superset-websocket (@dependabot[bot])
- [#24649](https://github.com/apache/superset/pull/24649) chore: Update Rockset—switching out rockset for rockset-sqlalchemy (@gadhagod)
- [#24653](https://github.com/apache/superset/pull/24653) build(deps): bump semver from 5.7.1 to 5.7.2 in /superset-frontend (@dependabot[bot])
- [#24654](https://github.com/apache/superset/pull/24654) build(deps): bump semver from 6.3.0 to 6.3.1 in /superset-websocket (@dependabot[bot])
- [#24655](https://github.com/apache/superset/pull/24655) build(deps): bump semver from 6.3.0 to 6.3.1 in /superset-frontend/cypress-base (@dependabot[bot])
- [#24656](https://github.com/apache/superset/pull/24656) build(deps): bump trim and @superset-ui/core in /superset-frontend/cypress-base (@dependabot[bot])
- [#24659](https://github.com/apache/superset/pull/24659) build(deps): bump winston from 3.9.0 to 3.10.0 in /superset-websocket (@dependabot[bot])
- [#24626](https://github.com/apache/superset/pull/24626) chore: Re-enable some GitHub action workflows in draft mode (@john-bodley)
- [#24633](https://github.com/apache/superset/pull/24633) docs(databases): correct the way of using use environment variables (@duyet)
- [#24648](https://github.com/apache/superset/pull/24648) chore: update UI dev libs and fix warnings & vulnerabilities (@EugeneTorap)
- [#24651](https://github.com/apache/superset/pull/24651) build(deps): bump semver from 5.7.1 to 5.7.2 in /docs (@dependabot[bot])
- [#24634](https://github.com/apache/superset/pull/24634) build(deps): bump tough-cookie from 4.0.0 to 4.1.3 in /superset-embedded-sdk (@dependabot[bot])
- [#24614](https://github.com/apache/superset/pull/24614) build(deps): bump jsonwebtoken from 9.0.0 to 9.0.1 in /superset-websocket (@dependabot[bot])
- [#23987](https://github.com/apache/superset/pull/23987) docs(frontend): Fixed typo in command (@ved-kashyap-samsung)
- [#23992](https://github.com/apache/superset/pull/23992) docs: correct databricks pip package name (@devonkinghorn)
- [#24632](https://github.com/apache/superset/pull/24632) build(deps): bump tough-cookie from 4.0.0 to 4.1.3 in /superset-websocket (@dependabot[bot])
- [#24585](https://github.com/apache/superset/pull/24585) build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.60.1 to 5.61.0 in /superset-websocket (@dependabot[bot])
- [#24576](https://github.com/apache/superset/pull/24576) build(deps-dev): bump eslint from 8.43.0 to 8.44.0 in /superset-websocket (@dependabot[bot])
- [#24601](https://github.com/apache/superset/pull/24601) build(deps-dev): bump @types/node from 20.3.2 to 20.4.0 in /superset-websocket (@dependabot[bot])
- [#24584](https://github.com/apache/superset/pull/24584) build(deps-dev): bump @typescript-eslint/parser from 5.60.1 to 5.61.0 in /superset-websocket (@dependabot[bot])
- [#24600](https://github.com/apache/superset/pull/24600) build(deps): bump jsonwebtoken from 9.0.0 to 9.0.1 in /superset-websocket/utils/client-ws-app (@dependabot[bot])
- [#24570](https://github.com/apache/superset/pull/24570) docs(helm): reference the correct chart (@muniter)
- [#24564](https://github.com/apache/superset/pull/24564) docs: add notice not to use gevent worker with bigquery datasource (@okayhooni)
- [#24578](https://github.com/apache/superset/pull/24578) refactor: pkg_resources -> importlib.resources (@cwegener)
- [#24523](https://github.com/apache/superset/pull/24523) build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.60.0 to 5.60.1 in /superset-websocket (@dependabot[bot])
### 3.0.3 (Fri Dec 8 05:40:09 2023 -0800)
**Fixes**
- [#26215](https://github.com/apache/superset/pull/26215) fix(plugin-chart-echarts): support truncated numeric x-axis (@villebro)
- [#26199](https://github.com/apache/superset/pull/26199) fix(chart-filter): Avoid column denormalization if not enabled (@Vitor-Avila)
- [#26211](https://github.com/apache/superset/pull/26211) fix: support custom links in markdown (@villebro)
- [#26189](https://github.com/apache/superset/pull/26189) fix(dashboard): title formatting (@nytai)
- [#26207](https://github.com/apache/superset/pull/26207) fix: Includes 90° x-axis label rotation (@michael-s-molina)
- [#26157](https://github.com/apache/superset/pull/26157) fix(init-job): Fix envFrom for init job in helm chart (@sumagoudb)
- [#25878](https://github.com/apache/superset/pull/25878) fix(embedded): Hide sensitive payload data from guest users (@jfrag1)
- [#25894](https://github.com/apache/superset/pull/25894) fix(Alerts/Reports): allow use of ";" separator in slack recipient entry (@rtexelm)
- [#26116](https://github.com/apache/superset/pull/26116) fix(database-import): Support importing a DB connection with a version set (@Vitor-Avila)
- [#26154](https://github.com/apache/superset/pull/26154) fix: set label on adhoc column should persist (@betodealmeida)
- [#26140](https://github.com/apache/superset/pull/26140) fix(annotations): time grain column (@betodealmeida)
- [#23916](https://github.com/apache/superset/pull/23916) fix: remove default secret key from helm (@dpgaspar)
- [#26120](https://github.com/apache/superset/pull/26120) fix: alias column when fetching values (@betodealmeida)
- [#26106](https://github.com/apache/superset/pull/26106) fix: flaky test_explore_json_async test v2 (@villebro)
- [#26091](https://github.com/apache/superset/pull/26091) fix: bump node-fetch to 2.6.7 (@dpgaspar)
- [#26087](https://github.com/apache/superset/pull/26087) fix(plugin-chart-echarts): support numerical x-axis (@villebro)
- [#26059](https://github.com/apache/superset/pull/26059) fix: Flaky test_explore_json_async test (@michael-s-molina)
- [#26023](https://github.com/apache/superset/pull/26023) fix: Prevent cached bootstrap data from leaking between users w/ same first/last name (@jfrag1)
- [#26060](https://github.com/apache/superset/pull/26060) fix: Optimize fetching samples logic (@john-bodley)
- [#26010](https://github.com/apache/superset/pull/26010) fix: Remove annotation Fuzzy to get french translation (@aehanno)
- [#26005](https://github.com/apache/superset/pull/26005) fix(security): restore default value of SESSION_COOKIE_SECURE to False (@sfirke)
- [#25883](https://github.com/apache/superset/pull/25883) fix(horizontal filter bar filter labels): Increase max-width to 96px (@rtexelm)
**Others**
- [#26208](https://github.com/apache/superset/pull/26208) chore: Adds note about numerical x-axis (@michael-s-molina)
- [#26158](https://github.com/apache/superset/pull/26158) chore: Clean up the examples dashboards (@michael-s-molina)
- [#25931](https://github.com/apache/superset/pull/25931) chore(deps): bump pillow deps (@gnought)
### 3.0.2 (Mon Nov 20 07:38:38 2023 -0500)
**Fixes**

View File

@@ -22,18 +22,18 @@ under the License.
This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
## 3.1.0
- [24657](https://github.com/apache/superset/pull/24657): Bumps the cryptography package to augment the OpenSSL security vulnerability.
### Breaking Changes
### Potential Downtime
### Other
- [24982](https://github.com/apache/superset/pull/24982): By default, physical datasets on Oracle-like dialects like Snowflake will now use denormalized column names. However, existing datasets won't be affected. To change this behavior, the "Advanced" section on the dataset modal has a "Normalize column names" flag which can be changed to change this behavior.
## 3.0.3
- [26034](https://github.com/apache/superset/issues/26034): Fixes a problem where numeric x-axes were being treated as categorical values. As a consequence of that, the way labels are displayed might change given that ECharts has a different treatment for numerical and categorical values. To revert to the old behavior, users need to manually convert numerical columns to text so that they are treated as categories. Check https://github.com/apache/superset/issues/26159 for more details.
## 3.0.0
- [25053](https://github.com/apache/superset/pull/25053): Extends the `ab_user.email` column from 64 to 320 characters which has an associated unique key constraint. This will be problematic for MySQL metadata databases which use the InnoDB storage engine with the `innodb_large_prefix` parameter disabled as the key prefix limit is 767 bytes. Enabling said parameter and ensuring that the table uses either the `DYNAMIC` or `COMPRESSED` row format should remedy the problem. See [here](https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html) for more details.

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.10.15
version: 0.11.2
dependencies:
- name: postgresql
version: 12.1.6

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.10.15](https://img.shields.io/badge/Version-0.10.15-informational?style=flat-square)
![Version: 0.11.2](https://img.shields.io/badge/Version-0.11.2-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -40,6 +40,12 @@ helm repo add superset http://apache.github.io/superset/
helm install my-superset superset/superset
```
Make sure you set your own `SECRET_KEY` to something unique and secret. This secret key is used by Flask for
securely signing the session cookie and will be used to encrypt sensitive data on Superset's metadata database.
It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
## Requirements
| Repository | Name | Version |
@@ -124,6 +130,7 @@ helm install my-superset superset/superset
| supersetCeleryBeat.containerSecurityContext | object | `{}` | |
| supersetCeleryBeat.deploymentAnnotations | object | `{}` | Annotations to be added to supersetCeleryBeat deployment |
| supersetCeleryBeat.enabled | bool | `false` | This is only required if you intend to use alerts and reports |
| supersetCeleryBeat.extraContainers | list | `[]` | Launch additional containers into supersetCeleryBeat pods |
| supersetCeleryBeat.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetCeleryBeat.initContainers | list | a container waiting for postgres | List of init containers |
| supersetCeleryBeat.podAnnotations | object | `{}` | Annotations to be added to supersetCeleryBeat pods |
@@ -136,6 +143,7 @@ helm install my-superset superset/superset
| supersetCeleryFlower.containerSecurityContext | object | `{}` | |
| supersetCeleryFlower.deploymentAnnotations | object | `{}` | Annotations to be added to supersetCeleryFlower deployment |
| supersetCeleryFlower.enabled | bool | `false` | Enables a Celery flower deployment (management UI to monitor celery jobs) WARNING: on superset 1.x, this requires a Superset image that has `flower<1.0.0` installed (which is NOT the case of the default images) flower>=1.0.0 requires Celery 5+ which Superset 1.5 does not support |
| supersetCeleryFlower.extraContainers | list | `[]` | Launch additional containers into supersetCeleryFlower pods |
| supersetCeleryFlower.initContainers | list | a container waiting for postgres and redis | List of init containers |
| supersetCeleryFlower.livenessProbe.failureThreshold | int | `3` | |
| supersetCeleryFlower.livenessProbe.httpGet.path | string | `"/api/workers"` | |
@@ -223,6 +231,7 @@ helm install my-superset superset/superset
| supersetWebsockets.containerSecurityContext | object | `{}` | |
| supersetWebsockets.deploymentAnnotations | object | `{}` | |
| supersetWebsockets.enabled | bool | `false` | This is only required if you intend to use `GLOBAL_ASYNC_QUERIES` in `ws` mode see https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries |
| supersetWebsockets.extraContainers | list | `[]` | Launch additional containers into supersetWebsockets pods |
| supersetWebsockets.image.pullPolicy | string | `"IfNotPresent"` | |
| supersetWebsockets.image.repository | string | `"oneacrefund/superset-websocket"` | There is no official image (yet), this one is community-supported |
| supersetWebsockets.image.tag | string | `"latest"` | |

View File

@@ -39,6 +39,12 @@ helm repo add superset http://apache.github.io/superset/
helm install my-superset superset/superset
```
Make sure you set your own `SECRET_KEY` to something unique and secret. This secret key is used by Flask for
securely signing the session cookie and will be used to encrypt sensitive data on Superset's metadata database.
It should be a long random bytes or str.
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}

View File

@@ -82,7 +82,6 @@ DATA_CACHE_CONFIG = CACHE_CONFIG
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{env('DB_USER')}:{env('DB_PASS')}@{env('DB_HOST')}:{env('DB_PORT')}/{env('DB_NAME')}"
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = env('SECRET_KEY', 'thisISaSECRET_1234')
class CeleryConfig:
imports = ("superset.sql_lab", )

View File

@@ -120,6 +120,9 @@ spec:
{{- else }}
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetCeleryBeat.extraContainers }}
{{- toYaml .Values.supersetCeleryBeat.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -115,6 +115,9 @@ spec:
{{- else }}
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
{{- if .Values.supersetCeleryFlower.extraContainers }}
{{- toYaml .Values.supersetCeleryFlower.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -114,6 +114,9 @@ spec:
{{- if .Values.supersetWebsockets.livenessProbe }}
livenessProbe: {{- .Values.supersetWebsockets.livenessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetWebsockets.extraContainers }}
{{- toYaml .Values.supersetWebsockets.extraContainers | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector: {{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -63,7 +63,7 @@ spec:
name: {{ tpl .Values.envFromSecret . }}
{{- range .Values.envFromSecrets }}
- secretRef:
name: {{ tpl . $ }}
name: {{ tpl . $ | quote }}
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.init.containerSecurityContext }}

View File

@@ -93,6 +93,8 @@ extraSecretEnv: {}
# # Google API Keys: https://console.cloud.google.com/apis/credentials
# GOOGLE_KEY: ...
# GOOGLE_SECRET: ...
# # Generate your own secret key for encryption. Use openssl rand -base64 42 to generate a good key
# SUPERSET_SECRET_KEY: 'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET'
# -- Extra files to mount on `/app/pythonpath`
extraConfigs: {}
@@ -441,6 +443,8 @@ supersetCeleryBeat:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
# -- Launch additional containers into supersetCeleryBeat pods
extraContainers: []
# -- Annotations to be added to supersetCeleryBeat deployment
deploymentAnnotations: {}
# -- Affinity to be added to supersetCeleryBeat deployment
@@ -522,6 +526,8 @@ supersetCeleryFlower:
- /bin/sh
- -c
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
# -- Launch additional containers into supersetCeleryFlower pods
extraContainers: []
# -- Annotations to be added to supersetCeleryFlower deployment
deploymentAnnotations: {}
# -- Affinity to be added to supersetCeleryFlower deployment
@@ -588,6 +594,8 @@ supersetWebsockets:
http: nil
command: []
resources: {}
# -- Launch additional containers into supersetWebsockets pods
extraContainers: []
deploymentAnnotations: {}
# -- Affinity to be added to supersetWebsockets deployment
affinity: {}

View File

@@ -252,7 +252,7 @@ prison==0.2.1
# via flask-appbuilder
prompt-toolkit==3.0.38
# via click-repl
pyarrow==12.0.0
pyarrow==14.0.1
# via apache-superset
pycparser==2.20
# via cffi
@@ -371,6 +371,7 @@ werkzeug==2.3.3
# via
# apache-superset
# flask
# flask-appbuilder
# flask-jwt-extended
# flask-login
wrapt==1.15.0

View File

@@ -113,7 +113,7 @@ setup(
"python-dateutil",
"python-dotenv",
"python-geohash",
"pyarrow>=12.0.0, <13",
"pyarrow>=14.0.1, <15",
"pyyaml>=6.0.0, <7.0.0",
"PyJWT>=2.4.0, <3.0",
"redis>=4.5.4, <5.0",
@@ -146,6 +146,7 @@ setup(
"cockroachdb": ["cockroachdb>=0.3.5, <0.4"],
"cors": ["flask-cors>=2.0.0"],
"crate": ["crate[sqlalchemy]>=0.26.0, <0.27"],
"databend": ["databend-sqlalchemy>=0.3.2, <1.0"],
"databricks": [
"databricks-sql-connector>=2.0.2, <3",
"sqlalchemy-databricks>=0.2.0",
@@ -201,7 +202,7 @@ setup(
"thrift>=0.14.1, <1.0.0",
],
"teradata": ["teradatasql>=16.20.0.23"],
"thumbnails": ["Pillow>=9.5.0, <10.0.0"],
"thumbnails": ["Pillow>=10.0.1, <11"],
"vertica": ["sqlalchemy-vertica-python>=0.5.9, < 0.6"],
"netezza": ["nzalchemy>=11.0.2"],
"starrocks": ["starrocks>=1.0.0"],

View File

@@ -29,10 +29,9 @@ describe('Alert list view', () => {
cy.getBySel('sort-header').eq(2).contains('Name');
cy.getBySel('sort-header').eq(3).contains('Schedule');
cy.getBySel('sort-header').eq(4).contains('Notification method');
cy.getBySel('sort-header').eq(5).contains('Created by');
cy.getBySel('sort-header').eq(6).contains('Owners');
cy.getBySel('sort-header').eq(7).contains('Modified');
cy.getBySel('sort-header').eq(8).contains('Active');
cy.getBySel('sort-header').eq(5).contains('Owners');
cy.getBySel('sort-header').eq(6).contains('Last modified');
cy.getBySel('sort-header').eq(7).contains('Active');
// TODO Cypress won't recognize the Actions column
// cy.getBySel('sort-header').eq(9).contains('Actions');
});

View File

@@ -29,10 +29,9 @@ describe('Report list view', () => {
cy.getBySel('sort-header').eq(2).contains('Name');
cy.getBySel('sort-header').eq(3).contains('Schedule');
cy.getBySel('sort-header').eq(4).contains('Notification method');
cy.getBySel('sort-header').eq(5).contains('Created by');
cy.getBySel('sort-header').eq(6).contains('Owners');
cy.getBySel('sort-header').eq(7).contains('Modified');
cy.getBySel('sort-header').eq(8).contains('Active');
cy.getBySel('sort-header').eq(5).contains('Owners');
cy.getBySel('sort-header').eq(6).contains('Last modified');
cy.getBySel('sort-header').eq(7).contains('Active');
// TODO Cypress won't recognize the Actions column
// cy.getBySel('sort-header').eq(9).contains('Actions');
});

View File

@@ -35,14 +35,14 @@ describe('Charts filters', () => {
setFilter('Owner', 'admin user');
});
it('should allow filtering by "Created by" correctly', () => {
setFilter('Created by', 'alpha user');
setFilter('Created by', 'admin user');
it('should allow filtering by "Modified by" correctly', () => {
setFilter('Modified by', 'alpha user');
setFilter('Modified by', 'admin user');
});
it('should allow filtering by "Chart type" correctly', () => {
setFilter('Chart type', 'Area Chart (legacy)');
setFilter('Chart type', 'Bubble Chart');
it('should allow filtering by "Type" correctly', () => {
setFilter('Type', 'Area Chart (legacy)');
setFilter('Type', 'Bubble Chart');
});
it('should allow filtering by "Dataset" correctly', () => {
@@ -51,7 +51,7 @@ describe('Charts filters', () => {
});
it('should allow filtering by "Dashboards" correctly', () => {
setFilter('Dashboards', 'Unicode Test');
setFilter('Dashboards', 'Tabbed Dashboard');
setFilter('Dashboard', 'Unicode Test');
setFilter('Dashboard', 'Tabbed Dashboard');
});
});

View File

@@ -109,14 +109,12 @@ describe('Charts list', () => {
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
cy.getBySel('sort-header').eq(1).contains('Chart');
cy.getBySel('sort-header').eq(2).contains('Visualization type');
cy.getBySel('sort-header').eq(1).contains('Name');
cy.getBySel('sort-header').eq(2).contains('Type');
cy.getBySel('sort-header').eq(3).contains('Dataset');
// cy.getBySel('sort-header').eq(4).contains('Dashboards added to');
cy.getBySel('sort-header').eq(4).contains('Modified by');
cy.getBySel('sort-header').eq(4).contains('Owners');
cy.getBySel('sort-header').eq(5).contains('Last modified');
cy.getBySel('sort-header').eq(6).contains('Created by');
cy.getBySel('sort-header').eq(7).contains('Actions');
cy.getBySel('sort-header').eq(6).contains('Actions');
});
it('should sort correctly in list mode', () => {

View File

@@ -113,7 +113,7 @@ function prepareDashboardFilters(
},
type: 'NATIVE_FILTER',
description: '',
chartsInScope: [6],
chartsInScope: [5],
tabsInScope: [],
});
});
@@ -150,7 +150,7 @@ function prepareDashboardFilters(
meta: {
width: 4,
height: 50,
chartId: 6,
chartId: 5,
sliceName: 'Most Populated Countries',
},
},
@@ -414,7 +414,7 @@ describe('Native filters', () => {
cy.createSampleDashboards([0]);
});
it('Verify that default value is respected after revisit', () => {
it.only('Verify that default value is respected after revisit', () => {
prepareDashboardFilters([
{ name: 'country_name', column: 'country_name', datasetId: 2 },
]);

View File

@@ -25,7 +25,6 @@ import { TABBED_DASHBOARD } from 'cypress/utils/urls';
import { expandFilterOnLeftPanel } from './utils';
const TREEMAP = { name: 'Treemap', viz: 'treemap_v2' };
const FILTER_BOX = { name: 'Region Filter', viz: 'filter_box' };
const LINE_CHART = { name: 'Growth Rate', viz: 'line' };
const BOX_PLOT = { name: 'Box plot', viz: 'box_plot' };
const BIG_NUMBER = { name: 'Number of Girls', viz: 'big_number_total' };
@@ -41,7 +40,6 @@ function topLevelTabs() {
function resetTabs() {
topLevelTabs();
cy.get('@top-level-tabs').first().click();
waitForChartLoad(FILTER_BOX);
waitForChartLoad(TREEMAP);
waitForChartLoad(BIG_NUMBER);
waitForChartLoad(TABLE);
@@ -96,7 +94,6 @@ describe('Dashboard tabs', () => {
it.skip('should send new queries when tab becomes visible', () => {
// landing in first tab
waitForChartLoad(FILTER_BOX);
waitForChartLoad(TREEMAP);
getChartAliasBySpec(TREEMAP).then(treemapAlias => {

View File

@@ -23,7 +23,6 @@ import { ChartSpec, waitForChartLoad } from 'cypress/utils';
export const WORLD_HEALTH_CHARTS = [
{ name: '% Rural', viz: 'world_map' },
{ name: 'Most Populated Countries', viz: 'table' },
{ name: 'Region Filter', viz: 'filter_box' },
{ name: "World's Population", viz: 'big_number' },
{ name: 'Growth Rate', viz: 'line' },
{ name: 'Rural Breakdown', viz: 'sunburst' },

View File

@@ -35,9 +35,9 @@ describe('Dashboards filters', () => {
setFilter('Owner', 'admin user');
});
it('should allow filtering by "Created by" correctly', () => {
setFilter('Created by', 'alpha user');
setFilter('Created by', 'admin user');
it('should allow filtering by "Modified by" correctly', () => {
setFilter('Modified by', 'alpha user');
setFilter('Modified by', 'admin user');
});
it('should allow filtering by "Status" correctly', () => {

View File

@@ -54,13 +54,11 @@ describe('Dashboards list', () => {
it('should load rows in list mode', () => {
cy.getBySel('listview-table').should('be.visible');
cy.getBySel('sort-header').eq(1).contains('Title');
cy.getBySel('sort-header').eq(2).contains('Modified by');
cy.getBySel('sort-header').eq(3).contains('Status');
cy.getBySel('sort-header').eq(4).contains('Modified');
cy.getBySel('sort-header').eq(5).contains('Created by');
cy.getBySel('sort-header').eq(6).contains('Owners');
cy.getBySel('sort-header').eq(7).contains('Actions');
cy.getBySel('sort-header').eq(1).contains('Name');
cy.getBySel('sort-header').eq(2).contains('Status');
cy.getBySel('sort-header').eq(3).contains('Owners');
cy.getBySel('sort-header').eq(4).contains('Last modified');
cy.getBySel('sort-header').eq(5).contains('Actions');
});
it('should sort correctly in list mode', () => {

View File

@@ -18,7 +18,7 @@
*/
import '@cypress/code-coverage/support';
import '@applitools/eyes-cypress/commands';
import failOnConsoleError, { Config } from 'cypress-fail-on-console-error';
import failOnConsoleError from 'cypress-fail-on-console-error';
require('cy-verify-downloads').addCustomCommand();

View File

@@ -1,6 +1,6 @@
{
"name": "superset",
"version": "0.0.0-dev",
"version": "3.1.0",
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
"keywords": [
"big",

View File

@@ -67,6 +67,7 @@ function SafeMarkdown({
rehypePlugins={rehypePlugins}
remarkPlugins={[remarkGfm]}
skipHtml={false}
transformLinkUri={null}
>
{source}
</ReactMarkdown>

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
import { defaultXAxis } from '../defaults';
import { EchartsBubbleFormData } from './types';
export const DEFAULT_FORM_DATA: Partial<EchartsBubbleFormData> = {
@@ -26,9 +27,10 @@ export const DEFAULT_FORM_DATA: Partial<EchartsBubbleFormData> = {
logYAxis: false,
xAxisTitleMargin: 30,
yAxisTitleMargin: 30,
truncateXAxis: false,
truncateYAxis: false,
yAxisBounds: [null, null],
xAxisLabelRotation: 0,
xAxisLabelRotation: defaultXAxis.xAxisLabelRotation,
opacity: 0.6,
};

View File

@@ -26,10 +26,15 @@ import {
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './constants';
import { legendSection } from '../controls';
import {
legendSection,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../controls';
import { defaultYAxis } from '../defaults';
const { logAxis, truncateYAxis, yAxisBounds, xAxisLabelRotation, opacity } =
DEFAULT_FORM_DATA;
const { logAxis, truncateYAxis, yAxisBounds, opacity } = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -127,26 +132,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
[
{
name: 'x_axis_title_margin',
@@ -211,7 +197,7 @@ const config: ControlPanelConfig = {
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
default: defaultYAxis.yAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
@@ -246,6 +232,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -28,9 +28,9 @@ import {
import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types';
import { DEFAULT_FORM_DATA, MINIMUM_BUBBLE_SIZE } from './constants';
import { defaultGrid } from '../defaults';
import { getLegendProps } from '../utils/series';
import { getLegendProps, getMinAndMaxFromBounds } from '../utils/series';
import { Refs } from '../types';
import { parseYAxisBound } from '../utils/controls';
import { parseAxisBound } from '../utils/controls';
import { getDefaultTooltip } from '../utils/tooltip';
import { getPadding } from '../Timeseries/transformers';
import { convertInteger } from '../utils/convertInteger';
@@ -84,6 +84,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
series: bubbleSeries,
xAxisLabel: bubbleXAxisTitle,
yAxisLabel: bubbleYAxisTitle,
xAxisBounds,
xAxisFormat,
yAxisFormat,
yAxisBounds,
@@ -91,6 +92,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
logYAxis,
xAxisTitleMargin,
yAxisTitleMargin,
truncateXAxis,
truncateYAxis,
xAxisLabelRotation,
yAxisLabelRotation,
@@ -141,7 +143,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
const yAxisFormatter = getNumberFormatter(yAxisFormat);
const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat);
const [min, max] = yAxisBounds.map(parseYAxisBound);
const [xAxisMin, xAxisMax] = xAxisBounds.map(parseAxisBound);
const [yAxisMin, yAxisMax] = yAxisBounds.map(parseAxisBound);
const padding = getPadding(
showLegend,
@@ -155,6 +158,7 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
convertInteger(xAxisTitleMargin),
);
const xAxisType = logXAxis ? AxisType.log : AxisType.value;
const echartOptions: EChartsCoreOption = {
series,
xAxis: {
@@ -172,7 +176,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
fontWight: 'bolder',
},
nameGap: convertInteger(xAxisTitleMargin),
type: logXAxis ? AxisType.log : AxisType.value,
type: xAxisType,
...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax),
},
yAxis: {
axisLabel: { formatter: yAxisFormatter },
@@ -189,8 +194,8 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
fontWight: 'bolder',
},
nameGap: convertInteger(yAxisTitleMargin),
min,
max,
min: yAxisMin,
max: yAxisMax,
type: logYAxis ? AxisType.log : AxisType.value,
},
legend: {

View File

@@ -32,7 +32,11 @@ import {
import { DEFAULT_FORM_DATA } from './types';
import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
import { legendSection, richTooltipSection } from '../controls';
import {
legendSection,
richTooltipSection,
xAxisLabelRotation,
} from '../controls';
const {
area,
@@ -49,7 +53,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
yAxisIndex,
} = DEFAULT_FORM_DATA;
@@ -314,26 +317,7 @@ const config: ControlPanelConfig = {
...legendSection,
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
['x_axis_time_format'],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],

View File

@@ -53,7 +53,7 @@ import {
ForecastSeriesEnum,
Refs,
} from '../types';
import { parseYAxisBound } from '../utils/controls';
import { parseAxisBound } from '../utils/controls';
import {
getOverMaxHiddenFormatter,
dedupSeries,
@@ -345,9 +345,9 @@ export default function transformProps(
});
// yAxisBounds need to be parsed to replace incompatible values with undefined
let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
let [min, max] = (yAxisBounds || []).map(parseAxisBound);
let [minSecondary, maxSecondary] = (yAxisBoundsSecondary || []).map(
parseYAxisBound,
parseAxisBound,
);
const array = ensureIsArray(chartProps.rawFormData?.time_compare);

View File

@@ -37,6 +37,9 @@ import {
richTooltipSection,
seriesOrderSection,
percentageThresholdControl,
xAxisLabelRotation,
truncateXAxis,
xAxisBounds,
} from '../../controls';
import { AreaChartStackControlOptions } from '../../constants';
@@ -51,7 +54,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -191,26 +193,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
@@ -240,6 +223,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -35,6 +35,9 @@ import {
richTooltipSection,
seriesOrderSection,
showValueSection,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../../../controls';
import { OrientationType } from '../../types';
@@ -49,7 +52,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
orientation,
} = DEFAULT_FORM_DATA;
@@ -163,21 +165,9 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
],
[
{
name: 'xAxisLabelRotation',
name: xAxisLabelRotation.name,
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
...xAxisLabelRotation.config,
visibility: ({ controls }: ControlPanelsContainerProps) =>
isXAxis ? isVertical(controls) : isHorizontal(controls),
},
@@ -223,6 +213,8 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -38,6 +38,9 @@ import {
richTooltipSection,
seriesOrderSection,
showValueSection,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../../../controls';
const {
@@ -52,7 +55,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -179,26 +181,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
@@ -228,6 +211,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -37,6 +37,9 @@ import {
richTooltipSection,
seriesOrderSection,
showValueSection,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../../../controls';
const {
@@ -48,7 +51,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -122,26 +124,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
// eslint-disable-next-line react/jsx-key
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
@@ -172,6 +155,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -37,6 +37,9 @@ import {
richTooltipSection,
seriesOrderSection,
showValueSectionWithoutStack,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../../../controls';
const {
@@ -48,7 +51,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -121,26 +123,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
// eslint-disable-next-line react/jsx-key
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
@@ -172,6 +155,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -35,6 +35,9 @@ import {
richTooltipSection,
seriesOrderSection,
showValueSection,
truncateXAxis,
xAxisBounds,
xAxisLabelRotation,
} from '../../controls';
const {
@@ -48,7 +51,6 @@ const {
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -173,26 +175,7 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
[xAxisLabelRotation],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<ControlSubSectionHeader>{t('Y Axis')}</ControlSubSectionHeader>],
@@ -222,6 +205,8 @@ const config: ControlPanelConfig = {
},
},
],
[truncateXAxis],
[xAxisBounds],
[
{
name: 'truncateYAxis',

View File

@@ -30,6 +30,7 @@ import {
DEFAULT_LEGEND_FORM_DATA,
DEFAULT_TITLE_FORM_DATA,
} from '../constants';
import { defaultXAxis } from '../defaults';
// @ts-ignore
export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
@@ -57,11 +58,12 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
seriesType: EchartsTimeseriesSeriesType.Line,
stack: false,
tooltipTimeFormat: 'smart_date',
truncateXAxis: true,
truncateYAxis: false,
yAxisBounds: [null, null],
zoomable: false,
richTooltip: true,
xAxisLabelRotation: 0,
xAxisLabelRotation: defaultXAxis.xAxisLabelRotation,
groupby: [],
showValue: false,
onlyTotal: false,

View File

@@ -54,7 +54,7 @@ import {
} from './types';
import { DEFAULT_FORM_DATA } from './constants';
import { ForecastSeriesEnum, ForecastValue, Refs } from '../types';
import { parseYAxisBound } from '../utils/controls';
import { parseAxisBound } from '../utils/controls';
import {
calculateLowerLogTick,
dedupSeries,
@@ -64,6 +64,7 @@ import {
getAxisType,
getColtypesMapping,
getLegendProps,
getMinAndMaxFromBounds,
} from '../utils/series';
import {
extractAnnotationLabels,
@@ -161,8 +162,10 @@ export default function transformProps(
stack,
tooltipTimeFormat,
tooltipSortByMetric,
truncateXAxis,
truncateYAxis,
xAxis: xAxisOrig,
xAxisBounds,
xAxisLabelRotation,
xAxisSortSeries,
xAxisSortSeriesAscending,
@@ -388,15 +391,20 @@ export default function transformProps(
}
});
// yAxisBounds need to be parsed to replace incompatible values with undefined
let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
// axis bounds need to be parsed to replace incompatible values with undefined
const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
// default to 0-100% range when doing row-level contribution chart
if ((contributionMode === 'row' || isAreaExpand) && stack) {
if (min === undefined) min = 0;
if (max === undefined) max = 1;
} else if (logAxis && min === undefined && minPositiveValue !== undefined) {
min = calculateLowerLogTick(minPositiveValue);
if (yAxisMin === undefined) yAxisMin = 0;
if (yAxisMax === undefined) yAxisMax = 1;
} else if (
logAxis &&
yAxisMin === undefined &&
minPositiveValue !== undefined
) {
yAxisMin = calculateLowerLogTick(minPositiveValue);
}
const tooltipFormatter =
@@ -452,12 +460,14 @@ export default function transformProps(
xAxisType === AxisType.time && timeGrainSqla
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
: 0,
...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax),
};
let yAxis: any = {
...defaultYAxis,
type: logAxis ? AxisType.log : AxisType.value,
min,
max,
min: yAxisMin,
max: yAxisMax,
minorTick: { show: true },
minorSplitLine: { show: minorSplitLine },
axisLabel: {

View File

@@ -75,10 +75,12 @@ export type EchartsTimeseriesFormData = QueryFormData & {
stack: StackType;
timeCompare?: string[];
tooltipTimeFormat?: string;
truncateXAxis: boolean;
truncateYAxis: boolean;
yAxisFormat?: string;
xAxisTimeFormat?: string;
timeGrainSqla?: TimeGranularity;
xAxisBounds: [number | undefined | null, number | undefined | null];
yAxisBounds: [number | undefined | null, number | undefined | null];
zoomable: boolean;
richTooltip: boolean;

View File

@@ -19,15 +19,14 @@
import {
buildQueryContext,
ensureIsArray,
getXAxisColumn,
isXAxisSet,
QueryFormData,
} from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
const { x_axis, granularity_sqla, groupby } = formData;
const columns = [
...(isXAxisSet(formData) ? ensureIsArray(getXAxisColumn(formData)) : []),
...ensureIsArray(formData.groupby),
...ensureIsArray(x_axis || granularity_sqla),
...ensureIsArray(groupby),
];
return buildQueryContext(formData, baseQueryObject => [
{

View File

@@ -17,25 +17,27 @@
* under the License.
*/
import React from 'react';
import { t } from '@superset-ui/core';
import { hasGenericChartAxes, t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlSubSectionHeader,
D3_TIME_FORMAT_DOCS,
DEFAULT_TIME_FORMAT,
formatSelectOptions,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { showValueControl } from '../controls';
const config: ControlPanelConfig = {
controlPanelSections: [
sections.genericTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['x_axis'],
['time_grain_sqla'],
[hasGenericChartAxes ? 'x_axis' : null],
[hasGenericChartAxes ? 'time_grain_sqla' : null],
['groupby'],
['metric'],
['adhoc_filters'],

View File

@@ -61,7 +61,7 @@ export default class EchartsWaterfallChartPlugin extends ChartPlugin<
{ url: example3 },
],
name: t('Waterfall Chart'),
tags: [t('Categorical'), t('Comparison'), t('ECharts')],
tags: [t('Categorical'), t('Comparison'), t('ECharts'), t('Popular')],
thumbnail,
}),
transformProps,

View File

@@ -185,6 +185,7 @@ export default function transformProps(
const { setDataMask = () => {}, onContextMenu, onLegendStateChanged } = hooks;
const {
currencyFormat,
granularitySqla = '',
groupby,
increaseColor,
decreaseColor,
@@ -213,7 +214,10 @@ export default function transformProps(
const breakdownName = isAdhocColumn(breakdownColumn)
? breakdownColumn.label!
: breakdownColumn;
const xAxisName = isAdhocColumn(xAxis) ? xAxis.label! : xAxis;
const xAxisColumn = xAxis || granularitySqla;
const xAxisName = isAdhocColumn(xAxisColumn)
? xAxisColumn.label!
: xAxisColumn;
const metricLabel = getMetricLabel(metric);
const transformedData = transformer({

View File

@@ -29,6 +29,7 @@ import {
} from '@superset-ui/chart-controls';
import { DEFAULT_LEGEND_FORM_DATA, StackControlOptions } from './constants';
import { DEFAULT_FORM_DATA } from './Timeseries/constants';
import { defaultXAxis } from './defaults';
const { legendMargin, legendOrientation, legendType, showLegend } =
DEFAULT_LEGEND_FORM_DATA;
@@ -243,8 +244,57 @@ const sortSeriesAscending: ControlSetItem = {
},
};
export const xAxisLabelRotation = {
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
[90, '90°'],
],
default: defaultXAxis.xAxisLabelRotation,
renderTrigger: true,
description: t('Input field supports custom rotation. e.g. 30 for 30°'),
},
};
export const seriesOrderSection: ControlSetRow[] = [
[<ControlSubSectionHeader>{t('Series Order')}</ControlSubSectionHeader>],
[sortSeriesType],
[sortSeriesAscending],
];
export const truncateXAxis: ControlSetItem = {
name: 'truncateXAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate X Axis'),
default: DEFAULT_FORM_DATA.truncateXAxis,
renderTrigger: true,
description: t(
'Truncate X Axis. Can be overridden by specifying a min or max bound. Only applicable for numercal X axis.',
),
},
};
export const xAxisBounds: ControlSetItem = {
name: 'xAxisBounds',
config: {
type: 'BoundsControl',
label: t('X Axis Bounds'),
renderTrigger: true,
default: DEFAULT_FORM_DATA.xAxisBounds,
description: t(
'Bounds for numerical X axis. Not applicable for temporal or categorical axes. ' +
'When left empty, the bounds are dynamically defined based on the min/max of the data. ' +
"Note that this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateXAxis?.value),
},
};

View File

@@ -24,6 +24,11 @@ export const defaultGrid = {
export const defaultYAxis = {
scale: true,
yAxisLabelRotation: 0,
};
export const defaultXAxis = {
xAxisLabelRotation: 0,
};
export const defaultLegendPadding = {

View File

@@ -20,7 +20,7 @@
import { validateNumber } from '@superset-ui/core';
// eslint-disable-next-line import/prefer-default-export
export function parseYAxisBound(
export function parseAxisBound(
bound?: string | number | null,
): number | undefined {
if (bound === undefined || bound === null || Number.isNaN(Number(bound))) {

View File

@@ -543,3 +543,17 @@ export function calculateLowerLogTick(minPositiveValue: number) {
const logBase10 = Math.floor(Math.log10(minPositiveValue));
return Math.pow(10, logBase10);
}
export function getMinAndMaxFromBounds(
axisType: AxisType,
truncateAxis: boolean,
min?: number,
max?: number,
): { min: number | 'dataMin'; max: number | 'dataMax' } | {} {
return truncateAxis && axisType === AxisType.value
? {
min: min === undefined ? 'dataMin' : min,
max: max === undefined ? 'dataMax' : max,
}
: {};
}

View File

@@ -48,6 +48,7 @@ describe('Bubble transformProps', () => {
expressionType: 'simple',
label: 'SUM(sales)',
},
xAxisBounds: [null, null],
yAxisBounds: [null, null],
};
const chartProps = new ChartProps({

View File

@@ -16,22 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
import { parseYAxisBound } from '../../src/utils/controls';
import { parseAxisBound } from '../../src/utils/controls';
describe('parseYAxisBound', () => {
it('should return undefined for invalid values', () => {
expect(parseYAxisBound(null)).toBeUndefined();
expect(parseYAxisBound(undefined)).toBeUndefined();
expect(parseYAxisBound(NaN)).toBeUndefined();
expect(parseYAxisBound('abc')).toBeUndefined();
expect(parseAxisBound(null)).toBeUndefined();
expect(parseAxisBound(undefined)).toBeUndefined();
expect(parseAxisBound(NaN)).toBeUndefined();
expect(parseAxisBound('abc')).toBeUndefined();
});
it('should return numeric value for valid values', () => {
expect(parseYAxisBound(0)).toEqual(0);
expect(parseYAxisBound('0')).toEqual(0);
expect(parseYAxisBound(1)).toEqual(1);
expect(parseYAxisBound('1')).toEqual(1);
expect(parseYAxisBound(10.1)).toEqual(10.1);
expect(parseYAxisBound('10.1')).toEqual(10.1);
expect(parseAxisBound(0)).toEqual(0);
expect(parseAxisBound('0')).toEqual(0);
expect(parseAxisBound(1)).toEqual(1);
expect(parseAxisBound('1')).toEqual(1);
expect(parseAxisBound(10.1)).toEqual(10.1);
expect(parseAxisBound('10.1')).toEqual(10.1);
});
});

View File

@@ -36,6 +36,7 @@ import {
getChartPadding,
getLegendProps,
getOverMaxHiddenFormatter,
getMinAndMaxFromBounds,
sanitizeHtml,
sortAndFilterSeries,
sortRows,
@@ -879,3 +880,30 @@ test('getAxisType', () => {
expect(getAxisType(GenericDataType.BOOLEAN)).toEqual(AxisType.category);
expect(getAxisType(GenericDataType.STRING)).toEqual(AxisType.category);
});
test('getMinAndMaxFromBounds returns empty object when not truncating', () => {
expect(getMinAndMaxFromBounds(AxisType.value, false, 10, 100)).toEqual({});
});
test('getMinAndMaxFromBounds returns automatic bounds when truncating', () => {
expect(
getMinAndMaxFromBounds(AxisType.value, true, undefined, undefined),
).toEqual({
min: 'dataMin',
max: 'dataMax',
});
});
test('getMinAndMaxFromBounds returns automatic upper bound when truncating', () => {
expect(getMinAndMaxFromBounds(AxisType.value, true, 10, undefined)).toEqual({
min: 10,
max: 'dataMax',
});
});
test('getMinAndMaxFromBounds returns automatic lower bound when truncating', () => {
expect(getMinAndMaxFromBounds(AxisType.value, true, undefined, 100)).toEqual({
min: 'dataMin',
max: 100,
});
});

View File

@@ -45,7 +45,7 @@ export interface QueryAutoRefreshProps {
// returns true if the Query.state matches one of the specifc values indicating the query is still processing on server
export const isQueryRunning = (q: Query): boolean =>
runningQueryStateList.includes(q?.state);
runningQueryStateList.includes(q?.state) && !q?.resultsKey;
// returns true if at least one query is running and within the max age to poll timeframe
export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {

View File

@@ -19,9 +19,10 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import QueryHistory from 'src/SqlLab/components/QueryHistory';
import { initialState } from 'src/SqlLab/fixtures';
const mockedProps = {
queries: [],
queryEditorId: 123,
displayLimit: 1000,
latestQueryId: 'yhMUZCGb',
};
@@ -32,7 +33,7 @@ const setup = (overrides = {}) => (
describe('QueryHistory', () => {
it('Renders an empty state for query history', () => {
render(setup());
render(setup(), { useRedux: true, initialState });
const emptyStateText = screen.getByText(
/run a query to display query history/i,

View File

@@ -16,13 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { EmptyStateMedium } from 'src/components/EmptyState';
import { t, styled, QueryResponse } from '@superset-ui/core';
import { t, styled } from '@superset-ui/core';
import QueryTable from 'src/SqlLab/components/QueryTable';
import { SqlLabRootState } from 'src/SqlLab/types';
interface QueryHistoryProps {
queries: QueryResponse[];
queryEditorId: string | number;
displayLimit: number;
latestQueryId: string | undefined;
}
@@ -39,11 +41,23 @@ const StyledEmptyStateWrapper = styled.div`
`;
const QueryHistory = ({
queries,
queryEditorId,
displayLimit,
latestQueryId,
}: QueryHistoryProps) =>
queries.length > 0 ? (
}: QueryHistoryProps) => {
const queries = useSelector(
({ sqlLab: { queries } }: SqlLabRootState) => queries,
shallowEqual,
);
const editorQueries = useMemo(
() =>
Object.values(queries).filter(
({ sqlEditorId }) => String(sqlEditorId) === String(queryEditorId),
),
[queries, queryEditorId],
);
return editorQueries.length > 0 ? (
<QueryTable
columns={[
'state',
@@ -55,7 +69,7 @@ const QueryHistory = ({
'results',
'actions',
]}
queries={queries}
queries={editorQueries}
displayLimit={displayLimit}
latestQueryId={latestQueryId}
/>
@@ -67,5 +81,6 @@ const QueryHistory = ({
/>
</StyledEmptyStateWrapper>
);
};
export default QueryHistory;

View File

@@ -251,8 +251,7 @@ const QueryTable = ({
modalBody={
<ResultSet
showSql
user={user}
query={query}
queryId={query.id}
height={400}
displayLimit={displayLimit}
defaultQueryLimit={1000}

View File

@@ -37,65 +37,91 @@ import {
const mockedProps = {
cache: true,
query: queries[0],
queryId: queries[0].id,
height: 140,
database: { allows_virtual_table_explore: true },
user,
displayLimit: 1000,
defaultQueryLimit: 1000,
};
const stoppedQueryProps = { ...mockedProps, query: stoppedQuery };
const runningQueryProps = { ...mockedProps, query: runningQuery };
const fetchingQueryProps = {
...mockedProps,
query: {
dbId: 1,
cached: false,
ctas: false,
id: 'ryhHUZCGb',
progress: 100,
state: 'fetching',
startDttm: Date.now() - 500,
},
};
const cachedQueryProps = { ...mockedProps, query: cachedQuery };
const failedQueryWithErrorMessageProps = {
...mockedProps,
query: failedQueryWithErrorMessage,
};
const failedQueryWithErrorsProps = {
...mockedProps,
query: failedQueryWithErrors,
};
const newProps = {
query: {
cached: false,
resultsKey: 'new key',
results: {
data: [{ a: 1 }],
const stoppedQueryState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[stoppedQuery.id]: stoppedQuery,
},
},
};
const runningQueryState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[runningQuery.id]: runningQuery,
},
},
};
const fetchingQueryState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[mockedProps.queryId]: {
dbId: 1,
cached: false,
ctas: false,
id: 'ryhHUZCGb',
progress: 100,
state: 'fetching',
startDttm: Date.now() - 500,
},
},
},
};
const cachedQueryState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[cachedQuery.id]: cachedQuery,
},
},
};
const failedQueryWithErrorMessageState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[failedQueryWithErrorMessage.id]: failedQueryWithErrorMessage,
},
},
};
const failedQueryWithErrorsState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[failedQueryWithErrors.id]: failedQueryWithErrors,
},
},
};
const newProps = {
displayLimit: 1001,
};
const asyncQueryProps = {
...mockedProps,
database: { allow_run_async: true },
};
const asyncRefetchDataPreviewProps = {
...asyncQueryProps,
query: {
state: 'success',
results: undefined,
isDataPreview: true,
},
};
const asyncRefetchResultsTableProps = {
...asyncQueryProps,
query: {
state: 'success',
results: undefined,
resultsKey: 'async results key',
},
};
const reRunQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.get('glob:*/api/v1/dataset/?*', { result: [] });
fetchMock.post(reRunQueryEndpoint, { result: [] });
fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] });
beforeEach(() => {
fetchMock.resetHistory();
});
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -107,25 +133,47 @@ const setup = (props?: any, store?: Store) =>
describe('ResultSet', () => {
test('renders a Table', async () => {
const { getByTestId } = setup(mockedProps, mockStore(initialState));
const { getByTestId } = setup(
mockedProps,
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[queries[0].id]: queries[0],
},
},
}),
);
const table = getByTestId('table-container');
expect(table).toBeInTheDocument();
});
test('should render success query', async () => {
const query = queries[0];
const { queryAllByText, getByTestId } = setup(
mockedProps,
mockStore(initialState),
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
}),
);
const table = getByTestId('table-container');
expect(table).toBeInTheDocument();
const firstColumn = queryAllByText(
mockedProps.query.results?.columns[0].column_name ?? '',
query.results?.columns[0].column_name ?? '',
)[0];
const secondColumn = queryAllByText(
mockedProps.query.results?.columns[1].column_name ?? '',
query.results?.columns[1].column_name ?? '',
)[0];
expect(firstColumn).toBeInTheDocument();
expect(secondColumn).toBeInTheDocument();
@@ -135,12 +183,24 @@ describe('ResultSet', () => {
});
test('should render empty results', async () => {
const props = {
...mockedProps,
query: { ...mockedProps.query, results: { data: [] } },
const query = {
...queries[0],
results: { data: [] },
};
await waitFor(() => {
setup(props, mockStore(initialState));
setup(
mockedProps,
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
}),
);
});
const alert = screen.getByRole('alert');
@@ -149,42 +209,70 @@ describe('ResultSet', () => {
});
test('should call reRunQuery if timed out', async () => {
const store = mockStore(initialState);
const propsWithError = {
...mockedProps,
query: { ...queries[0], errorMessage: 'Your session timed out' },
const query = {
...queries[0],
errorMessage: 'Your session timed out',
};
const store = mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
});
setup(propsWithError, store);
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
setup(mockedProps, store);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.errorMessage).toEqual(
'Your session timed out',
);
expect(store.getActions()[0].type).toEqual('START_QUERY');
await waitFor(() =>
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
);
});
test('should not call reRunQuery if no error', async () => {
const store = mockStore(initialState);
const query = queries[0];
const store = mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
});
setup(mockedProps, store);
expect(store.getActions()).toEqual([]);
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
});
test('should render cached query', async () => {
const store = mockStore(initialState);
const { rerender } = setup(cachedQueryProps, store);
const store = mockStore(cachedQueryState);
const { rerender } = setup(
{ ...mockedProps, queryId: cachedQuery.id },
store,
);
// @ts-ignore
rerender(<ResultSet {...newProps} />);
expect(store.getActions()).toHaveLength(2);
expect(store.getActions()[0].query.results).toEqual(
cachedQueryProps.query.results,
);
rerender(<ResultSet {...mockedProps} {...newProps} />);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.results).toEqual(cachedQuery.results);
expect(store.getActions()[0].type).toEqual('CLEAR_QUERY_RESULTS');
});
test('should render stopped query', async () => {
await waitFor(() => {
setup(stoppedQueryProps, mockStore(initialState));
setup(
{ ...mockedProps, queryId: stoppedQuery.id },
mockStore(stoppedQueryState),
);
});
const alert = screen.getByRole('alert');
@@ -192,15 +280,18 @@ describe('ResultSet', () => {
});
test('should render running/pending/fetching query', async () => {
const { getByTestId } = setup(runningQueryProps, mockStore(initialState));
const { getByTestId } = setup(
{ ...mockedProps, queryId: runningQuery.id },
mockStore(runningQueryState),
);
const progressBar = getByTestId('progress-bar');
expect(progressBar).toBeInTheDocument();
});
test('should render fetching w/ 100 progress query', async () => {
const { getByRole, getByText } = setup(
fetchingQueryProps,
mockStore(initialState),
mockedProps,
mockStore(fetchingQueryState),
);
const loading = getByRole('status');
expect(loading).toBeInTheDocument();
@@ -209,7 +300,10 @@ describe('ResultSet', () => {
test('should render a failed query with an error message', async () => {
await waitFor(() => {
setup(failedQueryWithErrorMessageProps, mockStore(initialState));
setup(
{ ...mockedProps, queryId: failedQueryWithErrorMessage.id },
mockStore(failedQueryWithErrorMessageState),
);
});
expect(screen.getByText('Database error')).toBeInTheDocument();
@@ -218,44 +312,129 @@ describe('ResultSet', () => {
test('should render a failed query with an errors object', async () => {
await waitFor(() => {
setup(failedQueryWithErrorsProps, mockStore(initialState));
setup(
{ ...mockedProps, queryId: failedQueryWithErrors.id },
mockStore(failedQueryWithErrorsState),
);
});
expect(screen.getByText('Database error')).toBeInTheDocument();
});
test('renders if there is no limit in query.results but has queryLimit', async () => {
const query = {
...queries[0],
};
await waitFor(() => {
setup(
mockedProps,
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
}),
);
});
const { getByRole } = setup(mockedProps, mockStore(initialState));
expect(getByRole('table')).toBeInTheDocument();
});
test('renders if there is a limit in query.results but not queryLimit', async () => {
const props = { ...mockedProps, query: queryWithNoQueryLimit };
const { getByRole } = setup(props, mockStore(initialState));
const props = { ...mockedProps, queryId: queryWithNoQueryLimit.id };
const { getByRole } = setup(
props,
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[queryWithNoQueryLimit.id]: queryWithNoQueryLimit,
},
},
}),
);
expect(getByRole('table')).toBeInTheDocument();
});
test('Async queries - renders "Fetch data preview" button when data preview has no results', () => {
setup(asyncRefetchDataPreviewProps, mockStore(initialState));
const asyncRefetchDataPreviewQuery = {
...queries[0],
state: 'success',
results: undefined,
isDataPreview: true,
};
setup(
{ ...asyncQueryProps, queryId: asyncRefetchDataPreviewQuery.id },
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[asyncRefetchDataPreviewQuery.id]: asyncRefetchDataPreviewQuery,
},
},
}),
);
expect(
screen.getByRole('button', {
name: /fetch data preview/i,
}),
).toBeVisible();
expect(screen.queryByRole('grid')).toBe(null);
expect(screen.queryByRole('table')).toBe(null);
});
test('Async queries - renders "Refetch results" button when a query has no results', () => {
setup(asyncRefetchResultsTableProps, mockStore(initialState));
const asyncRefetchResultsTableQuery = {
...queries[0],
state: 'success',
results: undefined,
resultsKey: 'async results key',
};
setup(
{ ...asyncQueryProps, queryId: asyncRefetchResultsTableQuery.id },
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[asyncRefetchResultsTableQuery.id]: asyncRefetchResultsTableQuery,
},
},
}),
);
expect(
screen.getByRole('button', {
name: /refetch results/i,
}),
).toBeVisible();
expect(screen.queryByRole('grid')).toBe(null);
expect(screen.queryByRole('table')).toBe(null);
});
test('Async queries - renders on the first call', () => {
setup(asyncQueryProps, mockStore(initialState));
const query = {
...queries[0],
};
setup(
{ ...asyncQueryProps, queryId: query.id },
mockStore({
...initialState,
user,
sqlLab: {
...initialState.sqlLab,
queries: {
[query.id]: query,
},
},
}),
);
expect(screen.getByRole('table')).toBeVisible();
expect(
screen.queryByRole('button', {

View File

@@ -17,14 +17,14 @@
* under the License.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import pick from 'lodash/pick';
import ButtonGroup from 'src/components/ButtonGroup';
import Alert from 'src/components/Alert';
import Button from 'src/components/Button';
import shortid from 'shortid';
import {
QueryResponse,
QueryState,
styled,
t,
@@ -41,8 +41,7 @@ import {
ISimpleColumn,
SaveDatasetModal,
} from 'src/SqlLab/components/SaveDatasetModal';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { EXPLORE_CHART_DEFAULT } from 'src/SqlLab/types';
import { EXPLORE_CHART_DEFAULT, SqlLabRootState } from 'src/SqlLab/types';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { postFormData } from 'src/explore/exploreUtils/formData';
import ProgressBar from 'src/components/ProgressBar';
@@ -82,12 +81,11 @@ export interface ResultSetProps {
database?: Record<string, any>;
displayLimit: number;
height: number;
query: QueryResponse;
queryId: string;
search?: boolean;
showSql?: boolean;
showSqlInline?: boolean;
visualize?: boolean;
user: UserWithPermissionsAndRoles;
defaultQueryLimit: number;
}
@@ -145,14 +143,44 @@ const ResultSet = ({
database = {},
displayLimit,
height,
query,
queryId,
search = true,
showSql = false,
showSqlInline = false,
visualize = true,
user,
defaultQueryLimit,
}: ResultSetProps) => {
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
const query = useSelector(
({ sqlLab: { queries } }: SqlLabRootState) =>
pick(queries[queryId], [
'id',
'errorMessage',
'cached',
'results',
'resultsKey',
'dbId',
'tab',
'sql',
'templateParams',
'schema',
'rows',
'queryLimit',
'limitingFactor',
'trackingUrl',
'state',
'errors',
'link',
'ctas',
'ctas_method',
'tempSchema',
'tempTable',
'isDataPreview',
'progress',
'extra',
]),
shallowEqual,
);
const ResultTable =
extensionsRegistry.get('sqleditor.extension.resultTable') ??
FilterableTable;
@@ -179,8 +207,8 @@ const ResultSet = ({
reRunQueryIfSessionTimeoutErrorOnMount();
}, [reRunQueryIfSessionTimeoutErrorOnMount]);
const fetchResults = (query: QueryResponse) => {
dispatch(fetchQueryResults(query, displayLimit));
const fetchResults = (q: typeof query) => {
dispatch(fetchQueryResults(q, displayLimit));
};
const prevQuery = usePrevious(query);
@@ -479,7 +507,7 @@ const ResultSet = ({
<ResultlessStyles>
<ErrorMessageWithStackTrace
title={t('Database error')}
error={query?.errors?.[0]}
error={query?.extra?.errors?.[0] || query?.errors?.[0]}
subtitle={<MonospaceDiv>{query.errorMessage}</MonospaceDiv>}
copyText={query.errorMessage || undefined}
link={query.link}
@@ -662,4 +690,4 @@ const ResultSet = ({
);
};
export default ResultSet;
export default React.memo(ResultSet);

View File

@@ -0,0 +1,135 @@
/**
* 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 } from 'spec/helpers/testing-library';
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { denormalizeTimestamp } from '@superset-ui/core';
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from 'src/SqlLab/constants';
import Results from './Results';
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
latestQueryId: 'LCly_kkIN',
height: 1,
displayLimit: 1,
defaultQueryLimit: 100,
};
const mockedEmptyProps = {
queryEditorId: 'random_id',
latestQueryId: 'empty_query_id',
height: 100,
displayLimit: 100,
defaultQueryLimit: 100,
};
const mockedExpiredProps = {
...mockedEmptyProps,
latestQueryId: 'expired_query_id',
};
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
const expireDateTime = Date.now() - LOCALSTORAGE_MAX_QUERY_AGE_MS - 1;
const mockState = {
...initialState,
sqlLab: {
...initialState,
offline: false,
tables: [
{
...table,
dataPreviewQueryId: '2g2_iRFMl',
queryEditorId: defaultQueryEditor.id,
},
],
databases: {},
queries: {
LCly_kkIN: {
cached: false,
changed_on: denormalizeTimestamp(new Date().toISOString()),
db: 'main',
dbId: 1,
id: 'LCly_kkIN',
startDttm: Date.now(),
sqlEditorId: defaultQueryEditor.id,
extra: { progress: latestQueryProgressMsg },
sql: 'select * from table1',
},
lXJa7F9_r: {
cached: false,
changed_on: denormalizeTimestamp(new Date(1559238500401).toISOString()),
db: 'main',
dbId: 1,
id: 'lXJa7F9_r',
startDttm: 1559238500401,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table2',
},
'2g2_iRFMl': {
cached: false,
changed_on: denormalizeTimestamp(new Date(1559238506925).toISOString()),
db: 'main',
dbId: 1,
id: '2g2_iRFMl',
startDttm: 1559238506925,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table3',
},
expired_query_id: {
cached: false,
changed_on: denormalizeTimestamp(
new Date(expireDateTime).toISOString(),
),
db: 'main',
dbId: 1,
id: 'expired_query_id',
startDttm: expireDateTime,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table4',
},
},
},
};
test('Renders an empty state for results', async () => {
const { getByText } = render(<Results {...mockedEmptyProps} />, {
useRedux: true,
initialState: mockState,
});
const emptyStateText = getByText(/run a query to display results/i);
expect(emptyStateText).toBeVisible();
});
test('Renders an empty state for expired results', async () => {
const { getByText } = render(<Results {...mockedExpiredProps} />, {
useRedux: true,
initialState: mockState,
});
const emptyStateText = getByText(/run a query to display results/i);
expect(emptyStateText).toBeVisible();
});
test('should pass latest query down to ResultSet component', async () => {
const { getByText } = render(<Results {...mockedProps} />, {
useRedux: true,
initialState: mockState,
});
expect(getByText(latestQueryProgressMsg)).toBeVisible();
});

View File

@@ -0,0 +1,106 @@
/**
* 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 { shallowEqual, useSelector } from 'react-redux';
import Alert from 'src/components/Alert';
import { EmptyStateMedium } from 'src/components/EmptyState';
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
import { SqlLabRootState } from 'src/SqlLab/types';
import ResultSet from '../ResultSet';
import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../constants';
const EXTRA_HEIGHT_RESULTS = 8; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
type Props = {
latestQueryId: string;
height: number;
displayLimit: number;
defaultQueryLimit: number;
};
const StyledEmptyStateWrapper = styled.div`
height: 100%;
.ant-empty-image img {
margin-right: 28px;
}
p {
margin-right: 28px;
}
`;
const Results: React.FC<Props> = ({
latestQueryId,
height,
displayLimit,
defaultQueryLimit,
}) => {
const databases = useSelector(
({ sqlLab: { databases } }: SqlLabRootState) => databases,
shallowEqual,
);
const latestQuery = useSelector(
({ sqlLab: { queries } }: SqlLabRootState) => queries[latestQueryId || ''],
shallowEqual,
);
if (
!latestQuery ||
Date.now() - latestQuery.startDttm > LOCALSTORAGE_MAX_QUERY_AGE_MS
) {
return (
<StyledEmptyStateWrapper>
<EmptyStateMedium
title={t('Run a query to display results')}
image="document.svg"
/>
</StyledEmptyStateWrapper>
);
}
if (
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
latestQuery.state === 'success' &&
!latestQuery.resultsKey &&
!latestQuery.results
) {
return (
<Alert
type="warning"
message={t('No stored results found, you need to re-run your query')}
/>
);
}
return (
<ResultSet
search
queryId={latestQuery.id}
height={height + EXTRA_HEIGHT_RESULTS}
database={databases[latestQuery.dbId]}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
showSql
showSqlInline
/>
);
};
export default Results;

View File

@@ -17,15 +17,12 @@
* under the License.
*/
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import SouthPane, { SouthPaneProps } from 'src/SqlLab/components/SouthPane';
import { render } from 'spec/helpers/testing-library';
import SouthPane from 'src/SqlLab/components/SouthPane';
import '@testing-library/jest-dom/extend-expect';
import { STATUS_OPTIONS } from 'src/SqlLab/constants';
import { initialState, table, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { denormalizeTimestamp } from '@superset-ui/core';
import { Store } from 'redux';
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
@@ -37,29 +34,32 @@ const mockedProps = {
const mockedEmptyProps = {
queryEditorId: 'random_id',
latestQueryId: '',
latestQueryId: 'empty_query_id',
height: 100,
displayLimit: 100,
defaultQueryLimit: 100,
};
jest.mock('src/SqlLab/components/SqlEditorLeftBar', () => jest.fn());
const latestQueryProgressMsg = 'LATEST QUERY MESSAGE - LCly_kkIN';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const store = mockStore({
const mockState = {
...initialState,
sqlLab: {
...initialState,
...initialState.sqlLab,
offline: false,
tables: [
{
...table,
name: 'table3',
dataPreviewQueryId: '2g2_iRFMl',
queryEditorId: defaultQueryEditor.id,
},
{
...table,
name: 'table4',
dataPreviewQueryId: 'erWdqEWPm',
queryEditorId: defaultQueryEditor.id,
},
],
databases: {},
queries: {
@@ -72,6 +72,7 @@ const store = mockStore({
startDttm: Date.now(),
sqlEditorId: defaultQueryEditor.id,
extra: { progress: latestQueryProgressMsg },
sql: 'select * from table1',
},
lXJa7F9_r: {
cached: false,
@@ -81,6 +82,7 @@ const store = mockStore({
id: 'lXJa7F9_r',
startDttm: 1559238500401,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table2',
},
'2g2_iRFMl': {
cached: false,
@@ -90,6 +92,7 @@ const store = mockStore({
id: '2g2_iRFMl',
startDttm: 1559238506925,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table3',
},
erWdqEWPm: {
cached: false,
@@ -99,44 +102,38 @@ const store = mockStore({
id: 'erWdqEWPm',
startDttm: 1559238516395,
sqlEditorId: defaultQueryEditor.id,
sql: 'select * from table4',
},
},
},
});
const setup = (props: SouthPaneProps, store: Store) =>
render(<SouthPane {...props} />, {
};
test('should render offline when the state is offline', async () => {
const { getByText } = render(<SouthPane {...mockedEmptyProps} />, {
useRedux: true,
...(store && { store }),
initialState: {
...initialState,
sqlLab: {
...initialState.sqlLab,
offline: true,
},
},
});
describe('SouthPane', () => {
const renderAndWait = (props: SouthPaneProps, store: Store) =>
waitFor(async () => setup(props, store));
expect(getByText(STATUS_OPTIONS.offline)).toBeVisible();
});
it('Renders an empty state for results', async () => {
await renderAndWait(mockedEmptyProps, store);
const emptyStateText = screen.getByText(/run a query to display results/i);
expect(emptyStateText).toBeVisible();
test('should render tabs for table preview queries', () => {
const { getAllByRole } = render(<SouthPane {...mockedProps} />, {
useRedux: true,
initialState: mockState,
});
it('should render offline when the state is offline', async () => {
await renderAndWait(
mockedEmptyProps,
mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
offline: true,
},
}),
);
expect(screen.getByText(STATUS_OPTIONS.offline)).toBeVisible();
});
it('should pass latest query down to ResultSet component', async () => {
await renderAndWait(mockedProps, store);
expect(screen.getByText(latestQueryProgressMsg)).toBeVisible();
const tabs = getAllByRole('tab');
expect(tabs).toHaveLength(mockState.sqlLab.tables.length + 2);
expect(tabs[0]).toHaveTextContent('Results');
expect(tabs[1]).toHaveTextContent('Query history');
mockState.sqlLab.tables.forEach(({ name }, index) => {
expect(tabs[index + 2]).toHaveTextContent(`Preview: \`${name}\``);
});
});

View File

@@ -19,10 +19,8 @@
import React, { createRef, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import shortid from 'shortid';
import Alert from 'src/components/Alert';
import Tabs from 'src/components/Tabs';
import { EmptyStateMedium } from 'src/components/EmptyState';
import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core';
import { styled, t } from '@superset-ui/core';
import { setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
@@ -33,11 +31,11 @@ import ResultSet from '../ResultSet';
import {
STATUS_OPTIONS,
STATE_TYPE_MAP,
LOCALSTORAGE_MAX_QUERY_AGE_MS,
STATUS_OPTIONS_LOCALIZED,
} from '../../constants';
import Results from './Results';
const TAB_HEIGHT = 140;
const TAB_HEIGHT = 130;
/*
editorQueries are queries executed by users passed from SqlEditor component
@@ -85,18 +83,6 @@ const StyledPane = styled.div<StyledPaneProps>`
}
`;
const EXTRA_HEIGHT_RESULTS = 24; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab.
const StyledEmptyStateWrapper = styled.div`
height: 100%;
.ant-empty-image img {
margin-right: 28px;
}
p {
margin-right: 28px;
}
`;
const SouthPane = ({
queryEditorId,
latestQueryId,
@@ -105,128 +91,43 @@ const SouthPane = ({
defaultQueryLimit,
}: SouthPaneProps) => {
const dispatch = useDispatch();
const user = useSelector(({ user }: SqlLabRootState) => user, shallowEqual);
const { databases, offline, queries, tables } = useSelector(
({ sqlLab: { databases, offline, queries, tables } }: SqlLabRootState) => ({
databases,
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
offline,
queries,
tables,
}),
shallowEqual,
);
const editorQueries = useMemo(
() =>
Object.values(queries).filter(
({ sqlEditorId }) => sqlEditorId === queryEditorId,
),
[queries, queryEditorId],
const queries = useSelector(
({ sqlLab: { queries } }: SqlLabRootState) => Object.keys(queries),
shallowEqual,
);
const dataPreviewQueries = useMemo(
() =>
tables
.filter(
({ dataPreviewQueryId, queryEditorId: qeId }) =>
dataPreviewQueryId &&
queryEditorId === qeId &&
queries[dataPreviewQueryId],
)
.map(({ name, dataPreviewQueryId }) => ({
...queries[dataPreviewQueryId || ''],
tableName: name,
})),
[queries, queryEditorId, tables],
);
const latestQuery = useMemo(
() => editorQueries.find(({ id }) => id === latestQueryId),
[editorQueries, latestQueryId],
);
const activeSouthPaneTab =
useSelector<SqlLabRootState, string>(
state => state.sqlLab.activeSouthPaneTab as string,
) ?? 'Results';
const querySet = useMemo(() => new Set(queries), [queries]);
const dataPreviewQueries = useMemo(
() =>
tables.filter(
({ dataPreviewQueryId, queryEditorId: qeId }) =>
dataPreviewQueryId &&
queryEditorId === qeId &&
querySet.has(dataPreviewQueryId),
),
[queryEditorId, tables, querySet],
);
const innerTabContentHeight = height - TAB_HEIGHT;
const southPaneRef = createRef<HTMLDivElement>();
const switchTab = (id: string) => {
dispatch(setActiveSouthPaneTab(id));
};
const renderOfflineStatus = () => (
return offline ? (
<Label className="m-r-3" type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
{STATUS_OPTIONS_LOCALIZED.offline}
</Label>
);
const renderResults = () => {
let results;
if (latestQuery) {
if (latestQuery?.extra?.errors) {
latestQuery.errors = latestQuery.extra.errors;
}
if (
isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
latestQuery.state === 'success' &&
!latestQuery.resultsKey &&
!latestQuery.results
) {
results = (
<Alert
type="warning"
message={t(
'No stored results found, you need to re-run your query',
)}
/>
);
return results;
}
if (Date.now() - latestQuery.startDttm <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
results = (
<ResultSet
search
query={latestQuery}
user={user}
height={innerTabContentHeight + EXTRA_HEIGHT_RESULTS}
database={databases[latestQuery.dbId]}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
showSql
showSqlInline
/>
);
}
} else {
results = (
<StyledEmptyStateWrapper>
<EmptyStateMedium
title={t('Run a query to display results')}
image="document.svg"
/>
</StyledEmptyStateWrapper>
);
}
return results;
};
const renderDataPreviewTabs = () =>
dataPreviewQueries.map(query => (
<Tabs.TabPane
tab={t('Preview: `%s`', decodeURIComponent(query.tableName))}
key={query.id}
>
<ResultSet
query={query}
visualize={false}
csv={false}
cache
user={user}
height={innerTabContentHeight}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
/>
</Tabs.TabPane>
));
return offline ? (
renderOfflineStatus()
) : (
<StyledPane
data-test="south-pane"
@@ -243,16 +144,41 @@ const SouthPane = ({
animated={false}
>
<Tabs.TabPane tab={t('Results')} key="Results">
{renderResults()}
{latestQueryId && (
<Results
height={innerTabContentHeight}
latestQueryId={latestQueryId}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
/>
)}
</Tabs.TabPane>
<Tabs.TabPane tab={t('Query history')} key="History">
<QueryHistory
queries={editorQueries}
queryEditorId={queryEditorId}
displayLimit={displayLimit}
latestQueryId={latestQueryId}
/>
</Tabs.TabPane>
{renderDataPreviewTabs()}
{dataPreviewQueries.map(
({ name, dataPreviewQueryId }) =>
dataPreviewQueryId && (
<Tabs.TabPane
tab={t('Preview: `%s`', decodeURIComponent(name))}
key={dataPreviewQueryId}
>
<ResultSet
queryId={dataPreviewQueryId}
visualize={false}
csv={false}
cache
height={innerTabContentHeight}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
/>
</Tabs.TabPane>
),
)}
</Tabs>
</StyledPane>
);

View File

@@ -145,8 +145,8 @@ describe('SqlEditor', () => {
(SqlEditorLeftBar as jest.Mock).mockImplementation(() => (
<div data-test="mock-sql-editor-left-bar" />
));
(ResultSet as jest.Mock).mockClear();
(ResultSet as jest.Mock).mockImplementation(() => (
(ResultSet as unknown as jest.Mock).mockClear();
(ResultSet as unknown as jest.Mock).mockImplementation(() => (
<div data-test="mock-result-set" />
));
});
@@ -182,7 +182,8 @@ describe('SqlEditor', () => {
const editor = await findByTestId('react-ace');
const sql = 'select *';
const renderCount = (SqlEditorLeftBar as jest.Mock).mock.calls.length;
const renderCountForSouthPane = (ResultSet as jest.Mock).mock.calls.length;
const renderCountForSouthPane = (ResultSet as unknown as jest.Mock).mock
.calls.length;
expect(SqlEditorLeftBar).toHaveBeenCalledTimes(renderCount);
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
fireEvent.change(editor, { target: { value: sql } });

View File

@@ -345,7 +345,7 @@ export default function sqlLabReducer(state = {}, action) {
return state;
}
const alts = {
endDttm: now(),
endDttm: action?.results?.query?.endDttm || now(),
progress: 100,
results: action.results,
rows: action?.results?.query?.rows || 0,
@@ -674,7 +674,14 @@ export default function sqlLabReducer(state = {}, action) {
if (!change) {
newQueries = state.queries;
}
return { ...state, queries: newQueries, queriesLastUpdate };
return {
...state,
queries: newQueries,
queriesLastUpdate:
queriesLastUpdate > state.queriesLastUpdate
? queriesLastUpdate
: Date.now(),
};
},
[actions.CLEAR_INACTIVE_QUERIES]() {
const { queries } = state;
@@ -701,7 +708,11 @@ export default function sqlLabReducer(state = {}, action) {
},
]),
);
return { ...state, queries: cleanedQueries };
return {
...state,
queries: cleanedQueries,
queriesLastUpdate: Date.now(),
};
},
[actions.SET_USER_OFFLINE]() {
return { ...state, offline: action.offline };

View File

@@ -20,6 +20,7 @@ import { QueryState } from '@superset-ui/core';
import sqlLabReducer from 'src/SqlLab/reducers/sqlLab';
import * as actions from 'src/SqlLab/actions/sqlLab';
import { table, initialState as mockState } from '../fixtures';
import { QUERY_UPDATE_FREQ } from '../components/QueryAutoRefresh';
const initialState = mockState.sqlLab;
@@ -404,6 +405,7 @@ describe('sqlLabReducer', () => {
};
});
it('updates queries that have already been completed', () => {
const current = Date.now();
newState = sqlLabReducer(
{
...newState,
@@ -418,9 +420,10 @@ describe('sqlLabReducer', () => {
},
},
},
actions.clearInactiveQueries(Date.now()),
actions.clearInactiveQueries(QUERY_UPDATE_FREQ),
);
expect(newState.queries.abcd.state).toBe(QueryState.SUCCESS);
expect(newState.queriesLastUpdate).toBeGreaterThanOrEqual(current);
});
});
});

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { ModifiedInfo } from '.';
const TEST_DATE = '2023-11-20';
const USER = {
id: 1,
first_name: 'Foo',
last_name: 'Bar',
};
test('should render a tooltip when user is provided', async () => {
render(<ModifiedInfo user={USER} date={TEST_DATE} />);
const dateElement = screen.getByTestId('audit-info-date');
expect(dateElement).toBeInTheDocument();
expect(screen.getByText(TEST_DATE)).toBeInTheDocument();
expect(screen.queryByText('Modified by: Foo Bar')).not.toBeInTheDocument();
userEvent.hover(dateElement);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(screen.getByText('Modified by: Foo Bar')).toBeInTheDocument();
});
test('should render only the date if username is not provided', async () => {
render(<ModifiedInfo date={TEST_DATE} />);
const dateElement = screen.getByTestId('audit-info-date');
expect(dateElement).toBeInTheDocument();
expect(screen.getByText(TEST_DATE)).toBeInTheDocument();
userEvent.hover(dateElement);
await waitFor(
() => {
const tooltip = screen.queryByRole('tooltip');
expect(tooltip).not.toBeInTheDocument();
},
{ timeout: 1000 },
);
});

View File

@@ -0,0 +1,30 @@
import React from 'react';
import Owner from 'src/types/Owner';
import { Tooltip } from 'src/components/Tooltip';
import getOwnerName from 'src/utils/getOwnerName';
import { t } from '@superset-ui/core';
export type ModifiedInfoProps = {
user?: Owner;
date: string;
};
export const ModifiedInfo = ({ user, date }: ModifiedInfoProps) => {
const dateSpan = (
<span className="no-wrap" data-test="audit-info-date">
{date}
</span>
);
if (user) {
const userName = getOwnerName(user);
const title = t('Modified by: %s', userName);
return (
<Tooltip title={title} placement="bottom">
{dateSpan}
</Tooltip>
);
}
return dateSpan;
};

View File

@@ -269,9 +269,12 @@ export function runAnnotationQuery({
return Promise.resolve();
}
const granularity = fd.time_grain_sqla || fd.granularity;
fd.time_grain_sqla = granularity;
fd.granularity = granularity;
// In the original formData the `granularity` attribute represents the time grain (eg
// `P1D`), but in the request payload it corresponds to the name of the column where
// the time grain should be applied (eg, `Date`), so we need to move things around.
fd.time_grain_sqla = fd.time_grain_sqla || fd.granularity;
fd.granularity = fd.granularity_sqla;
const overridesKeys = Object.keys(annotation.overrides);
if (overridesKeys.includes('since') || overridesKeys.includes('until')) {
annotation.overrides = {

View File

@@ -21,6 +21,7 @@ import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import * as chartlib from '@superset-ui/core';
import { SupersetClient } from '@superset-ui/core';
import { LOG_EVENT } from 'src/logger/actions';
import * as exploreUtils from 'src/explore/exploreUtils';
import * as actions from 'src/components/Chart/chartAction';
@@ -233,4 +234,70 @@ describe('chart actions', () => {
expect(json.result[0].value.toString()).toEqual(expectedBigNumber);
});
});
describe('runAnnotationQuery', () => {
const mockDispatch = jest.fn();
const mockGetState = () => ({
charts: {
chartKey: {
latestQueryFormData: {
time_grain_sqla: 'P1D',
granularity_sqla: 'Date',
},
},
},
});
beforeEach(() => {
jest.clearAllMocks();
});
it('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
const annotation = {
name: 'Holidays',
annotationType: 'EVENT',
sourceType: 'NATIVE',
color: null,
opacity: '',
style: 'solid',
width: 1,
showMarkers: false,
hideLine: false,
value: 1,
overrides: {
time_range: null,
},
show: true,
showLabel: false,
titleColumn: '',
descriptionColumns: [],
timeColumn: '',
intervalEndColumn: '',
};
const key = undefined;
const postSpy = jest.spyOn(SupersetClient, 'post');
postSpy.mockImplementation(() =>
Promise.resolve({ json: { result: [] } }),
);
const buildV1ChartDataPayloadSpy = jest.spyOn(
exploreUtils,
'buildV1ChartDataPayload',
);
const queryFunc = actions.runAnnotationQuery({ annotation, key });
await queryFunc(mockDispatch, mockGetState);
expect(buildV1ChartDataPayloadSpy).toHaveBeenCalledWith({
formData: {
granularity: 'Date',
granularity_sqla: 'Date',
time_grain_sqla: 'P1D',
},
force: false,
resultFormat: 'json',
resultType: 'full',
});
});
});
});

View File

@@ -290,7 +290,13 @@ test('Sends the correct db when changing the database', async () => {
test('Sends the correct schema when changing the schema', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />, { useRedux: true, store });
const { rerender } = render(<DatabaseSelector {...props} db={null} />, {
useRedux: true,
store,
});
await waitFor(() => expect(fetchMock.calls(databaseApiRoute).length).toBe(1));
rerender(<DatabaseSelector {...props} />);
expect(props.onSchemaChange).toBeCalledTimes(0);
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
@@ -301,4 +307,5 @@ test('Sends the correct schema when changing the schema', async () => {
await waitFor(() =>
expect(props.onSchemaChange).toHaveBeenCalledWith('information_schema'),
);
expect(props.onSchemaChange).toBeCalledTimes(1);
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { ReactNode, useState, useMemo, useEffect } from 'react';
import React, { ReactNode, useState, useMemo, useEffect, useRef } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
import rison from 'rison';
import { AsyncSelect, Select } from 'src/components';
@@ -133,6 +133,8 @@ export default function DatabaseSelector({
const [currentSchema, setCurrentSchema] = useState<SchemaOption | undefined>(
schema ? { label: schema, value: schema, title: schema } : undefined,
);
const schemaRef = useRef(schema);
schemaRef.current = schema;
const { addSuccessToast } = useToasts();
const loadDatabases = useMemo(
@@ -215,7 +217,7 @@ export default function DatabaseSelector({
function changeSchema(schema: SchemaOption | undefined) {
setCurrentSchema(schema);
if (onSchemaChange) {
if (onSchemaChange && schema?.value !== schemaRef.current) {
onSchemaChange(schema?.value);
}
}
@@ -229,7 +231,9 @@ export default function DatabaseSelector({
onSuccess: (schemas, isFetched) => {
if (schemas.length === 1) {
changeSchema(schemas[0]);
} else if (!schemas.find(schemaOption => schema === schemaOption.value)) {
} else if (
!schemas.find(schemaOption => schemaRef.current === schemaOption.value)
) {
changeSchema(undefined);
}

View File

@@ -1114,7 +1114,7 @@ class DatasourceEditor extends React.PureComponent {
<div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
<Field
fieldKey="table_name"
label={t('Dataset name')}
label={t('Name')}
control={
<TextControl
controlId="table_name"

View File

@@ -113,10 +113,7 @@ export const DynamicEditableTitle = ({
// then we can measure the width of that span to resize the input element
useLayoutEffect(() => {
if (sizerRef?.current) {
sizerRef.current.innerHTML = (currentTitle || placeholder).replace(
/\s/g,
'&nbsp;',
);
sizerRef.current.textContent = currentTitle || placeholder;
}
}, [currentTitle, placeholder, sizerRef]);

View File

@@ -611,9 +611,14 @@ export function setDirectPathToChild(path) {
return { type: SET_DIRECT_PATH, path };
}
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
export function setActiveTab(tabId, prevTabId) {
return { type: SET_ACTIVE_TAB, tabId, prevTabId };
}
export const SET_ACTIVE_TABS = 'SET_ACTIVE_TABS';
export function setActiveTabs(tabId, prevTabId) {
return { type: SET_ACTIVE_TABS, tabId, prevTabId };
export function setActiveTabs(activeTabs) {
return { type: SET_ACTIVE_TABS, activeTabs };
}
export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD';

View File

@@ -25,7 +25,7 @@ import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/Dashboar
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
import {
fetchFaveStar,
setActiveTabs,
setActiveTab,
setDirectPathToChild,
} from 'src/dashboard/actions/dashboardState';
import {
@@ -41,7 +41,7 @@ fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
jest.mock('src/dashboard/actions/dashboardState', () => ({
...jest.requireActual('src/dashboard/actions/dashboardState'),
fetchFaveStar: jest.fn(),
setActiveTabs: jest.fn(),
setActiveTab: jest.fn(),
setDirectPathToChild: jest.fn(),
}));
jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth');
@@ -90,7 +90,7 @@ describe('DashboardBuilder', () => {
favStarStub = (fetchFaveStar as jest.Mock).mockReturnValue({
type: 'mock-action',
});
activeTabsStub = (setActiveTabs as jest.Mock).mockReturnValue({
activeTabsStub = (setActiveTab as jest.Mock).mockReturnValue({
type: 'mock-action',
});
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [

View File

@@ -681,7 +681,7 @@ const PropertiesModal = ({
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<FormItem label={t('Title')} name="title">
<FormItem label={t('Name')} name="title">
<Input
data-test="dashboard-title-input"
type="text"

View File

@@ -51,7 +51,7 @@ const propTypes = {
// actions (from DashboardComponent.jsx)
logEvent: PropTypes.func.isRequired,
setActiveTabs: PropTypes.func,
setActiveTab: PropTypes.func,
// grid related
availableColumnCount: PropTypes.number,
@@ -75,7 +75,7 @@ const defaultProps = {
columnWidth: 0,
activeTabs: [],
directPathToChild: [],
setActiveTabs() {},
setActiveTab() {},
onResizeStart() {},
onResize() {},
onResizeStop() {},
@@ -125,12 +125,12 @@ export class Tabs extends React.PureComponent {
}
componentDidMount() {
this.props.setActiveTabs(this.state.activeKey);
this.props.setActiveTab(this.state.activeKey);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.activeKey !== this.state.activeKey) {
this.props.setActiveTabs(this.state.activeKey, prevState.activeKey);
this.props.setActiveTab(this.state.activeKey, prevState.activeKey);
}
}

View File

@@ -35,7 +35,7 @@ import {
} from 'src/dashboard/actions/dashboardLayout';
import {
setDirectPathToChild,
setActiveTabs,
setActiveTab,
setFullSizeChartId,
} from 'src/dashboard/actions/dashboardState';
@@ -109,7 +109,7 @@ function mapDispatchToProps(dispatch) {
handleComponentDrop,
setDirectPathToChild,
setFullSizeChartId,
setActiveTabs,
setActiveTab,
logEvent,
},
dispatch,

View File

@@ -37,6 +37,7 @@ import {
SET_DIRECT_PATH,
SET_FOCUSED_FILTER_FIELD,
UNSET_FOCUSED_FILTER_FIELD,
SET_ACTIVE_TAB,
SET_ACTIVE_TABS,
SET_FULL_SIZE_CHART_ID,
ON_FILTERS_REFRESH,
@@ -179,7 +180,7 @@ export default function dashboardStateReducer(state = {}, action) {
directPathLastUpdated: Date.now(),
};
},
[SET_ACTIVE_TABS]() {
[SET_ACTIVE_TAB]() {
const newActiveTabs = new Set(state.activeTabs);
newActiveTabs.delete(action.prevTabId);
newActiveTabs.add(action.tabId);
@@ -188,6 +189,12 @@ export default function dashboardStateReducer(state = {}, action) {
activeTabs: Array.from(newActiveTabs),
};
},
[SET_ACTIVE_TABS]() {
return {
...state,
activeTabs: action.activeTabs,
};
},
[SET_OVERRIDE_CONFIRM]() {
return {
...state,

View File

@@ -18,21 +18,33 @@
*/
import dashboardStateReducer from './dashboardState';
import { setActiveTabs } from '../actions/dashboardState';
import { setActiveTab, setActiveTabs } from '../actions/dashboardState';
describe('DashboardState reducer', () => {
it('SET_ACTIVE_TABS', () => {
it('SET_ACTIVE_TAB', () => {
expect(
dashboardStateReducer({ activeTabs: [] }, setActiveTabs('tab1')),
dashboardStateReducer({ activeTabs: [] }, setActiveTab('tab1')),
).toEqual({ activeTabs: ['tab1'] });
expect(
dashboardStateReducer({ activeTabs: ['tab1'] }, setActiveTabs('tab1')),
dashboardStateReducer({ activeTabs: ['tab1'] }, setActiveTab('tab1')),
).toEqual({ activeTabs: ['tab1'] });
expect(
dashboardStateReducer(
{ activeTabs: ['tab1'] },
setActiveTabs('tab2', 'tab1'),
setActiveTab('tab2', 'tab1'),
),
).toEqual({ activeTabs: ['tab2'] });
});
it('SET_ACTIVE_TABS', () => {
expect(
dashboardStateReducer({ activeTabs: [] }, setActiveTabs(['tab1'])),
).toEqual({ activeTabs: ['tab1'] });
expect(
dashboardStateReducer(
{ activeTabs: ['tab1', 'tab2'] },
setActiveTabs(['tab3', 'tab4']),
),
).toEqual({ activeTabs: ['tab3', 'tab4'] });
});
});

View File

@@ -0,0 +1,77 @@
/**
* 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, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import ColumnSelectPopover from 'src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('ColumnSelectPopover - onTabChange function', () => {
it('updates adhocColumn when switching to sqlExpression tab with custom label', () => {
const mockColumns = [{ column_name: 'year' }];
const mockOnClose = jest.fn();
const mockOnChange = jest.fn();
const mockGetCurrentTab = jest.fn();
const mockSetDatasetModal = jest.fn();
const mockSetLabel = jest.fn();
const store = mockStore({ explore: { datasource: { type: 'table' } } });
const { container, getByText } = render(
<Provider store={store}>
<ThemeProvider theme={supersetTheme}>
<ColumnSelectPopover
columns={mockColumns}
editedColumn={mockColumns[0]}
getCurrentTab={mockGetCurrentTab}
hasCustomLabel
isTemporal
label="Custom Label"
onChange={mockOnChange}
onClose={mockOnClose}
setDatasetModal={mockSetDatasetModal}
setLabel={mockSetLabel}
/>
</ThemeProvider>
</Provider>,
);
const sqlExpressionTab = container.querySelector(
'#adhoc-metric-edit-tabs-tab-sqlExpression',
);
expect(sqlExpressionTab).not.toBeNull();
fireEvent.click(sqlExpressionTab!);
expect(mockGetCurrentTab).toHaveBeenCalledWith('sqlExpression');
const saveButton = getByText('Save');
fireEvent.click(saveButton);
expect(mockOnChange).toHaveBeenCalledWith({
label: 'Custom Label',
sqlExpression: 'year',
expressionType: 'SQL',
});
});
});

View File

@@ -68,6 +68,7 @@ interface ColumnSelectPopoverProps {
editedColumn?: ColumnMeta | AdhocColumn;
onChange: (column: ColumnMeta | AdhocColumn) => void;
onClose: () => void;
hasCustomLabel: boolean;
setLabel: (title: string) => void;
getCurrentTab: (tab: string) => void;
label: string;
@@ -93,13 +94,14 @@ const getInitialColumnValues = (
const ColumnSelectPopover = ({
columns,
editedColumn,
getCurrentTab,
hasCustomLabel,
isTemporal,
label,
onChange,
onClose,
setDatasetModal,
setLabel,
getCurrentTab,
label,
isTemporal,
}: ColumnSelectPopoverProps) => {
const datasourceType = useSelector<ExplorePageState, string | undefined>(
state => state.explore.datasource.type,
@@ -117,6 +119,7 @@ const ColumnSelectPopover = ({
const [selectedSimpleColumn, setSelectedSimpleColumn] = useState<
ColumnMeta | undefined
>(initialSimpleColumn);
const [selectedTab, setSelectedTab] = useState<string | null>(null);
const [resizeButton, width, height] = useResizeButton(
POPOVER_INITIAL_WIDTH,
@@ -188,7 +191,34 @@ const ColumnSelectPopover = ({
useEffect(() => {
getCurrentTab(defaultActiveTabKey);
}, [defaultActiveTabKey, getCurrentTab]);
setSelectedTab(defaultActiveTabKey);
}, [defaultActiveTabKey, getCurrentTab, setSelectedTab]);
useEffect(() => {
/* if the adhoc column is not set (because it was never edited) but the
* tab is selected and the label has changed, then we need to set the
* adhoc column manually */
if (
adhocColumn === undefined &&
selectedTab === 'sqlExpression' &&
hasCustomLabel
) {
const sqlExpression =
selectedSimpleColumn?.column_name ||
selectedCalculatedColumn?.expression ||
'';
setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' });
}
}, [
adhocColumn,
defaultActiveTabKey,
hasCustomLabel,
getCurrentTab,
label,
selectedCalculatedColumn,
selectedSimpleColumn,
selectedTab,
]);
const onSave = useCallback(() => {
if (adhocColumn && adhocColumn.label !== label) {
@@ -225,6 +255,7 @@ const ColumnSelectPopover = ({
const onTabChange = useCallback(
tab => {
getCurrentTab(tab);
setSelectedTab(tab);
// @ts-ignore
sqlEditorRef.current?.editor.focus();
},

View File

@@ -103,6 +103,7 @@ const ColumnSelectPopoverTrigger = ({
setDatasetModal={setDatasetModal}
onClose={handleClosePopover}
onChange={onColumnEdit}
hasCustomLabel={hasCustomLabel}
label={popoverLabel}
setLabel={setPopoverLabel}
getCurrentTab={getCurrentTab}
@@ -114,6 +115,7 @@ const ColumnSelectPopoverTrigger = ({
columns,
editedColumn,
getCurrentTab,
hasCustomLabel,
handleClosePopover,
isTemporal,
onColumnEdit,
@@ -121,10 +123,13 @@ const ColumnSelectPopoverTrigger = ({
],
);
const onLabelChange = useCallback((e: any) => {
setPopoverLabel(e.target.value);
setHasCustomLabel(true);
}, []);
const onLabelChange = useCallback(
(e: any) => {
setPopoverLabel(e.target.value);
setHasCustomLabel(true);
},
[setPopoverLabel, setHasCustomLabel],
);
const popoverTitle = useMemo(
() => (

View File

@@ -287,7 +287,7 @@ const AnnotationModal: FunctionComponent<AnnotationModalProps> = ({
</StyledAnnotationTitle>
<AnnotationContainer>
<div className="control-label">
{t('Annotation name')}
{t('Name')}
<span className="required">*</span>
</div>
<input

View File

@@ -105,6 +105,9 @@ const CssTemplateModal: FunctionComponent<CssTemplateModalProps> = ({
const update_id = currentCssTemplate.id;
delete currentCssTemplate.id;
delete currentCssTemplate.created_by;
delete currentCssTemplate.changed_by;
delete currentCssTemplate.changed_on_delta_humanized;
updateResource(update_id, currentCssTemplate).then(response => {
if (!response) {
return;
@@ -235,7 +238,7 @@ const CssTemplateModal: FunctionComponent<CssTemplateModalProps> = ({
</StyledCssTemplateTitle>
<TemplateContainer>
<div className="control-label">
{t('CSS template name')}
{t('Name')}
<span className="required">*</span>
</div>
<input

View File

@@ -1,3 +1,5 @@
import Owner from 'src/types/Owner';
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -16,17 +18,12 @@
* specific language governing permissions and limitations
* under the License.
*/
type CreatedByUser = {
id: number;
first_name: string;
last_name: string;
};
export type TemplateObject = {
id?: number;
changed_on_delta_humanized?: string;
created_on?: string;
created_by?: CreatedByUser;
changed_by?: Owner;
created_by?: Owner;
css?: string;
template_name: string;
};

View File

@@ -24,7 +24,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
import { Row, Col, Grid } from 'src/components';
import { MainNav as DropdownMenu, MenuMode } from 'src/components/Menu';
import { Tooltip } from 'src/components/Tooltip';
import { Link, useLocation } from 'react-router-dom';
import { NavLink, useLocation } from 'react-router-dom';
import { GenericLink } from 'src/components/GenericLink/GenericLink';
import Icons from 'src/components/Icons';
import { useUiConfig } from 'src/components/UiConfigContext';
@@ -154,6 +154,29 @@ const globalStyles = (theme: SupersetTheme) => css`
margin-left: ${theme.gridUnit * 1.75}px;
}
}
.ant-menu-item-selected {
background-color: transparent;
&:not(.ant-menu-item-active) {
color: inherit;
border-bottom-color: transparent;
& > a {
color: inherit;
}
}
}
.ant-menu-horizontal > .ant-menu-item:has(> .is-active) {
color: ${theme.colors.primary.base};
border-bottom-color: ${theme.colors.primary.base};
& > a {
color: ${theme.colors.primary.base};
}
}
.ant-menu-vertical > .ant-menu-item:has(> .is-active) {
background-color: ${theme.colors.primary.light5};
& > a {
color: ${theme.colors.primary.base};
}
}
`;
const { SubMenu } = DropdownMenu;
@@ -226,9 +249,9 @@ export function Menu({
if (url && isFrontendRoute) {
return (
<DropdownMenu.Item key={label} role="presentation">
<Link role="button" to={url}>
<NavLink role="button" to={url} activeClassName="is-active">
{label}
</Link>
</NavLink>
</DropdownMenu.Item>
);
}
@@ -253,7 +276,13 @@ export function Menu({
return (
<DropdownMenu.Item key={`${child.label}`}>
{child.isFrontendRoute ? (
<Link to={child.url || ''}>{child.label}</Link>
<NavLink
to={child.url || ''}
exact
activeClassName="is-active"
>
{child.label}
</NavLink>
) : (
<a href={child.url}>{child.label}</a>
)}

View File

@@ -56,10 +56,12 @@ test('renders correctly in edit mode', () => {
changed_on_delta_humanized: '',
created_on_delta_humanized: '',
created_by: {
id: 1,
first_name: 'joe',
last_name: 'smith',
},
changed_by: {
id: 2,
first_name: 'tom',
last_name: 'brown',
},

View File

@@ -26,7 +26,7 @@ import { Input } from 'antd';
import { Divider } from 'src/components';
import Button from 'src/components/Button';
import { Tag } from 'src/views/CRUD/types';
import { fetchObjects } from 'src/features/tags/tags';
import { fetchObjectsByTagIds } from 'src/features/tags/tags';
const StyledModalBody = styled.div`
.ant-select-dropdown {
@@ -115,8 +115,8 @@ const TagModal: React.FC<TagModalProps> = ({
};
clearResources();
if (isEditMode) {
fetchObjects(
{ tags: editTag.name, types: null },
fetchObjectsByTagIds(
{ tagIds: [editTag.id], types: null },
(data: Tag[]) => {
data.forEach(updateResourceOptions);
setDashboardsToTag(resourceMap[TaggableResources.Dashboard]);

View File

@@ -194,3 +194,20 @@ export function fetchObjects(
.then(({ json }) => callback(json.result))
.catch(response => error(response));
}
export function fetchObjectsByTagIds(
{
tagIds = [],
types,
}: { tagIds: number[] | undefined; types: string | null },
callback: (json: JsonObject) => void,
error: (response: Response) => void,
) {
let url = `/api/v1/tag/get_objects/?tagIds=${tagIds}`;
if (types) {
url += `&types=${types}`;
}
SupersetClient.get({ endpoint: url })
.then(({ json }) => callback(json.result))
.catch(response => error(response));
}

View File

@@ -31,6 +31,7 @@ export const useDashboard = (idOrSlug: string | number) =>
(dashboard.json_metadata && JSON.parse(dashboard.json_metadata)) || {},
position_data:
dashboard.position_json && JSON.parse(dashboard.position_json),
owners: dashboard.owners || [],
}),
);

View File

@@ -53,6 +53,8 @@ import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import Owner from 'src/types/Owner';
import AlertReportModal from 'src/features/alerts/AlertReportModal';
import { AlertObject, AlertState } from 'src/features/alerts/types';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
@@ -303,18 +305,6 @@ function AlertList({
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
original: { created_by },
},
}: any) =>
created_by ? `${created_by.first_name} ${created_by.last_name}` : '',
Header: t('Created by'),
id: 'created_by',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
@@ -329,10 +319,13 @@ function AlertList({
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
@@ -407,6 +400,10 @@ function AlertList({
disableSortBy: true,
size: 'xl',
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[canDelete, canEdit, isReportEnabled, toggleActive],
);
@@ -448,6 +445,13 @@ function AlertList({
const filters: Filters = useMemo(
() => [
{
Header: t('Name'),
key: 'search',
id: 'name',
input: 'search',
operator: FilterOperator.contains,
},
{
Header: t('Owner'),
key: 'owner',
@@ -465,23 +469,6 @@ function AlertList({
),
paginate: true,
},
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: 'All',
fetchSelects: createFetchRelated(
'report',
'created_by',
createErrorHandler(errMsg =>
t('An error occurred while fetching created by values: %s', errMsg),
),
user,
),
paginate: true,
},
{
Header: t('Status'),
key: 'status',
@@ -504,11 +491,24 @@ function AlertList({
],
},
{
Header: t('Search'),
key: 'search',
id: 'name',
input: 'search',
operator: FilterOperator.contains,
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'report',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
user,
),
paginate: true,
},
],
[],

View File

@@ -33,8 +33,9 @@ import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
import { Tag } from 'src/views/CRUD/types';
import TagModal from 'src/features/tags/TagModal';
import withToasts, { useToasts } from 'src/components/MessageToasts/withToasts';
import { fetchObjects, fetchSingleTag } from 'src/features/tags/tags';
import { fetchObjectsByTagIds, fetchSingleTag } from 'src/features/tags/tags';
import Loading from 'src/components/Loading';
import getOwnerName from 'src/utils/getOwnerName';
interface TaggedObject {
id: number;
@@ -132,7 +133,7 @@ function AllEntities() {
const owner: Owner = {
type: MetadataType.OWNER,
createdBy: `${tag?.created_by.first_name} ${tag?.created_by.last_name}`,
createdBy: getOwnerName(tag?.created_by),
createdOn: tag?.created_on_delta_humanized || '',
};
items.push(owner);
@@ -140,14 +141,18 @@ function AllEntities() {
const lastModified: LastModified = {
type: MetadataType.LAST_MODIFIED,
value: tag?.changed_on_delta_humanized || '',
modifiedBy: `${tag?.changed_by.first_name} ${tag?.changed_by.last_name}`,
modifiedBy: getOwnerName(tag?.changed_by),
};
items.push(lastModified);
const fetchTaggedObjects = () => {
setLoading(true);
fetchObjects(
{ tags: tag?.name || '', types: null },
if (!tag) {
addDangerToast('Error tag object is not referenced!');
return;
}
fetchObjectsByTagIds(
{ tagIds: [tag?.id] || '', types: null },
(data: TaggedObject[]) => {
const objects = { dashboard: [], chart: [], query: [] };
data.forEach(function (object) {

View File

@@ -21,7 +21,6 @@ import React, { useMemo, useState } from 'react';
import rison from 'rison';
import { t, SupersetClient } from '@superset-ui/core';
import { Link, useHistory } from 'react-router-dom';
import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
@@ -36,9 +35,10 @@ import DeleteModal from 'src/components/DeleteModal';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import AnnotationLayerModal from 'src/features/annotationLayers/AnnotationLayerModal';
import { AnnotationLayerObject } from 'src/features/annotationLayers/types';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const PAGE_SIZE = 25;
const MOMENT_FORMAT = 'MMM DD, YYYY';
interface AnnotationLayersListProps {
addDangerToast: (msg: string) => void;
@@ -156,65 +156,16 @@ function AnnotationLayersList({
{
Cell: ({
row: {
original: { changed_on: changedOn },
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => {
const date = new Date(changedOn);
const utc = new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
),
);
return moment(utc).format(MOMENT_FORMAT);
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on',
size: 'xl',
},
{
Cell: ({
row: {
original: { created_on: createdOn },
},
}: any) => {
const date = new Date(createdOn);
const utc = new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
),
);
return moment(utc).format(MOMENT_FORMAT);
},
Header: t('Created on'),
accessor: 'created_on',
size: 'xl',
},
{
accessor: 'created_by',
disableSortBy: true,
Header: t('Created by'),
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleAnnotationLayerEdit(original);
@@ -249,6 +200,10 @@ function AnnotationLayersList({
hidden: !canEdit && !canDelete,
size: 'xl',
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[canDelete, canCreate],
);
@@ -280,15 +235,22 @@ function AnnotationLayersList({
const filters: Filters = useMemo(
() => [
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
Header: t('Name'),
key: 'search',
id: 'name',
input: 'search',
operator: FilterOperator.contains,
},
{
Header: t('Changed by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'annotation_layer',
'created_by',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
@@ -299,13 +261,6 @@ function AnnotationLayersList({
),
paginate: true,
},
{
Header: t('Search'),
key: 'search',
id: 'name',
input: 'search',
operator: FilterOperator.contains,
},
],
[],
);

View File

@@ -154,7 +154,7 @@ function AnnotationList({
() => [
{
accessor: 'short_descr',
Header: t('Label'),
Header: t('Name'),
},
{
accessor: 'long_descr',

View File

@@ -29,7 +29,6 @@ import {
import React, { useState, useMemo, useCallback } from 'react';
import rison from 'rison';
import { uniqBy } from 'lodash';
import moment from 'moment';
import { useSelector } from 'react-redux';
import {
createErrorHandler,
@@ -69,11 +68,13 @@ import setupPlugins from 'src/setup/setupPlugins';
import InfoTooltip from 'src/components/InfoTooltip';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { GenericLink } from 'src/components/GenericLink/GenericLink';
import Owner from 'src/types/Owner';
import { loadTags } from 'src/components/Tags/utils';
import FacePile from 'src/components/FacePile';
import ChartCard from 'src/features/charts/ChartCard';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { findPermission } from 'src/utils/findPermission';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const FlexRowContainer = styled.div`
align-items: center;
@@ -245,10 +246,6 @@ function ChartList(props: ChartListProps) {
});
setPreparingExport(true);
};
const changedByName = (lastSavedBy: Owner) =>
lastSavedBy?.first_name
? `${lastSavedBy?.first_name} ${lastSavedBy?.last_name}`
: null;
function handleBulkChartDelete(chartsToDelete: Chart[]) {
SupersetClient.delete({
@@ -366,7 +363,7 @@ function ChartList(props: ChartListProps) {
)}
</FlexRowContainer>
),
Header: t('Chart'),
Header: t('Name'),
accessor: 'slice_name',
},
{
@@ -375,7 +372,7 @@ function ChartList(props: ChartListProps) {
original: { viz_type: vizType },
},
}: any) => registry.get(vizType)?.name || vizType,
Header: t('Visualization type'),
Header: t('Type'),
accessor: 'viz_type',
size: 'xxl',
},
@@ -438,44 +435,27 @@ function ChartList(props: ChartListProps) {
{
Cell: ({
row: {
original: { last_saved_by: lastSavedBy },
original: { owners = [] },
},
}: any) => <>{changedByName(lastSavedBy)}</>,
Header: t('Modified by'),
accessor: 'last_saved_by.first_name',
}: any) => <FacePile users={owners} />,
Header: t('Owners'),
accessor: 'owners',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
original: { last_saved_at: lastSavedAt },
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => (
<span className="no-wrap">
{lastSavedAt ? moment.utc(lastSavedAt).fromNow() : null}
</span>
),
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'last_saved_at',
size: 'xl',
},
{
accessor: 'owners',
hidden: true,
disableSortBy: true,
},
{
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
Header: t('Created by'),
accessor: 'created_by',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleDelete = () =>
@@ -563,6 +543,10 @@ function ChartList(props: ChartListProps) {
disableSortBy: true,
hidden: !canEdit && !canDelete,
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[
userId,
@@ -597,58 +581,14 @@ function ChartList(props: ChartListProps) {
const filters: Filters = useMemo(() => {
const filters_list = [
{
Header: t('Search'),
Header: t('Name'),
key: 'search',
id: 'slice_name',
input: 'search',
operator: FilterOperator.chartAllText,
},
{
Header: t('Owner'),
key: 'owner',
id: 'owners',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart owners values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart created by values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Chart type'),
Header: t('Type'),
key: 'viz_type',
id: 'viz_type',
input: 'select',
@@ -683,8 +623,43 @@ function ChartList(props: ChartListProps) {
fetchSelects: createFetchDatasets,
paginate: true,
},
...(isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag
? [
{
Header: t('Tag'),
key: 'tags',
id: 'tags',
input: 'select',
operator: FilterOperator.chartTags,
unfilteredLabel: t('All'),
fetchSelects: loadTags,
},
]
: []),
{
Header: t('Dashboards'),
Header: t('Owner'),
key: 'owner',
id: 'owners',
input: 'select',
operator: FilterOperator.relationManyMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'chart',
'owners',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching chart owners values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Dashboard'),
key: 'dashboards',
id: 'dashboards',
input: 'select',
@@ -707,18 +682,27 @@ function ChartList(props: ChartListProps) {
{ label: t('No'), value: false },
],
},
] as Filters;
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag) {
filters_list.push({
Header: t('Tags'),
key: 'tags',
id: 'tags',
{
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.chartTags,
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: loadTags,
});
}
fetchSelects: createFetchRelated(
'chart',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
props.user,
),
paginate: true,
},
] as Filters;
return filters_list;
}, [addDangerToast, favoritesFilter, props.user]);

View File

@@ -21,13 +21,11 @@ import React, { useMemo, useState } from 'react';
import { t, SupersetClient } from '@superset-ui/core';
import rison from 'rison';
import moment from 'moment';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
import { Tooltip } from 'src/components/Tooltip';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
import ListView, {
@@ -37,6 +35,8 @@ import ListView, {
} from 'src/components/ListView';
import CssTemplateModal from 'src/features/cssTemplates/CssTemplateModal';
import { TemplateObject } from 'src/features/cssTemplates/types';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const PAGE_SIZE = 25;
@@ -138,66 +138,12 @@ function CssTemplatesList({
changed_by: changedBy,
},
},
}: any) => {
let name = 'null';
if (changedBy) {
name = `${changedBy.first_name} ${changedBy.last_name}`;
}
return (
<Tooltip
id="allow-run-async-header-tooltip"
title={t('Last modified by %s', name)}
placement="right"
>
<span>{changedOn}</span>
</Tooltip>
);
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
disableSortBy: true,
},
{
Cell: ({
row: {
original: { created_on: createdOn },
},
}: any) => {
const date = new Date(createdOn);
const utc = new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds(),
),
);
return moment(utc).fromNow();
},
Header: t('Created on'),
accessor: 'created_on',
size: 'xl',
disableSortBy: true,
},
{
accessor: 'created_by',
disableSortBy: true,
Header: t('Created by'),
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleCssTemplateEdit(original);
@@ -232,6 +178,10 @@ function CssTemplatesList({
hidden: !canEdit && !canDelete,
size: 'xl',
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[canDelete, canCreate],
);
@@ -270,15 +220,22 @@ function CssTemplatesList({
const filters: Filters = useMemo(
() => [
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
Header: t('Name'),
key: 'search',
id: 'template_name',
input: 'search',
operator: FilterOperator.contains,
},
{
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'css_template',
'created_by',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
@@ -289,13 +246,6 @@ function CssTemplatesList({
),
paginate: true,
},
{
Header: t('Search'),
key: 'search',
id: 'template_name',
input: 'search',
operator: FilterOperator.contains,
},
],
[],
);

View File

@@ -57,13 +57,17 @@ import { Tooltip } from 'src/components/Tooltip';
import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard';
import { Dashboard as CRUDDashboard } from 'src/views/CRUD/types';
import {
Dashboard as CRUDDashboard,
QueryObjectColumns,
} from 'src/views/CRUD/types';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { loadTags } from 'src/components/Tags/utils';
import DashboardCard from 'src/features/dashboards/DashboardCard';
import { DashboardStatus } from 'src/features/dashboards/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { findPermission } from 'src/utils/findPermission';
import { ModifiedInfo } from 'src/components/AuditInfo';
const PAGE_SIZE = 25;
const PASSWORDS_NEEDED_MESSAGE = t(
@@ -108,11 +112,7 @@ const Actions = styled.div`
`;
function DashboardList(props: DashboardListProps) {
const {
addDangerToast,
addSuccessToast,
user: { userId },
} = props;
const { addDangerToast, addSuccessToast, user } = props;
const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
@@ -178,7 +178,7 @@ function DashboardList(props: DashboardListProps) {
};
// TODO: Fix usage of localStorage keying on the user id
const userKey = dangerouslyGetItemDoNotUse(userId?.toString(), null);
const userKey = dangerouslyGetItemDoNotUse(user?.userId?.toString(), null);
const canCreate = hasPerm('can_write');
const canEdit = hasPerm('can_write');
@@ -274,7 +274,7 @@ function DashboardList(props: DashboardListProps) {
original: { id },
},
}: any) =>
userId && (
user?.userId && (
<FaveStar
itemId={id}
saveFaveStar={saveFavoriteStatus}
@@ -285,7 +285,7 @@ function DashboardList(props: DashboardListProps) {
id: 'id',
disableSortBy: true,
size: 'xs',
hidden: !userId,
hidden: !user?.userId,
},
{
Cell: ({
@@ -310,9 +310,20 @@ function DashboardList(props: DashboardListProps) {
{dashboardTitle}
</Link>
),
Header: t('Title'),
Header: t('Name'),
accessor: 'dashboard_title',
},
{
Cell: ({
row: {
original: { status },
},
}: any) =>
status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
Header: t('Status'),
accessor: 'published',
size: 'xl',
},
{
Cell: ({
row: {
@@ -338,49 +349,6 @@ function DashboardList(props: DashboardListProps) {
disableSortBy: true,
hidden: !isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM),
},
{
Cell: ({
row: {
original: { changed_by_name: changedByName },
},
}: any) => <>{changedByName}</>,
Header: t('Modified by'),
accessor: 'changed_by.first_name',
size: 'xl',
},
{
Cell: ({
row: {
original: { status },
},
}: any) =>
status === DashboardStatus.PUBLISHED ? t('Published') : t('Draft'),
Header: t('Status'),
accessor: 'published',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({
row: {
original: { created_by: createdBy },
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
Header: t('Created by'),
accessor: 'created_by',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
@@ -392,6 +360,19 @@ function DashboardList(props: DashboardListProps) {
disableSortBy: true,
size: 'xl',
},
{
Cell: ({
row: {
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
const handleDelete = () =>
@@ -475,9 +456,13 @@ function DashboardList(props: DashboardListProps) {
hidden: !canEdit && !canDelete && !canExport,
disableSortBy: true,
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[
userId,
user?.userId,
canEdit,
canDelete,
canExport,
@@ -509,12 +494,37 @@ function DashboardList(props: DashboardListProps) {
const filters: Filters = useMemo(() => {
const filters_list = [
{
Header: t('Search'),
Header: t('Name'),
key: 'search',
id: 'dashboard_title',
input: 'search',
operator: FilterOperator.titleOrSlug,
},
{
Header: t('Status'),
key: 'published',
id: 'published',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Published'), value: true },
{ label: t('Draft'), value: false },
],
},
...(isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag
? [
{
Header: t('Tag'),
key: 'tags',
id: 'tags',
input: 'select',
operator: FilterOperator.dashboardTags,
unfilteredLabel: t('All'),
fetchSelects: loadTags,
},
]
: []),
{
Header: t('Owner'),
key: 'owner',
@@ -537,41 +547,7 @@ function DashboardList(props: DashboardListProps) {
),
paginate: true,
},
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dashboard',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching dashboard created by values: %s',
errMsg,
),
),
),
props.user,
),
paginate: true,
},
{
Header: t('Status'),
key: 'published',
id: 'published',
input: 'select',
operator: FilterOperator.equals,
unfilteredLabel: t('Any'),
selects: [
{ label: t('Published'), value: true },
{ label: t('Draft'), value: false },
],
},
...(userId ? [favoritesFilter] : []),
...(user?.userId ? [favoritesFilter] : []),
{
Header: t('Certified'),
key: 'certified',
@@ -585,18 +561,27 @@ function DashboardList(props: DashboardListProps) {
{ label: t('No'), value: false },
],
},
] as Filters;
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM) && canReadTag) {
filters_list.push({
Header: t('Tags'),
key: 'tags',
id: 'tags',
{
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.dashboardTags,
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: loadTags,
});
}
fetchSelects: createFetchRelated(
'dashboard',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
user,
),
paginate: true,
},
] as Filters;
return filters_list;
}, [addDangerToast, favoritesFilter, props.user]);
@@ -632,7 +617,7 @@ function DashboardList(props: DashboardListProps) {
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
userId={userId}
userId={user?.userId}
loading={loading}
openDashboardEditModal={openDashboardEditModal}
saveFavoriteStatus={saveFavoriteStatus}
@@ -646,7 +631,7 @@ function DashboardList(props: DashboardListProps) {
favoriteStatus,
hasPerm,
loading,
userId,
user?.userId,
saveFavoriteStatus,
userKey,
],
@@ -743,7 +728,7 @@ function DashboardList(props: DashboardListProps) {
addSuccessToast,
addDangerToast,
undefined,
userId,
user?.userId,
);
setDashboardToDelete(null);
}}

View File

@@ -218,7 +218,7 @@ describe('Admin DatabaseList', () => {
await waitForComponentToPaint(wrapper);
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t),(col:allow_run_async,opr:eq,value:!f),(col:database_name,opr:ct,value:fooo)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
`"http://localhost/api/v1/database/?q=(filters:!((col:database_name,opr:ct,value:fooo),(col:expose_in_sqllab,opr:eq,value:!t),(col:allow_run_async,opr:eq,value:!f)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
);
});

View File

@@ -32,7 +32,11 @@ import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
import Loading from 'src/components/Loading';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createErrorHandler, uploadUserPerms } from 'src/views/CRUD/utils';
import {
createErrorHandler,
createFetchRelated,
uploadUserPerms,
} from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
@@ -48,6 +52,8 @@ import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import type { MenuObjectProps } from 'src/types/bootstrapTypes';
import DatabaseModal from 'src/features/databases/DatabaseModal';
import { DatabaseObject } from 'src/features/databases/types';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
const DatabaseDeleteRelatedExtension = extensionsRegistry.get(
@@ -67,6 +73,11 @@ interface DatabaseDeleteObject extends DatabaseObject {
interface DatabaseListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
user: {
userId: string | number;
firstName: string;
lastName: string;
};
}
const IconCheck = styled(Icons.Check)`
@@ -90,7 +101,11 @@ function BooleanDisplay({ value }: { value: Boolean }) {
return value ? <IconCheck /> : <IconCancelX />;
}
function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
function DatabaseList({
addDangerToast,
addSuccessToast,
user,
}: DatabaseListProps) {
const {
state: {
loading,
@@ -105,7 +120,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
t('database'),
addDangerToast,
);
const user = useSelector<any, UserWithPermissionsAndRoles>(
const fullUser = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
const showDatabaseModal = getUrlParam(URL_PARAMS.showDatabaseModal);
@@ -123,11 +138,11 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
null,
);
const [allowUploads, setAllowUploads] = useState<boolean>(false);
const isAdmin = isUserAdmin(user);
const isAdmin = isUserAdmin(fullUser);
const showUploads = allowUploads || isAdmin;
const [preparingExport, setPreparingExport] = useState<boolean>(false);
const { roles } = user;
const { roles } = fullUser;
const {
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
@@ -313,7 +328,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
() => [
{
accessor: 'database_name',
Header: t('Database'),
Header: t('Name'),
},
{
accessor: 'backend',
@@ -380,23 +395,14 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
size: 'md',
},
{
accessor: 'created_by',
disableSortBy: true,
Header: t('Created by'),
Cell: ({
row: {
original: { created_by: createdBy },
original: {
changed_by: changedBy,
changed_on_delta_humanized: changedOn,
},
},
}: any) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => changedOn,
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
@@ -470,12 +476,23 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
hidden: !canEdit && !canDelete,
disableSortBy: true,
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[canDelete, canEdit, canExport],
);
const filters: Filters = useMemo(
() => [
{
Header: t('Name'),
key: 'search',
id: 'database_name',
input: 'search',
operator: FilterOperator.contains,
},
{
Header: t('Expose in SQL Lab'),
key: 'expose_in_sql_lab',
@@ -509,11 +526,24 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
],
},
{
Header: t('Search'),
key: 'search',
id: 'database_name',
input: 'search',
operator: FilterOperator.contains,
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'database',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
user,
),
paginate: true,
},
],
[],

View File

@@ -285,56 +285,41 @@ describe('RTL', () => {
});
describe('Prevent unsafe URLs', () => {
const columnCount = 8;
const exploreUrlIndex = 1;
const getTdIndex = (rowNumber: number): number =>
rowNumber * columnCount + exploreUrlIndex;
const mockedProps = {};
let wrapper: any;
it('Check prevent unsafe is on renders relative links', async () => {
const tdColumnsNumber = 9;
useSelectorMock.mockReturnValue(true);
wrapper = await mountAndWait(mockedProps);
const tdElements = wrapper.find(ListView).find('td');
expect(
tdElements
.at(0 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('/https://www.google.com?0');
expect(
tdElements
.at(1 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('/https://www.google.com?1');
expect(
tdElements
.at(2 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('/https://www.google.com?2');
expect(tdElements.at(getTdIndex(0)).find('a').prop('href')).toBe(
'/https://www.google.com?0',
);
expect(tdElements.at(getTdIndex(1)).find('a').prop('href')).toBe(
'/https://www.google.com?1',
);
expect(tdElements.at(getTdIndex(2)).find('a').prop('href')).toBe(
'/https://www.google.com?2',
);
});
it('Check prevent unsafe is off renders absolute links', async () => {
const tdColumnsNumber = 9;
useSelectorMock.mockReturnValue(false);
wrapper = await mountAndWait(mockedProps);
const tdElements = wrapper.find(ListView).find('td');
expect(
tdElements
.at(0 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('https://www.google.com?0');
expect(
tdElements
.at(1 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('https://www.google.com?1');
expect(
tdElements
.at(2 * tdColumnsNumber + 1)
.find('a')
.prop('href'),
).toBe('https://www.google.com?2');
expect(tdElements.at(getTdIndex(0)).find('a').prop('href')).toBe(
'https://www.google.com?0',
);
expect(tdElements.at(getTdIndex(1)).find('a').prop('href')).toBe(
'https://www.google.com?1',
);
expect(tdElements.at(getTdIndex(2)).find('a').prop('href')).toBe(
'https://www.google.com?2',
);
});
});

View File

@@ -70,6 +70,8 @@ import {
} from 'src/features/datasets/constants';
import DuplicateDatasetModal from 'src/features/datasets/DuplicateDatasetModal';
import { useSelector } from 'react-redux';
import { ModifiedInfo } from 'src/components/AuditInfo';
import { QueryObjectColumns } from 'src/views/CRUD/types';
const extensionsRegistry = getExtensionsRegistry();
const DatasetDeleteRelatedExtension = extensionsRegistry.get(
@@ -380,26 +382,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
accessor: 'schema',
size: 'lg',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
},
}: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_by_name: changedByName },
},
}: any) => changedByName,
Header: t('Modified by'),
accessor: 'changed_by.first_name',
size: 'xl',
},
{
accessor: 'database',
disableSortBy: true,
@@ -416,6 +398,19 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true,
size: 'lg',
},
{
Cell: ({
row: {
original: {
changed_on_delta_humanized: changedOn,
changed_by: changedBy,
},
},
}: any) => <ModifiedInfo date={changedOn} user={changedBy} />,
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
},
{
accessor: 'sql',
hidden: true,
@@ -515,6 +510,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
hidden: !canEdit && !canDelete && !canDuplicate,
disableSortBy: true,
},
{
accessor: QueryObjectColumns.changed_by,
hidden: true,
},
],
[canEdit, canDelete, canExport, openDatasetEditModal, canDuplicate, user],
);
@@ -522,31 +521,23 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const filterTypes: Filters = useMemo(
() => [
{
Header: t('Search'),
Header: t('Name'),
key: 'search',
id: 'table_name',
input: 'search',
operator: FilterOperator.contains,
},
{
Header: t('Owner'),
key: 'owner',
id: 'owners',
Header: t('Type'),
key: 'sql',
id: 'sql',
input: 'select',
operator: FilterOperator.relationManyMany,
operator: FilterOperator.datasetIsNullOrEmpty,
unfilteredLabel: 'All',
fetchSelects: createFetchRelated(
'dataset',
'owners',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset owner values: %s',
errMsg,
),
),
user,
),
paginate: true,
selects: [
{ label: t('Virtual'), value: false },
{ label: t('Physical'), value: true },
],
},
{
Header: t('Database'),
@@ -581,16 +572,24 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
paginate: true,
},
{
Header: t('Type'),
key: 'sql',
id: 'sql',
Header: t('Owner'),
key: 'owner',
id: 'owners',
input: 'select',
operator: FilterOperator.datasetIsNullOrEmpty,
operator: FilterOperator.relationManyMany,
unfilteredLabel: 'All',
selects: [
{ label: t('Virtual'), value: false },
{ label: t('Physical'), value: true },
],
fetchSelects: createFetchRelated(
'dataset',
'owners',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset owner values: %s',
errMsg,
),
),
user,
),
paginate: true,
},
{
Header: t('Certified'),
@@ -605,6 +604,26 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
{ label: t('No'), value: false },
],
},
{
Header: t('Modified by'),
key: 'changed_by',
id: 'changed_by',
input: 'select',
operator: FilterOperator.relationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'dataset',
'changed_by',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching dataset datasource values: %s',
errMsg,
),
),
user,
),
paginate: true,
},
],
[user],
);

Some files were not shown because too many files have changed in this diff Show More