mirror of
https://github.com/apache/superset.git
synced 2026-06-20 15:09:27 +00:00
Compare commits
32 Commits
fix/helm-r
...
3.1.0rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc29cee9a | ||
|
|
be81aaa31a | ||
|
|
8185ac3e33 | ||
|
|
463989dbc9 | ||
|
|
38b8b03f90 | ||
|
|
d0961d0ed8 | ||
|
|
b699df7030 | ||
|
|
c2612d8c26 | ||
|
|
ec0a338aa3 | ||
|
|
6fa75b7047 | ||
|
|
77c73b63db | ||
|
|
fb50819fcd | ||
|
|
5c24c580dd | ||
|
|
2104a9a853 | ||
|
|
0925d75dfa | ||
|
|
96c0497fa9 | ||
|
|
5bcd3ef17e | ||
|
|
5ec1edc876 | ||
|
|
aaa50c4b4a | ||
|
|
880086c750 | ||
|
|
d0aa34bf79 | ||
|
|
77332bfb38 | ||
|
|
ceac19fa2f | ||
|
|
79d5975028 | ||
|
|
4a4f9983df | ||
|
|
26e59662fb | ||
|
|
fad4616d2f | ||
|
|
2c3bf2895f | ||
|
|
93319696de | ||
|
|
e382d0dd28 | ||
|
|
f4fd0e19e2 | ||
|
|
c8844bdd5e |
396
CHANGELOG.md
396
CHANGELOG.md
@@ -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**
|
||||
|
||||
10
UPDATING.md
10
UPDATING.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
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"` | |
|
||||
|
||||
@@ -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" . }}
|
||||
|
||||
@@ -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", )
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
5
setup.py
5
setup.py
@@ -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"],
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -67,6 +67,7 @@ function SafeMarkdown({
|
||||
rehypePlugins={rehypePlugins}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={false}
|
||||
transformLinkUri={null}
|
||||
>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => [
|
||||
{
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,6 +24,11 @@ export const defaultGrid = {
|
||||
|
||||
export const defaultYAxis = {
|
||||
scale: true,
|
||||
yAxisLabelRotation: 0,
|
||||
};
|
||||
|
||||
export const defaultXAxis = {
|
||||
xAxisLabelRotation: 0,
|
||||
};
|
||||
|
||||
export const defaultLegendPadding = {
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('Bubble transformProps', () => {
|
||||
expressionType: 'simple',
|
||||
label: 'SUM(sales)',
|
||||
},
|
||||
xAxisBounds: [null, null],
|
||||
yAxisBounds: [null, null],
|
||||
};
|
||||
const chartProps = new ChartProps({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -251,8 +251,7 @@ const QueryTable = ({
|
||||
modalBody={
|
||||
<ResultSet
|
||||
showSql
|
||||
user={user}
|
||||
query={query}
|
||||
queryId={query.id}
|
||||
height={400}
|
||||
displayLimit={displayLimit}
|
||||
defaultQueryLimit={1000}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
106
superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
Normal file
106
superset-frontend/src/SqlLab/components/SouthPane/Results.tsx
Normal 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;
|
||||
@@ -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}\``);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
30
superset-frontend/src/components/AuditInfo/index.tsx
Normal file
30
superset-frontend/src/components/AuditInfo/index.tsx
Normal 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;
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
' ',
|
||||
);
|
||||
sizerRef.current.textContent = currentTitle || placeholder;
|
||||
}
|
||||
}, [currentTitle, placeholder, sizerRef]);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
() => (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 || [],
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -154,7 +154,7 @@ function AnnotationList({
|
||||
() => [
|
||||
{
|
||||
accessor: 'short_descr',
|
||||
Header: t('Label'),
|
||||
Header: t('Name'),
|
||||
},
|
||||
{
|
||||
accessor: 'long_descr',
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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)"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user