Compare commits
217 Commits
1.3.1
...
v2021.36.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d35a91642 | ||
|
|
cc821bb747 | ||
|
|
37c2020035 | ||
|
|
9de2196b7f | ||
|
|
effcf3b50f | ||
|
|
7faa5c6aff | ||
|
|
3fe2e6ec73 | ||
|
|
7cbced8833 | ||
|
|
df5c0fbce9 | ||
|
|
e60b489867 | ||
|
|
be77ad2288 | ||
|
|
359383b578 | ||
|
|
5f8dff1341 | ||
|
|
9bb890ebed | ||
|
|
4e380db3fd | ||
|
|
a0db5367d2 | ||
|
|
070fdbeebc | ||
|
|
02798a3517 | ||
|
|
2e11b05d73 | ||
|
|
75a1b19174 | ||
|
|
e947f8a4c8 | ||
|
|
80c39daa85 | ||
|
|
e024f8c7d6 | ||
|
|
68c2a6d43a | ||
|
|
c5a5cf7db9 | ||
|
|
1f1e2dd29a | ||
|
|
f001c44727 | ||
|
|
d25b0967a1 | ||
|
|
ad8336a5b4 | ||
|
|
e2469162fa | ||
|
|
a616b8785e | ||
|
|
960d1e4d5d | ||
|
|
2199f65373 | ||
|
|
8aa24e50d9 | ||
|
|
90e2f09c67 | ||
|
|
1ffd73d03b | ||
|
|
62d8ab7f9c | ||
|
|
147637a02d | ||
|
|
8adc31d14c | ||
|
|
a413f796a6 | ||
|
|
ee2eccdb67 | ||
|
|
fd6456186d | ||
|
|
f422f1ea49 | ||
|
|
8ad495a572 | ||
|
|
ac1d779a30 | ||
|
|
577ede4b12 | ||
|
|
c66d6d83b9 | ||
|
|
1c71eda70f | ||
|
|
1badcaed45 | ||
|
|
ec087507e5 | ||
|
|
18be181946 | ||
|
|
db11c3e6c8 | ||
|
|
93c60e4021 | ||
|
|
6a2cec51c5 | ||
|
|
08b8aa277f | ||
|
|
78d8089b18 | ||
|
|
5e472980a6 | ||
|
|
631ad02a76 | ||
|
|
e71c6e60e4 | ||
|
|
5eded9fe1b | ||
|
|
c0e9006eb7 | ||
|
|
81241b6024 | ||
|
|
35864748f2 | ||
|
|
6a5568764e | ||
|
|
575e7af859 | ||
|
|
9a37ad1a1e | ||
|
|
f6637cac7d | ||
|
|
1fc9318594 | ||
|
|
c14364c616 | ||
|
|
486ef6b81f | ||
|
|
a6aad52e38 | ||
|
|
c768941f2f | ||
|
|
0cdc7675b4 | ||
|
|
bc4b6f0a6c | ||
|
|
7e4c940314 | ||
|
|
970d762779 | ||
|
|
3faf653e5f | ||
|
|
a9f502b67b | ||
|
|
c5081991fc | ||
|
|
649e509607 | ||
|
|
518c3c9ae0 | ||
|
|
adebc0997b | ||
|
|
13a2ee373c | ||
|
|
ea803c3d1c | ||
|
|
575ee24a99 | ||
|
|
50d896f1b7 | ||
|
|
37f09bd296 | ||
|
|
86f4e691d4 | ||
|
|
c5c28618a5 | ||
|
|
d75da748d5 | ||
|
|
42cd21e383 | ||
|
|
ec8d3b03e4 | ||
|
|
afb8bd5fe6 | ||
|
|
efe850b731 | ||
|
|
a547dcb73e | ||
|
|
2c595b09ea | ||
|
|
5f060a2227 | ||
|
|
482dffb1db | ||
|
|
9075e4206c | ||
|
|
4960b5ee2b | ||
|
|
7a284bb9e8 | ||
|
|
d5f63a74e4 | ||
|
|
b87e0b32ac | ||
|
|
ac8e54d909 | ||
|
|
3c0aefb61a | ||
|
|
4119bb9c1e | ||
|
|
8a36356f49 | ||
|
|
f581e0402b | ||
|
|
f5fbfef618 | ||
|
|
203c311ca3 | ||
|
|
5e75baf0cc | ||
|
|
9876c36f6e | ||
|
|
d13b081cfe | ||
|
|
2be84e78d2 | ||
|
|
be7065faf8 | ||
|
|
36bc7b0b80 | ||
|
|
0df15bf207 | ||
|
|
36abc51f90 | ||
|
|
b5c7ed9f18 | ||
|
|
7b724439b9 | ||
|
|
8e07dd28bc | ||
|
|
ee9a384758 | ||
|
|
542b864e61 | ||
|
|
0668eaad6a | ||
|
|
22231addec | ||
|
|
3709131089 | ||
|
|
d46dc9aa45 | ||
|
|
9b2dffeb1d | ||
|
|
24b43beff9 | ||
|
|
a5dbe6a14d | ||
|
|
f94695480a | ||
|
|
720e5b111a | ||
|
|
c09f6ed15b | ||
|
|
4ae88ce3b4 | ||
|
|
2611681de9 | ||
|
|
6cd15d54a0 | ||
|
|
d6f9c48aa1 | ||
|
|
517a678cd7 | ||
|
|
5d3d6b6eae | ||
|
|
cdcc161846 | ||
|
|
9d0dc561fc | ||
|
|
171514360e | ||
|
|
67c4c0116e | ||
|
|
a1e18ed110 | ||
|
|
2c5731aea3 | ||
|
|
16a9d219ed | ||
|
|
a16e290765 | ||
|
|
b61c34f7c9 | ||
|
|
7de54d016e | ||
|
|
5a8484185b | ||
|
|
c79de7abd7 | ||
|
|
b4555dfa4f | ||
|
|
ccfc95fbe6 | ||
|
|
6c304b83a9 | ||
|
|
98fc29cbbb | ||
|
|
4df3672baa | ||
|
|
3aefa6925b | ||
|
|
a30d884cfc | ||
|
|
9841c78967 | ||
|
|
a0c9b9d9c2 | ||
|
|
6df16c4b1f | ||
|
|
628169a171 | ||
|
|
2dc0bdda5d | ||
|
|
9f52c103ac | ||
|
|
a3102488a1 | ||
|
|
5e64d65a8b | ||
|
|
7b3fce7e81 | ||
|
|
3f86a54ac1 | ||
|
|
fd80ae34a3 | ||
|
|
f0e3b68cc2 | ||
|
|
63ace7b288 | ||
|
|
5488a8a948 | ||
|
|
6e1d16d956 | ||
|
|
a70248736f | ||
|
|
273ab3d257 | ||
|
|
07f33998ac | ||
|
|
bb1d8fe4ef | ||
|
|
79e8d77acc | ||
|
|
3712ee02fa | ||
|
|
606a7bf429 | ||
|
|
5ce38839e7 | ||
|
|
a51851308b | ||
|
|
b7cc89c6d4 | ||
|
|
578a9e9d53 | ||
|
|
6ac4f4ef2f | ||
|
|
2db1615c83 | ||
|
|
df50a47777 | ||
|
|
b07c80a839 | ||
|
|
ddb5005900 | ||
|
|
3bbcc30d69 | ||
|
|
85ae8e3477 | ||
|
|
85329c374e | ||
|
|
22d8d582f8 | ||
|
|
28c383af68 | ||
|
|
772da8de63 | ||
|
|
b80f018691 | ||
|
|
6edc1ee3bb | ||
|
|
e59f318ef9 | ||
|
|
423ff50768 | ||
|
|
2bfc1c29c5 | ||
|
|
e6292a89bb | ||
|
|
23072161e2 | ||
|
|
b72fd7b9f4 | ||
|
|
e6274e0764 | ||
|
|
af204ff449 | ||
|
|
1dbd1e9f02 | ||
|
|
a59d458e41 | ||
|
|
11a2d4dfdd | ||
|
|
7ef97a54e2 | ||
|
|
7effa44d54 | ||
|
|
4359650b7d | ||
|
|
7c95595b77 | ||
|
|
86cecaeec5 | ||
|
|
2c55cc6558 | ||
|
|
1917464d2b | ||
|
|
7332055ff6 | ||
|
|
490890de23 |
6
.github/CODEOWNERS
vendored
@@ -3,6 +3,6 @@
|
||||
/superset/migrations/ @apache/superset-committers
|
||||
|
||||
# Notify Preset team when ephemeral env settings are changed
|
||||
.github/workflows/ecs-task-definition.json @robdiciuccio @craig-rueda @willbarrett @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
.github/workflows/docker-ephemeral-env.yml @robdiciuccio @craig-rueda @willbarrett @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
.github/workflows/ephemeral*.yml @robdiciuccio @craig-rueda @willbarrett @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
.github/workflows/ecs-task-definition.json @robdiciuccio @craig-rueda @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
.github/workflows/docker-ephemeral-env.yml @robdiciuccio @craig-rueda @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
.github/workflows/ephemeral*.yml @robdiciuccio @craig-rueda @rusackas @eschutho @dpgaspar @nytai @mistercrunch
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -11,6 +11,7 @@
|
||||
<!--- Check any relevant boxes with "x" -->
|
||||
<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->
|
||||
- [ ] Has associated issue:
|
||||
- [ ] Required feature flags:
|
||||
- [ ] Changes UI
|
||||
- [ ] Includes DB Migration (follow approval process in [SIP-59](https://github.com/apache/superset/issues/13351))
|
||||
- [ ] Migration is atomic, supports rollback & is backwards-compatible
|
||||
|
||||
@@ -24,9 +24,10 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.790
|
||||
rev: v0.910
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-all]
|
||||
- repo: https://github.com/peterdemin/pip-compile-multi
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
|
||||
14
.pylintrc
@@ -70,7 +70,8 @@ confidence=
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
#enable=
|
||||
enable=
|
||||
useless-suppression,
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
@@ -84,13 +85,10 @@ confidence=
|
||||
disable=
|
||||
missing-docstring,
|
||||
too-many-lines,
|
||||
ungrouped-imports,
|
||||
import-outside-toplevel,
|
||||
raise-missing-from,
|
||||
super-with-arguments,
|
||||
too-few-public-methods,
|
||||
too-many-locals,
|
||||
|
||||
duplicate-code,
|
||||
unspecified-encoding,
|
||||
# re-enable once this no longer raises false positives
|
||||
too-many-instance-attributes
|
||||
|
||||
[REPORTS]
|
||||
|
||||
|
||||
45
CHANGELOG.md
@@ -17,50 +17,7 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
## Change Log
|
||||
### 1.3.1
|
||||
**Database Migrations**
|
||||
|
||||
**Features**
|
||||
- [#16711](https://github.com/apache/superset/pull/16711) feat(jinja): improve url parameter formatting (@villebro)
|
||||
- [#14955](https://github.com/apache/superset/pull/14955) feat: show build number value in the About if present in the config (@cccs-joel)
|
||||
- [#16594](https://github.com/apache/superset/pull/16594) feat: Experimental cross-filter plugins (@simcha90)
|
||||
- [#16416](https://github.com/apache/superset/pull/16416) feat: add Shillelagh DB engine spec (@betodealmeida)
|
||||
- [#16167](https://github.com/apache/superset/pull/16167) feat: Adding Rockset db engine spec (@srinify)
|
||||
|
||||
**Fixes**
|
||||
- [#16776](https://github.com/apache/superset/pull/16776) fix(dataset): retain is_dttm if set on metadata sync (@villebro)
|
||||
- [#16716](https://github.com/apache/superset/pull/16716) fix(pandas-postprocessing): percentage compare to use correct column (@villebro)
|
||||
- [#16692](https://github.com/apache/superset/pull/16692) fix: catch exception when create connection (@zhaoyongjie)
|
||||
- [#16699](https://github.com/apache/superset/pull/16699) fix(explore): only refresh data panel on relevant changes (@villebro)
|
||||
- [#16687](https://github.com/apache/superset/pull/16687) fix: don't send invalid URLs back to the user (@dpgaspar)
|
||||
- [#16662](https://github.com/apache/superset/pull/16662) fix: fix assignment in FilterBoxViz (@tianhe1986)
|
||||
- [#16634](https://github.com/apache/superset/pull/16634) fix(sqla): support for date adhoc filter (@villebro)
|
||||
- [#16536](https://github.com/apache/superset/pull/16536) fix: params in sql lab are jumpy in the ace editor (@eschutho)
|
||||
- [#16614](https://github.com/apache/superset/pull/16614) fix: TemporalWrapperType string representation (@villebro)
|
||||
- [#16452](https://github.com/apache/superset/pull/16452) fix: queryEditor bug (@AAfghahi)
|
||||
- [#16374](https://github.com/apache/superset/pull/16374) fix: update table ID in query context on chart import (@betodealmeida)
|
||||
- [#16289](https://github.com/apache/superset/pull/16289) fix: improve pivot post-processing (@betodealmeida)
|
||||
- [#16262](https://github.com/apache/superset/pull/16262) fix: pivot col names in post_process (@betodealmeida)
|
||||
- [#16592](https://github.com/apache/superset/pull/16592) fix: Remove export CSV in old filter box (@duynguyenhoang)
|
||||
- [#16573](https://github.com/apache/superset/pull/16573) fix: impersonate user label/tooltip (@betodealmeida)
|
||||
- [#16412](https://github.com/apache/superset/pull/16412) fix: Support Jinja template functions in global async queries (@robdiciuccio)
|
||||
- [#16482](https://github.com/apache/superset/pull/16482) fix: can't drop column when name overlap (@zhaoyongjie)
|
||||
- [#16526](https://github.com/apache/superset/pull/16526) fix: Set correct comparison operator for snowflake-sqlalchemy pinning (@danielewood)
|
||||
- [#16372](https://github.com/apache/superset/pull/16372) fix: ensure setting operator to `None` (#16371) (@grumpy-miner)
|
||||
- [#16515](https://github.com/apache/superset/pull/16515) fix: Pin snowflake-sqlalchemy to 1.2.4 (@danielewood)
|
||||
- [#16468](https://github.com/apache/superset/pull/16468) fix(native-filters): handle undefined control value gracefully (@villebro)
|
||||
- [#16464](https://github.com/apache/superset/pull/16464) fix: prevent page crash when chart can't render (@zhaoyongjie)
|
||||
- [#16460](https://github.com/apache/superset/pull/16460) fix(native-filters): handle null values in value filter (@villebro)
|
||||
- [#16299](https://github.com/apache/superset/pull/16299) fix: copy to Clipboard order (@AAfghahi)
|
||||
- [#16369](https://github.com/apache/superset/pull/16369) fix: call external metadata endpoint with correct rison object (@villebro)
|
||||
- [#16293](https://github.com/apache/superset/pull/16293) fix(sqlite): week grain refer to day of week (@villebro)
|
||||
|
||||
**Others**
|
||||
- [#16702](https://github.com/apache/superset/pull/16702) perf(dashboard): native filter select will be stuck if there has a filter box. (@stephenLYZ)
|
||||
- [#16648](https://github.com/apache/superset/pull/16648) chore: Bump Flask-OpenID to 1.3.0 (@dpgaspar)
|
||||
- [#16193](https://github.com/apache/superset/pull/16193) refactor: external metadata fetch API (@zhaoyongjie)
|
||||
|
||||
### 1.3.0 (Fri Aug 13 20:41:03 2021 -0700)
|
||||
### 1.3.0 (2021-08-13)
|
||||
**Database Migrations**
|
||||
- [#16160](https://github.com/apache/superset/pull/16160) feat: change query predicate to text (@eschutho)
|
||||
- [#16077](https://github.com/apache/superset/pull/16077) fix: ensure that users viewing chart does not automatically save edit data (@pkdotson)
|
||||
|
||||
@@ -106,7 +106,7 @@ This statement thanks the following, on which it draws for content and inspirati
|
||||
|
||||
# Slack Community Guidelines
|
||||
|
||||
If you decide to join the [Community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ), please adhere to the following rules:
|
||||
If you decide to join the [Community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw), please adhere to the following rules:
|
||||
|
||||
**1. Treat everyone in the community with respect.**
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ RUN /frontend-mem-nag.sh \
|
||||
|
||||
# Next, copy in the rest and let webpack do its thing
|
||||
COPY ./superset-frontend /app/superset-frontend
|
||||
# This is BY FAR the most expensive step (thanks Terser!)
|
||||
# This seems to be the most expensive step
|
||||
RUN cd /app/superset-frontend \
|
||||
&& npm run ${BUILD_CMD} \
|
||||
&& rm -rf node_modules
|
||||
|
||||
11
Makefile
@@ -58,7 +58,7 @@ update-py:
|
||||
|
||||
update-js:
|
||||
# Install js packages
|
||||
cd superset-frontend; npm install
|
||||
cd superset-frontend; npm ci
|
||||
|
||||
venv:
|
||||
# Create a virtual environment and activate it (recommended)
|
||||
@@ -66,6 +66,9 @@ venv:
|
||||
test -d venv || ${PYTHON} -m venv venv # setup a python3 virtualenv
|
||||
. venv/bin/activate
|
||||
|
||||
make activate:
|
||||
source venv/bin/activate
|
||||
|
||||
pre-commit:
|
||||
# setup pre commit dependencies
|
||||
pip3 install -r requirements/integration.txt
|
||||
@@ -81,3 +84,9 @@ py-lint: pre-commit
|
||||
|
||||
js-format:
|
||||
cd superset-frontend; npm run prettier
|
||||
|
||||
flask-app:
|
||||
flask run -p 8088 --with-threads --reload --debugger
|
||||
|
||||
node-app:
|
||||
cd superset-frontend; npm run dev-server
|
||||
|
||||
@@ -25,7 +25,7 @@ under the License.
|
||||
[](https://badge.fury.io/py/apache-superset)
|
||||
[](https://codecov.io/github/apache/superset)
|
||||
[](https://pypi.python.org/pypi/apache-superset)
|
||||
[](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
|
||||
[](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
[](https://superset.apache.org)
|
||||
[](https://david-dm.org/apache/superset?path=superset-frontend)
|
||||
|
||||
@@ -136,7 +136,7 @@ Want to add support for your datastore or data engine? Read more [here](https://
|
||||
## Get Involved
|
||||
|
||||
- Ask and answer questions on [StackOverflow](https://stackoverflow.com/questions/tagged/apache-superset) using the **apache-superset** tag
|
||||
- [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
|
||||
- [Join our community's Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
and please read our [Slack Community Guidelines](https://github.com/apache/superset/blob/master/CODE_OF_CONDUCT.md#slack-community-guidelines)
|
||||
- [Join our dev@superset.apache.org Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ on the Superset Slack. People crafting releases and those interested in
|
||||
partaking in the process should join the channel.
|
||||
|
||||
## Release notes for recent releases
|
||||
|
||||
- [1.3](release-notes-1-3/README.md)
|
||||
- [1.2](release-notes-1-2/README.md)
|
||||
- [1.1](release-notes-1-1/README.md)
|
||||
- [1.0](release-notes-1-0/README.md)
|
||||
|
||||
@@ -384,12 +384,12 @@ def change_log(
|
||||
with open(csv, "w") as csv_file:
|
||||
log_items = list(logs)
|
||||
field_names = log_items[0].keys()
|
||||
writer = lib_csv.DictWriter(
|
||||
writer = lib_csv.DictWriter( # type: ignore
|
||||
csv_file,
|
||||
delimiter=",",
|
||||
quotechar='"',
|
||||
quoting=lib_csv.QUOTE_ALL,
|
||||
fieldnames=field_names,
|
||||
fieldnames=field_names, # type: ignore
|
||||
)
|
||||
writer.writeheader()
|
||||
for log in logs:
|
||||
|
||||
73
RELEASING/release-notes-1-3/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Release Notes for Superset 1.3
|
||||
|
||||
Superset 1.3 focuses on hardening and polishing the superset user experience, with tons of UX improvements and bug fixes focused on charts, dashboards, and the new dashboard-native filters.
|
||||
|
||||
- [**User Experience**](#user-experience)
|
||||
- [**PR Highlights**](#pr-highlights)
|
||||
- [**Breaking Changes and Full Changelog**](#breaking-changes-and-full-changelog)
|
||||
|
||||
# User Experience
|
||||
One major goal of this release is to improve and harden dashboard-native filters. These filters live at the dashboard level instead of within a chart and affect all charts under their scope within a dashboard. Improvements in this release include clearer visual indicators of what charts are within the scope of a selected filter.
|
||||
|
||||

|
||||
|
||||
Native-filters can also be set to load collapsed, which also improves connected thumbnail and alerts/reports functionality.
|
||||
|
||||

|
||||
|
||||
For charts, we've added a new funnel chart.
|
||||
|
||||

|
||||
|
||||
Users can also now use Jinja templating in calculated columns and SQL metrics.
|
||||
|
||||

|
||||
|
||||
At the dashboard level, work has been focused on improving available information and UX ergonomics. Users can now download a full .csv of the full dataset behind a table chart from the dashboard.
|
||||
|
||||

|
||||
|
||||
Continuing on the theme of making more things accessible directly from the dashboard, users can now view the SQL Query behind any chart directly from the dashboard as well.
|
||||
|
||||

|
||||
|
||||
# Developer Experience
|
||||
The API has received a new endpoint to allow the developer to pass DB-specific parameters instead of the full SQLAlchemy URI.
|
||||
|
||||
# Database Connectivity
|
||||
We have improved support for Ascend.io's engine spec and fixed a long list of bugs.
|
||||
|
||||
Also in the works is a new database connection UI, which should make connecting to a database easier without having to put together a SQLAlchemy URI. It's behind a feature flag for now, but it can be turned on in config.py with `FORCE_DATABASE_CONNECTIONS_SSL = True`.
|
||||
|
||||
# PR Highlights
|
||||
|
||||
- [14682](https://github.com/apache/superset/pull/14682) add ascend engine spec (#14682) (@Daniel Wood)
|
||||
- [14420](https://github.com/apache/superset/pull/14420) feat: API endpoint to validate databases using separate parameters (#14420) (@Beto Dealmeida)
|
||||
- [14934](https://github.com/apache/superset/pull/14934) feat: Adding FORCE_SSL as feature flag in config.py (#14934) (@AAfghahi)
|
||||
- [14480](https://github.com/apache/superset/pull/14480) feat(viz): add funnel chart (#14480) (@Ville Brofeldt)
|
||||
|
||||
|
||||
|
||||
## Breaking Changes and Full Changelog
|
||||
|
||||
- To see the complete changelog in this release, head to [CHANGELOG.MD](../../CHANGELOG.md).
|
||||
- 1.3.0 does not contain any backwards incompatible changes.
|
||||
BIN
RELEASING/release-notes-1-3/media/dashboard_native_filters_1.jpg
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
RELEASING/release-notes-1-3/media/export_full_csv.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
RELEASING/release-notes-1-3/media/funnel_chart.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
RELEASING/release-notes-1-3/media/jinja_templating.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
RELEASING/release-notes-1-3/media/native_filters_collapsed.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
RELEASING/release-notes-1-3/media/view_query_dashboard.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
@@ -61,6 +61,7 @@ Join our growing community!
|
||||
- [Tails.com](https://tails.com) [@alanmcruickshank]
|
||||
- [THE ICONIC](http://theiconic.com.au/) [@ksaagariconic]
|
||||
- [Utair](https://www.utair.ru) [@utair-digital]
|
||||
- [VkusVill](https://www.vkusvill.ru) [@ETselikov]
|
||||
- [Zalando](https://www.zalando.com) [@dmigo]
|
||||
- [Zalora](https://www.zalora.com) [@ksaagariconic]
|
||||
|
||||
@@ -96,6 +97,7 @@ Join our growing community!
|
||||
- [Showmax](https://tech.showmax.com) [@bobek]
|
||||
- [source{d}](https://www.sourced.tech) [@marnovo]
|
||||
- [Steamroot](https://streamroot.io/)
|
||||
- [TechAudit](https://www.techaudit.info) [@ETselikov]
|
||||
- [Tenable](https://www.tenable.com) [@dflionis]
|
||||
- [timbr.ai](https://timbr.ai/) [@semantiDan]
|
||||
- [Tobii](http://www.tobii.com/) [@dwa]
|
||||
@@ -104,7 +106,6 @@ Join our growing community!
|
||||
- [Windsor.ai](https://www.windsor.ai/) [@octaviancorlade]
|
||||
- [Zeta](https://www.zeta.tech/) [@shaikidris]
|
||||
|
||||
|
||||
### Entertainment
|
||||
- [6play](https://www.6play.fr) [@CoryChaplin]
|
||||
- [bilibili](https://www.bilibili.com) [@Moinheart]
|
||||
@@ -129,6 +130,7 @@ Join our growing community!
|
||||
|
||||
### Healthcare
|
||||
- [Amino](https://amino.com) [@shkr]
|
||||
- [Care](https://www.getcare.io/)[@alandao2021]
|
||||
- [Living Goods](https://www.livinggoods.org) [@chelule]
|
||||
- [Maieutical Labs](https://maieuticallabs.it) [@xrmx]
|
||||
- [QPID Health](http://www.qpidhealth.com/)
|
||||
|
||||
@@ -22,11 +22,12 @@ under the License.
|
||||
This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## 1.3.1
|
||||
## Next
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [16711](https://github.com/apache/incubator-superset/pull/16711): The `url_param` Jinja function will now by default escape the result. For instance, the value `O'Brien` will now be changed to `O''Brien`. To disable this behavior, call `url_param` with `escape_result` set to `False`: `url_param("my_key", "my default", escape_result=False)`.
|
||||
### Potential Downtime
|
||||
### Deprecations
|
||||
### Other
|
||||
|
||||
## 1.3.0
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
- redis:/data
|
||||
|
||||
db:
|
||||
env_file: docker/.env
|
||||
env_file: docker/.env-non-dev
|
||||
image: postgres:10
|
||||
container_name: superset_db
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1465,7 +1465,7 @@ install from pip: ::
|
||||
|
||||
and run via: ::
|
||||
|
||||
celery flower --app=superset.tasks.celery_app:app
|
||||
celery --app=superset.tasks.celery_app:app flower
|
||||
|
||||
Building from source
|
||||
---------------------
|
||||
|
||||
@@ -26,7 +26,7 @@ import { pmc } from '../resources/data';
|
||||
|
||||
const links = [
|
||||
[
|
||||
'https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ',
|
||||
'https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw',
|
||||
'Slack',
|
||||
'interact with other Superset users and community members',
|
||||
],
|
||||
|
||||
@@ -47,7 +47,7 @@ A list of some of the recommended packages.
|
||||
|[IBM Netezza Performance Server](/docs/databases/netezza)|```pip install nzalchemy```|```netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>```|
|
||||
|[MySQL](/docs/databases/mysql)|```pip install mysqlclient```|```mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>```|
|
||||
|[Oracle](/docs/databases/oracle)|```pip install cx_Oracle```|```oracle://```|
|
||||
|[PostgreSQL](/docs/databases/postgresql)|```pip install psycopg2```|```postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>```|
|
||||
|[PostgreSQL](/docs/databases/postgres)|```pip install psycopg2```|```postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>```|
|
||||
|[Trino](/docs/databases/trino)|```pip install sqlalchemy-trino```|```trino://{username}:{password}@{hostname}:{port}/{catalog}```|
|
||||
|[Presto](/docs/databases/presto)|```pip install pyhive```|```presto://```|
|
||||
|[SAP Hana](/docs/databases/hana)|```pip install hdbcli sqlalchemy-hana or pip install apache-superset[hana]```|```hana://{username}:{password}@{host}:{port}```|
|
||||
@@ -69,5 +69,5 @@ exists, please file an issue on the
|
||||
supporting it.
|
||||
|
||||
[StackOverflow](https://stackoverflow.com/questions/tagged/apache-superset+superset) and the
|
||||
[Superset community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
|
||||
[Superset community Slack](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
are great places to get help with connecting to databases in Superset.
|
||||
|
||||
@@ -8,8 +8,8 @@ version: 1
|
||||
|
||||
## Postgres
|
||||
|
||||
Note that the Postgres connector library [psycopg2](https://www.psycopg.org/docs/) comes out of the
|
||||
box with Superset.
|
||||
Note that, if you're using docker-compose, the Postgres connector library [psycopg2](https://www.psycopg.org/docs/)
|
||||
comes out of the box with Superset.
|
||||
|
||||
Postgres sample connection parameters:
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ Navigate to **Data ‣ Datasets** and select the **+ Dataset** button in the top
|
||||
|
||||
A modal window should pop up in front of you. Select your **Database**,
|
||||
**Schema**, and **Table** using the drop downs that appear. In the following example,
|
||||
we register the **Vehicle Sales** table from the **examples** database.
|
||||
we register the **cleaned_sales_data** table from the **examples** database.
|
||||
|
||||
<img src="/images/tutorial_09_add_new_table.png" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ The core contributors (or committers) to Superset communicate primarily in the f
|
||||
which you can join):
|
||||
|
||||
- [Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org)
|
||||
- [Apache Superset Slack community](https://join.slack.com/t/apache-superset/shared_invite/zt-l5f5e0av-fyYu8tlfdqbMdz_sPLwUqQ)
|
||||
- [Apache Superset Slack community](https://join.slack.com/t/apache-superset/shared_invite/zt-uxbh5g36-AISUtHbzOXcu0BIj7kgUaw)
|
||||
- [Github issues and PR's](https://github.com/apache/superset/issues)
|
||||
|
||||
If you're interested in contributing, we recommend reading the Community Contribution Guide
|
||||
|
||||
@@ -114,5 +114,5 @@ pip install flower
|
||||
You can run flower using:
|
||||
|
||||
```
|
||||
celery flower --app=superset.tasks.celery_app:app
|
||||
celery --app=superset.tasks.celery_app:app flower
|
||||
```
|
||||
|
||||
@@ -201,6 +201,29 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### Flask app Configuration Hook
|
||||
|
||||
`FLASK_APP_MUTATOR` is a configuration function that can be provided in your environment, receives
|
||||
the app object and can alter it in any way. For example, add `FLASK_APP_MUTATOR` into your
|
||||
`superset_config.py` to setup session cookie expiration time to 24 hours:
|
||||
|
||||
```python
|
||||
from flask import session
|
||||
from flask import Flask
|
||||
|
||||
|
||||
def make_session_permanent():
|
||||
'''
|
||||
Enable maxAge for the cookie 'session'
|
||||
'''
|
||||
session.permanent = True
|
||||
|
||||
# Set up max age of session to 24 hours
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)
|
||||
def FLASK_APP_MUTATOR(app: Flask) -> None:
|
||||
app.before_request_funcs.setdefault(None, []).append(make_session_permanent)
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
To support a diverse set of users, Superset has some features that are not enabled by default. For
|
||||
@@ -218,7 +241,7 @@ FEATURE_FLAGS = {
|
||||
}
|
||||
```
|
||||
|
||||
A current list of feature flags can be found in `RESOURCES/FEATURE_FLAGS.md`
|
||||
A current list of feature flags can be found in [RESOURCES/FEATURE_FLAGS.md](https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md).
|
||||
|
||||
### SIP 15
|
||||
|
||||
|
||||
@@ -36,6 +36,18 @@ Install the following packages using the `yum` package manager:
|
||||
sudo yum install gcc gcc-c++ libffi-devel python-devel python-pip python-wheel openssl-devel cyrus-sasl-devel openldap-devel
|
||||
```
|
||||
|
||||
In more recent versions of CentOS and Fedora, you may need to install a slightly different set of packages using `dnf`:
|
||||
|
||||
```
|
||||
sudo dnf install gcc gcc-c++ libffi-devel python3-devel python3-pip python3-wheel openssl-devel cyrus-sasl-devel openldap-devel
|
||||
```
|
||||
|
||||
Also, on CentOS, you may need to upgrade pip for the install to work:
|
||||
|
||||
```
|
||||
pip3 install --upgrade pip
|
||||
```
|
||||
|
||||
**Mac OS X**
|
||||
|
||||
If you're not on the latest version of OS X, we recommend upgrading because we've found that many
|
||||
|
||||
@@ -7,7 +7,7 @@ route: /docs/security
|
||||
### Roles
|
||||
|
||||
Security in Superset is handled by Flask AppBuilder (FAB), an application development framework
|
||||
built on top of Flask.”. FAB provides authentication, user management, permissions and roles.
|
||||
built on top of Flask. FAB provides authentication, user management, permissions and roles.
|
||||
Please read its [Security documentation](https://flask-appbuilder.readthedocs.io/en/latest/security.html).
|
||||
|
||||
### Provided Roles
|
||||
@@ -15,7 +15,7 @@ Please read its [Security documentation](https://flask-appbuilder.readthedocs.io
|
||||
Superset ships with a set of roles that are handled by Superset itself. You can assume
|
||||
that these roles will stay up-to-date as Superset evolves (and as you update Superset versions).
|
||||
|
||||
Even though **Admin** users have the ability, we don't recommend that altering the
|
||||
Even though **Admin** users have the ability, we don't recommend altering the
|
||||
permissions associated with each role (e.g. by removing or adding permissions to them). The permissions
|
||||
associated with each role will be re-synchronized to their original values when you run
|
||||
the **superset init** command (often done between Superset versions).
|
||||
|
||||
@@ -22,7 +22,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.3.5
|
||||
version: 0.3.6
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 10.2.0
|
||||
|
||||
@@ -86,6 +86,9 @@ spec:
|
||||
- name: superset-config
|
||||
mountPath: {{ .Values.configMountPath | quote }}
|
||||
readOnly: true
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- tpl (toYaml .) $ | nindent 12 -}}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
@@ -108,4 +111,7 @@ spec:
|
||||
- name: superset-config
|
||||
secret:
|
||||
secretName: {{ tpl .Values.configFromSecret . }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- tpl (toYaml .) $ | nindent 8 -}}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -87,6 +87,9 @@ spec:
|
||||
- name: superset-config
|
||||
mountPath: {{ .Values.configMountPath | quote }}
|
||||
readOnly: true
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- tpl (toYaml .) $ | nindent 12 -}}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
@@ -109,3 +112,6 @@ spec:
|
||||
- name: superset-config
|
||||
secret:
|
||||
secretName: {{ tpl .Values.configFromSecret . }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- tpl (toYaml .) $ | nindent 8 -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -95,6 +95,9 @@ spec:
|
||||
mountPath: {{ .Values.extraConfigMountPath | quote }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- tpl (toYaml .) $ | nindent 12 -}}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
@@ -127,3 +130,6 @@ spec:
|
||||
configMap:
|
||||
name: {{ template "superset.fullname" . }}-extra-config
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- tpl (toYaml .) $ | nindent 8 -}}
|
||||
{{- end }}
|
||||
|
||||
@@ -60,6 +60,9 @@ spec:
|
||||
mountPath: {{ .Values.extraConfigMountPath | quote }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumeMounts }}
|
||||
{{- tpl (toYaml .) $ | nindent 10 -}}
|
||||
{{- end }}
|
||||
command: {{ tpl (toJson .Values.init.command) . }}
|
||||
resources:
|
||||
{{ toYaml .Values.init.resources | indent 10 }}
|
||||
@@ -76,5 +79,8 @@ spec:
|
||||
configMap:
|
||||
name: {{ template "superset.fullname" . }}-extra-config
|
||||
{{- end }}
|
||||
{{- with .Values.extraVolumes }}
|
||||
{{- tpl (toYaml .) $ | nindent 8 -}}
|
||||
{{- end }}
|
||||
restartPolicy: Never
|
||||
{{- end }}
|
||||
|
||||
@@ -82,6 +82,22 @@ extraConfigs: {}
|
||||
|
||||
extraSecrets: {}
|
||||
|
||||
extraVolumes: []
|
||||
# - name: customConfig
|
||||
# configMap:
|
||||
# name: '{{ template "superset.fullname" . }}-custom-config'
|
||||
# - name: additionalSecret
|
||||
# secret:
|
||||
# secretName: my-secret
|
||||
# defaultMode: 0600
|
||||
|
||||
extraVolumeMounts: []
|
||||
# - name: customConfig
|
||||
# mountPath: /mnt/config
|
||||
# readOnly: true
|
||||
# - name: additionalSecret:
|
||||
# mountPath: /mnt/secret
|
||||
|
||||
# A dictionary of overrides to append at the end of superset_config.py - the name does not matter
|
||||
# WARNING: the order is not guaranteed
|
||||
configOverrides: {}
|
||||
|
||||
@@ -18,3 +18,4 @@
|
||||
-e file:.
|
||||
pyrsistent>=0.16.1,<0.17
|
||||
zipp==3.4.1
|
||||
sasl==0.2.1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SHA1:0862095245a068ae2fc00217da78331e1e7ae505
|
||||
# SHA1:57a754a4cf09b58d8e02c45bfb1058d2ce4286a6
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
@@ -7,9 +7,9 @@
|
||||
#
|
||||
-e file:.
|
||||
# via -r requirements/base.in
|
||||
aiohttp==3.7.2
|
||||
aiohttp==3.7.4.post0
|
||||
# via slackclient
|
||||
alembic==1.4.3
|
||||
alembic==1.6.5
|
||||
# via flask-migrate
|
||||
amqp==2.6.1
|
||||
# via kombu
|
||||
@@ -17,17 +17,17 @@ apispec[yaml]==3.3.2
|
||||
# via flask-appbuilder
|
||||
async-timeout==3.0.1
|
||||
# via aiohttp
|
||||
attrs==20.2.0
|
||||
attrs==21.2.0
|
||||
# via
|
||||
# aiohttp
|
||||
# jsonschema
|
||||
babel==2.8.0
|
||||
babel==2.9.1
|
||||
# via flask-babel
|
||||
backoff==1.10.0
|
||||
backoff==1.11.1
|
||||
# via apache-superset
|
||||
billiard==3.6.3.0
|
||||
billiard==3.6.4.0
|
||||
# via celery
|
||||
bleach==3.3.0
|
||||
bleach==3.3.1
|
||||
# via apache-superset
|
||||
brotli==1.0.9
|
||||
# via flask-compress
|
||||
@@ -35,9 +35,9 @@ cachelib==0.1.1
|
||||
# via apache-superset
|
||||
celery==4.4.7
|
||||
# via apache-superset
|
||||
cffi==1.14.3
|
||||
cffi==1.14.6
|
||||
# via cryptography
|
||||
chardet==3.0.4
|
||||
chardet==4.0.0
|
||||
# via aiohttp
|
||||
click==7.1.2
|
||||
# via
|
||||
@@ -48,23 +48,23 @@ colorama==0.4.4
|
||||
# via
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
convertdate==2.3.0
|
||||
convertdate==2.3.2
|
||||
# via holidays
|
||||
cron-descriptor==1.2.24
|
||||
# via apache-superset
|
||||
croniter==0.3.36
|
||||
croniter==1.0.15
|
||||
# via apache-superset
|
||||
cryptography==3.3.2
|
||||
cryptography==3.4.7
|
||||
# via apache-superset
|
||||
defusedxml==0.6.0
|
||||
defusedxml==0.7.1
|
||||
# via python3-openid
|
||||
deprecation==2.1.0
|
||||
# via apache-superset
|
||||
dnspython==2.0.0
|
||||
dnspython==2.1.0
|
||||
# via email-validator
|
||||
email-validator==1.1.1
|
||||
email-validator==1.1.3
|
||||
# via flask-appbuilder
|
||||
flask==1.1.2
|
||||
flask==1.1.4
|
||||
# via
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
@@ -77,35 +77,35 @@ flask==1.1.2
|
||||
# flask-openid
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==3.3.0
|
||||
flask-appbuilder==3.3.2
|
||||
# via apache-superset
|
||||
flask-babel==1.0.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==1.10.1
|
||||
# via apache-superset
|
||||
flask-compress==1.8.0
|
||||
flask-compress==1.10.1
|
||||
# via apache-superset
|
||||
flask-jwt-extended==3.24.1
|
||||
flask-jwt-extended==3.25.1
|
||||
# via flask-appbuilder
|
||||
flask-login==0.4.1
|
||||
# via flask-appbuilder
|
||||
flask-migrate==2.5.3
|
||||
flask-migrate==3.1.0
|
||||
# via apache-superset
|
||||
flask-openid==1.3.0
|
||||
flask-openid==1.2.5
|
||||
# via flask-appbuilder
|
||||
flask-sqlalchemy==2.4.4
|
||||
flask-sqlalchemy==2.5.1
|
||||
# via
|
||||
# flask-appbuilder
|
||||
# flask-migrate
|
||||
flask-talisman==0.7.0
|
||||
flask-talisman==0.8.1
|
||||
# via apache-superset
|
||||
flask-wtf==0.14.3
|
||||
# via
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
geographiclib==1.50
|
||||
geographiclib==1.52
|
||||
# via geopy
|
||||
geopy==2.0.0
|
||||
geopy==2.2.0
|
||||
# via apache-superset
|
||||
graphlib-backport==1.0.3
|
||||
# via apache-superset
|
||||
@@ -113,9 +113,9 @@ gunicorn==20.0.4
|
||||
# via apache-superset
|
||||
holidays==0.10.3
|
||||
# via apache-superset
|
||||
humanize==3.1.0
|
||||
humanize==3.11.0
|
||||
# via apache-superset
|
||||
idna==2.10
|
||||
idna==3.2
|
||||
# via
|
||||
# email-validator
|
||||
# yarl
|
||||
@@ -136,16 +136,16 @@ kombu==4.6.11
|
||||
# via celery
|
||||
korean-lunar-calendar==0.2.1
|
||||
# via holidays
|
||||
mako==1.1.3
|
||||
mako==1.1.4
|
||||
# via alembic
|
||||
markdown==3.3.3
|
||||
markdown==3.3.4
|
||||
# via apache-superset
|
||||
markupsafe==1.1.1
|
||||
markupsafe==2.0.1
|
||||
# via
|
||||
# jinja2
|
||||
# mako
|
||||
# wtforms
|
||||
marshmallow==3.9.0
|
||||
marshmallow==3.13.0
|
||||
# via
|
||||
# flask-appbuilder
|
||||
# marshmallow-enum
|
||||
@@ -154,23 +154,21 @@ marshmallow-enum==1.5.1
|
||||
# via flask-appbuilder
|
||||
marshmallow-sqlalchemy==0.23.1
|
||||
# via flask-appbuilder
|
||||
msgpack==1.0.0
|
||||
msgpack==1.0.2
|
||||
# via apache-superset
|
||||
multidict==5.0.0
|
||||
multidict==5.1.0
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
natsort==7.0.1
|
||||
# via croniter
|
||||
numpy==1.19.4
|
||||
numpy==1.21.1
|
||||
# via
|
||||
# pandas
|
||||
# pyarrow
|
||||
packaging==20.4
|
||||
packaging==21.0
|
||||
# via
|
||||
# bleach
|
||||
# deprecation
|
||||
pandas==1.2.2
|
||||
pandas==1.2.5
|
||||
# via apache-superset
|
||||
parsedatetime==2.6
|
||||
# via apache-superset
|
||||
@@ -189,7 +187,7 @@ pyjwt==1.7.1
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
pymeeus==0.3.7
|
||||
pymeeus==0.5.11
|
||||
# via convertdate
|
||||
pyparsing==2.4.7
|
||||
# via
|
||||
@@ -199,7 +197,7 @@ pyrsistent==0.16.1
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# jsonschema
|
||||
python-dateutil==2.8.1
|
||||
python-dateutil==2.8.2
|
||||
# via
|
||||
# alembic
|
||||
# apache-superset
|
||||
@@ -207,7 +205,7 @@ python-dateutil==2.8.1
|
||||
# flask-appbuilder
|
||||
# holidays
|
||||
# pandas
|
||||
python-dotenv==0.15.0
|
||||
python-dotenv==0.19.0
|
||||
# via apache-superset
|
||||
python-editor==1.0.4
|
||||
# via alembic
|
||||
@@ -215,7 +213,7 @@ python-geohash==0.8.5
|
||||
# via apache-superset
|
||||
python3-openid==3.2.0
|
||||
# via flask-openid
|
||||
pytz==2020.4
|
||||
pytz==2021.1
|
||||
# via
|
||||
# babel
|
||||
# celery
|
||||
@@ -228,29 +226,30 @@ pyyaml==5.4.1
|
||||
# apispec
|
||||
redis==3.5.3
|
||||
# via apache-superset
|
||||
sasl==0.2.1
|
||||
# via -r requirements/base.in
|
||||
selenium==3.141.0
|
||||
# via apache-superset
|
||||
simplejson==3.17.2
|
||||
simplejson==3.17.3
|
||||
# via apache-superset
|
||||
six==1.15.0
|
||||
six==1.16.0
|
||||
# via
|
||||
# bleach
|
||||
# cryptography
|
||||
# flask-jwt-extended
|
||||
# flask-talisman
|
||||
# holidays
|
||||
# isodate
|
||||
# jsonschema
|
||||
# packaging
|
||||
# polyline
|
||||
# prison
|
||||
# pyrsistent
|
||||
# python-dateutil
|
||||
# sasl
|
||||
# sqlalchemy-utils
|
||||
# wtforms-json
|
||||
slackclient==2.5.0
|
||||
# via apache-superset
|
||||
sqlalchemy==1.3.20
|
||||
sqlalchemy==1.3.24
|
||||
# via
|
||||
# alembic
|
||||
# apache-superset
|
||||
@@ -266,11 +265,11 @@ sqlparse==0.3.0
|
||||
# via apache-superset
|
||||
tabulate==0.8.9
|
||||
# via apache-superset
|
||||
typing-extensions==3.7.4.3
|
||||
typing-extensions==3.10.0.0
|
||||
# via
|
||||
# aiohttp
|
||||
# apache-superset
|
||||
urllib3==1.25.11
|
||||
urllib3==1.26.6
|
||||
# via selenium
|
||||
vine==1.3.0
|
||||
# via
|
||||
@@ -288,7 +287,7 @@ wtforms==2.3.3
|
||||
# wtforms-json
|
||||
wtforms-json==0.3.3
|
||||
# via apache-superset
|
||||
yarl==1.6.2
|
||||
yarl==1.6.3
|
||||
# via aiohttp
|
||||
zipp==3.4.1
|
||||
# via -r requirements/base.in
|
||||
|
||||
@@ -24,5 +24,5 @@ pyhive[hive]>=0.6.1
|
||||
psycopg2-binary==2.8.5
|
||||
tableschema
|
||||
thrift>=0.11.0,<1.0.0
|
||||
pygithub>=1.54.1,<2.0.0
|
||||
progress>=1.5,<2
|
||||
pyinstrument>=4.0.2,<5
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SHA1:c470411e2e9cb04b412a94f80a6a9d870bece74d
|
||||
# SHA1:e4f3ea65026a8aec3735d6d9977f89fef4a1a4f9
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
@@ -8,84 +8,77 @@
|
||||
-r base.txt
|
||||
-e file:.
|
||||
# via -r requirements/base.in
|
||||
boto3==1.16.10
|
||||
boto3==1.18.19
|
||||
# via tabulator
|
||||
botocore==1.19.10
|
||||
botocore==1.21.19
|
||||
# via
|
||||
# boto3
|
||||
# s3transfer
|
||||
cached-property==1.5.2
|
||||
# via tableschema
|
||||
certifi==2020.6.20
|
||||
certifi==2021.5.30
|
||||
# via requests
|
||||
deprecated==1.2.11
|
||||
# via pygithub
|
||||
et-xmlfile==1.0.1
|
||||
charset-normalizer==2.0.4
|
||||
# via requests
|
||||
et-xmlfile==1.1.0
|
||||
# via openpyxl
|
||||
flask-cors==3.0.9
|
||||
flask-cors==3.0.10
|
||||
# via -r requirements/development.in
|
||||
future==0.18.2
|
||||
# via pyhive
|
||||
ijson==3.1.2.post0
|
||||
ijson==3.1.4
|
||||
# via tabulator
|
||||
jdcal==1.4.1
|
||||
# via openpyxl
|
||||
jmespath==0.10.0
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
jsonlines==1.2.0
|
||||
jsonlines==2.0.0
|
||||
# via tabulator
|
||||
linear-tsv==1.1.0
|
||||
# via tabulator
|
||||
mysqlclient==1.4.2.post1
|
||||
# via -r requirements/development.in
|
||||
openpyxl==3.0.5
|
||||
openpyxl==3.0.7
|
||||
# via tabulator
|
||||
pillow==7.2.0
|
||||
# via -r requirements/development.in
|
||||
progress==1.5
|
||||
progress==1.6
|
||||
# via -r requirements/development.in
|
||||
psycopg2-binary==2.8.5
|
||||
# via -r requirements/development.in
|
||||
pydruid==0.6.1
|
||||
pure-sasl==0.6.2
|
||||
# via thrift-sasl
|
||||
pydruid==0.6.2
|
||||
# via -r requirements/development.in
|
||||
pygithub==1.54.1
|
||||
pyhive[hive]==0.6.4
|
||||
# via -r requirements/development.in
|
||||
pyhive[hive]==0.6.3
|
||||
pyinstrument==4.0.2
|
||||
# via -r requirements/development.in
|
||||
requests==2.24.0
|
||||
requests==2.26.0
|
||||
# via
|
||||
# pydruid
|
||||
# pygithub
|
||||
# tableschema
|
||||
# tabulator
|
||||
rfc3986==1.4.0
|
||||
rfc3986==1.5.0
|
||||
# via tableschema
|
||||
s3transfer==0.3.3
|
||||
s3transfer==0.5.0
|
||||
# via boto3
|
||||
sasl==0.2.1
|
||||
# via
|
||||
# pyhive
|
||||
# thrift-sasl
|
||||
tableschema==1.20.0
|
||||
tableschema==1.20.2
|
||||
# via -r requirements/development.in
|
||||
tabulator==1.52.5
|
||||
tabulator==1.53.5
|
||||
# via tableschema
|
||||
thrift==0.13.0
|
||||
# via
|
||||
# -r requirements/development.in
|
||||
# pyhive
|
||||
# thrift-sasl
|
||||
thrift-sasl==0.4.2
|
||||
thrift-sasl==0.4.3
|
||||
# via pyhive
|
||||
unicodecsv==0.14.1
|
||||
# via
|
||||
# tableschema
|
||||
# tabulator
|
||||
wrapt==1.12.1
|
||||
# via deprecated
|
||||
xlrd==1.2.0
|
||||
xlrd==2.0.1
|
||||
# via tabulator
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
-r base.txt
|
||||
-e file:.
|
||||
# via -r requirements/base.in
|
||||
gevent==20.9.0
|
||||
gevent==21.8.0
|
||||
# via -r requirements/docker.in
|
||||
greenlet==0.4.17
|
||||
greenlet==1.1.1
|
||||
# via gevent
|
||||
psycopg2-binary==2.8.6
|
||||
psycopg2-binary==2.9.1
|
||||
# via -r requirements/docker.in
|
||||
zope.event==4.5.0
|
||||
# via gevent
|
||||
zope.interface==5.1.2
|
||||
zope.interface==5.4.0
|
||||
# via gevent
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
|
||||
@@ -17,3 +17,6 @@
|
||||
pip-compile-multi!=1.5.9
|
||||
pre-commit
|
||||
tox
|
||||
py>=1.10.0
|
||||
click==7.1.2
|
||||
packaging==21.0
|
||||
|
||||
@@ -1,62 +1,74 @@
|
||||
# SHA1:f95c1152ed0bcc554f3668440d63eec2a7d1567c
|
||||
# SHA1:17ab2346746deadfc557e1df96014e77c8337f4b
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile-multi
|
||||
#
|
||||
appdirs==1.4.4
|
||||
backports.entry-points-selectable==1.1.0
|
||||
# via virtualenv
|
||||
cfgv==3.2.0
|
||||
cfgv==3.3.0
|
||||
# via pre-commit
|
||||
click==7.1.2
|
||||
# via
|
||||
# -r requirements/integration.in
|
||||
# pip-compile-multi
|
||||
# pip-tools
|
||||
distlib==0.3.1
|
||||
distlib==0.3.2
|
||||
# via virtualenv
|
||||
filelock==3.0.12
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
identify==1.5.9
|
||||
identify==2.2.13
|
||||
# via pre-commit
|
||||
nodeenv==1.5.0
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
packaging==20.4
|
||||
# via tox
|
||||
packaging==21.0
|
||||
# via
|
||||
# -r requirements/integration.in
|
||||
# tox
|
||||
pep517==0.11.0
|
||||
# via pip-tools
|
||||
pip-compile-multi==2.4.1
|
||||
# via -r requirements/integration.in
|
||||
pip-tools==5.3.1
|
||||
pip-tools==6.2.0
|
||||
# via pip-compile-multi
|
||||
platformdirs==2.2.0
|
||||
# via virtualenv
|
||||
pluggy==0.13.1
|
||||
# via tox
|
||||
pre-commit==2.8.2
|
||||
pre-commit==2.14.0
|
||||
# via -r requirements/integration.in
|
||||
py==1.9.0
|
||||
# via tox
|
||||
py==1.10.0
|
||||
# via
|
||||
# -r requirements/integration.in
|
||||
# tox
|
||||
pyparsing==2.4.7
|
||||
# via packaging
|
||||
pyyaml==5.4.1
|
||||
# via pre-commit
|
||||
six==1.15.0
|
||||
six==1.16.0
|
||||
# via
|
||||
# packaging
|
||||
# pip-tools
|
||||
# tox
|
||||
# virtualenv
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
toposort==1.5
|
||||
tomli==1.2.1
|
||||
# via pep517
|
||||
toposort==1.6
|
||||
# via pip-compile-multi
|
||||
tox==3.20.1
|
||||
tox==3.24.1
|
||||
# via -r requirements/integration.in
|
||||
virtualenv==20.1.0
|
||||
virtualenv==20.7.2
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.37.0
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
|
||||
@@ -22,14 +22,15 @@ freezegun
|
||||
ipdb
|
||||
# pinning ipython as pip-compile-multi was bringing higher version
|
||||
# of the ipython that was not found in CI
|
||||
ipython==7.16.1
|
||||
ipython
|
||||
openapi-spec-validator
|
||||
openpyxl
|
||||
parameterized
|
||||
pyfakefs
|
||||
pyhive[presto]>=0.6.3
|
||||
pylint
|
||||
pylint==2.10.2
|
||||
pytest
|
||||
pytest-cov
|
||||
statsd
|
||||
pytest-mock
|
||||
packaging==21.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SHA1:d39180c0eb498d1a7dd73b8428e6ab304b728484
|
||||
# SHA1:59e47200215ca4695f09e03a773e1a6f310f78da
|
||||
#
|
||||
# This file is autogenerated by pip-compile-multi
|
||||
# To update, run:
|
||||
@@ -11,79 +11,89 @@
|
||||
# via -r requirements/base.in
|
||||
appnope==0.1.2
|
||||
# via ipython
|
||||
astroid==2.4.2
|
||||
astroid==2.7.2
|
||||
# via pylint
|
||||
backcall==0.2.0
|
||||
# via ipython
|
||||
coverage==5.3
|
||||
coverage==5.5
|
||||
# via pytest-cov
|
||||
decorator==5.0.9
|
||||
# via ipython
|
||||
docker==4.3.1
|
||||
# via
|
||||
# ipdb
|
||||
# ipython
|
||||
docker==5.0.0
|
||||
# via -r requirements/testing.in
|
||||
flask-testing==0.8.0
|
||||
flask-testing==0.8.1
|
||||
# via -r requirements/testing.in
|
||||
freezegun==1.0.0
|
||||
freezegun==1.1.0
|
||||
# via -r requirements/testing.in
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
ipdb==0.13.4
|
||||
ipdb==0.13.9
|
||||
# via -r requirements/testing.in
|
||||
ipython==7.16.1
|
||||
ipython==7.26.0
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# ipdb
|
||||
ipython-genutils==0.2.0
|
||||
# via traitlets
|
||||
isort==5.6.4
|
||||
isort==5.9.3
|
||||
# via pylint
|
||||
jedi==0.17.2
|
||||
jedi==0.18.0
|
||||
# via ipython
|
||||
lazy-object-proxy==1.4.3
|
||||
lazy-object-proxy==1.6.0
|
||||
# via astroid
|
||||
matplotlib-inline==0.1.2
|
||||
# via ipython
|
||||
mccabe==0.6.1
|
||||
# via pylint
|
||||
openapi-spec-validator==0.2.9
|
||||
openapi-schema-validator==0.1.5
|
||||
# via openapi-spec-validator
|
||||
openapi-spec-validator==0.3.1
|
||||
# via -r requirements/testing.in
|
||||
parameterized==0.7.4
|
||||
parameterized==0.8.1
|
||||
# via -r requirements/testing.in
|
||||
parso==0.7.1
|
||||
parso==0.8.2
|
||||
# via jedi
|
||||
pexpect==4.8.0
|
||||
# via ipython
|
||||
pickleshare==0.7.5
|
||||
# via ipython
|
||||
prompt-toolkit==3.0.8
|
||||
prompt-toolkit==3.0.19
|
||||
# via ipython
|
||||
ptyprocess==0.6.0
|
||||
ptyprocess==0.7.0
|
||||
# via pexpect
|
||||
pyfakefs==4.4.0
|
||||
pyfakefs==4.5.0
|
||||
# via -r requirements/testing.in
|
||||
pygments==2.7.2
|
||||
pygments==2.9.0
|
||||
# via ipython
|
||||
pyhive[hive,presto]==0.6.3
|
||||
pyhive[hive,presto]==0.6.4
|
||||
# via
|
||||
# -r requirements/development.in
|
||||
# -r requirements/testing.in
|
||||
pylint==2.6.0
|
||||
pylint==2.10.2
|
||||
# via -r requirements/testing.in
|
||||
pytest==6.1.2
|
||||
pytest==6.2.4
|
||||
# via
|
||||
# -r requirements/testing.in
|
||||
# pytest-cov
|
||||
# pytest-mock
|
||||
pytest-cov==2.10.1
|
||||
pytest-cov==2.12.1
|
||||
# via -r requirements/testing.in
|
||||
pytest-mock==3.6.1
|
||||
# via -r requirements/testing.in
|
||||
statsd==3.3.0
|
||||
# via -r requirements/testing.in
|
||||
traitlets==5.0.5
|
||||
# via ipython
|
||||
# via
|
||||
# ipython
|
||||
# matplotlib-inline
|
||||
wcwidth==0.2.5
|
||||
# via prompt-toolkit
|
||||
websocket-client==0.57.0
|
||||
websocket-client==1.2.0
|
||||
# via docker
|
||||
wrapt==1.12.1
|
||||
# via astroid
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
||||
@@ -44,9 +44,13 @@ def import_migration_script(filepath: Path) -> ModuleType:
|
||||
Import migration script as if it were a module.
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location(filepath.stem, filepath)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
return module
|
||||
if spec:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
return module
|
||||
raise Exception(
|
||||
"No module spec found in location: `{path}`".format(path=str(filepath))
|
||||
)
|
||||
|
||||
|
||||
def extract_modified_tables(module: ModuleType) -> Set[str]:
|
||||
|
||||
@@ -34,7 +34,7 @@ REGEXES=()
|
||||
for CHECK in "$@"
|
||||
do
|
||||
if [[ ${CHECK} == "python" ]]; then
|
||||
REGEX="(^\.github\/workflows\/.*python|^tests\/|^superset\/|^setup\.py|^requirements\/.+\.txt)"
|
||||
REGEX="(^\.github\/workflows\/.*python|^tests\/|^superset\/|^setup\.py|^requirements\/.+\.txt|^\.pylintrc)"
|
||||
echo "Searching for changes in python files"
|
||||
elif [[ ${CHECK} == "frontend" ]]; then
|
||||
REGEX="(^\.github\/workflows\/.*(frontend|e2e)|^superset-frontend\/)"
|
||||
|
||||
@@ -30,7 +30,7 @@ combine_as_imports = true
|
||||
include_trailing_comma = true
|
||||
line_length = 88
|
||||
known_first_party = superset
|
||||
known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,cron_descriptor,croniter,cryptography,dateutil,deprecation,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_jwt_extended,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,graphlib,holidays,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,marshmallow_enum,msgpack,numpy,pandas,parameterized,parsedatetime,pgsanity,pkg_resources,polyline,prison,progress,pyarrow,pyhive,pyparsing,pytest,pytest_mock,pytz,redis,requests,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,tabulate,typing_extensions,werkzeug,wtforms,wtforms_json,yaml
|
||||
known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,cron_descriptor,croniter,cryptography,dateutil,deprecation,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_jwt_extended,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,graphlib,holidays,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,marshmallow_enum,msgpack,numpy,pandas,parameterized,parsedatetime,pgsanity,pkg_resources,polyline,prison,progress,pyarrow,pyhive,pyparsing,pytest,pytest_mock,pytz,redis,requests,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,typing_extensions,urllib3,werkzeug,wtforms,wtforms_json,yaml
|
||||
multi_line_output = 3
|
||||
order_by_type = false
|
||||
|
||||
|
||||
7
setup.py
@@ -106,14 +106,15 @@ setup(
|
||||
"simplejson>=3.15.0",
|
||||
"slackclient==2.5.0", # PINNED! slack changes file upload api in the future versions
|
||||
"sqlalchemy>=1.3.16, <1.4, !=1.3.21",
|
||||
"sqlalchemy-utils>=0.36.6,<0.37",
|
||||
"sqlalchemy-utils>=0.36.6, <0.37",
|
||||
"sqlparse==0.3.0", # PINNED! see https://github.com/andialbrecht/sqlparse/issues/562
|
||||
"tabulate==0.8.9",
|
||||
"typing-extensions>=3.7.4.3,<4", # needed to support typing.Literal on py37
|
||||
"typing-extensions>=3.10, <4", # needed to support Literal (3.8) and TypeGuard (3.10)
|
||||
"wtforms-json",
|
||||
],
|
||||
extras_require={
|
||||
"athena": ["pyathena>=1.10.8,<1.11"],
|
||||
"athena": ["pyathena>=1.10.8, <1.11"],
|
||||
"aurora-data-api": ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"],
|
||||
"bigquery": [
|
||||
"pandas_gbq>=0.10.0",
|
||||
"pybigquery>=0.4.10",
|
||||
|
||||
@@ -82,5 +82,8 @@ module.exports = {
|
||||
],
|
||||
],
|
||||
},
|
||||
testableProduction: {
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,6 +17,6 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.335 8.735L7.30998 1.735C7.04507 1.26004 6.54382 0.965683 5.99998 0.965683C5.45614 0.965683 4.9549 1.26004 4.68998 1.735L0.689984 8.735C0.416009 9.19706 0.410037 9.77036 0.674327 10.238C0.938617 10.7057 1.43282 10.9963 1.96998 11H10.03C10.5716 11.0053 11.0741 10.7182 11.3445 10.2489C11.6149 9.77957 11.6113 9.20091 11.335 8.735Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 4.5C5.5 4.22386 5.72386 4 6 4C6.27614 4 6.5 4.22386 6.5 4.5V6.5C6.5 6.77614 6.27614 7 6 7C5.72386 7 5.5 6.77614 5.5 6.5V4.5ZM5.5 8.5C5.5 8.22386 5.72386 8 6 8C6.27614 8 6.5 8.22386 6.5 8.5C6.5 8.77614 6.27614 9 6 9C5.72386 9 5.5 8.77614 5.5 8.5Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.345929226875306,14.745929226875305 L13.320909226875305,7.7459292268753055 C13.055999226875306,7.270969226875306 12.554749226875305,6.9766122268753055 12.010909226875306,6.9766122268753055 C11.467069226875305,6.9766122268753055 10.965829226875307,7.270969226875306 10.700909226875304,7.7459292268753055 L6.700913226875306,14.745929226875305 C6.426938226875307,15.207989226875306 6.420966226875306,15.781289226875305 6.685256226875307,16.248929226875305 C6.949546226875305,16.716629226875305 7.443749226875305,17.007229226875303 7.980909226875305,17.010929226875305 H16.040929226875306 C16.582529226875305,17.016229226875307 17.085029226875307,16.729129226875305 17.355429226875305,16.259829226875304 C17.625829226875304,15.790499226875305 17.622229226875305,15.211839226875306 17.345929226875306,14.745929226875305 z" fill="currentColor" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.510929226875305,10.510929226875305 C11.510929226875305,10.234789226875305 11.734789226875307,10.010929226875307 12.010929226875305,10.010929226875307 C12.287069226875303,10.010929226875307 12.510929226875305,10.234789226875305 12.510929226875305,10.510929226875305 V12.510929226875305 C12.510929226875305,12.787069226875305 12.287069226875303,13.010929226875307 12.010929226875305,13.010929226875307 C11.734789226875307,13.010929226875307 11.510929226875305,12.787069226875305 11.510929226875305,12.510929226875305 V10.510929226875305 zM11.510929226875305,14.510929226875305 C11.510929226875305,14.234789226875305 11.734789226875307,14.010929226875307 12.010929226875305,14.010929226875307 C12.287069226875303,14.010929226875307 12.510929226875305,14.234789226875305 12.510929226875305,14.510929226875305 C12.510929226875305,14.787069226875305 12.287069226875303,15.010929226875307 12.010929226875305,15.010929226875307 C11.734789226875307,15.010929226875307 11.510929226875305,14.787069226875305 11.510929226875305,14.510929226875305 z" fill="white" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -19,8 +19,8 @@
|
||||
module.exports = {
|
||||
testRegex: '(\\/spec|\\/src)\\/.*(_spec|\\.test)\\.(j|t)sx?$',
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less)$': '<rootDir>/spec/__mocks__/styleMock.js',
|
||||
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/fileMock.js',
|
||||
'\\.(css|less|geojson)$': '<rootDir>/spec/__mocks__/mockExportObject.js',
|
||||
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/mockExportString.js',
|
||||
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||
|
||||
6839
superset-frontend/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "1.3.1",
|
||||
"version": "0.0.0dev",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"license": "Apache-2.0",
|
||||
"directories": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"prod": "npm run build",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --colors",
|
||||
"build-instrumented": "cross-env NODE_ENV=development BABEL_ENV=instrumented webpack --mode=development --colors",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=production webpack --mode=production --colors",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --mode=production --colors",
|
||||
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx,.ts,.tsx . && npm run type",
|
||||
"prettier-check": "prettier --check '{src,stylesheets}/**/*.{css,less,sass,scss}'",
|
||||
"lint-fix": "eslint --fix --ignore-path=.eslintignore --ext .js,.jsx,.ts,tsx . && npm run clean-css && npm run type",
|
||||
@@ -65,37 +65,38 @@
|
||||
"@babel/runtime-corejs3": "^7.12.5",
|
||||
"@data-ui/sparkline": "^0.0.84",
|
||||
"@emotion/babel-preset-css-prop": "^11.2.0",
|
||||
"@emotion/cache": "^11.1.3",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@superset-ui/chart-controls": "^0.17.84",
|
||||
"@superset-ui/core": "^0.17.81",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.84",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.84",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.17.84",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.10",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.84",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.17.84",
|
||||
"@superset-ui/plugin-chart-pivot-table": "^0.17.84",
|
||||
"@superset-ui/plugin-chart-table": "^0.17.84",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.17.84",
|
||||
"@superset-ui/preset-chart-xy": "^0.17.84",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@superset-ui/chart-controls": "^0.18.2",
|
||||
"@superset-ui/core": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-event-flow": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-force-directed": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-heatmap": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-histogram": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-map-box": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-sankey": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.2",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.2",
|
||||
"@superset-ui/legacy-preset-chart-big-number": "^0.18.2",
|
||||
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.12",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.2",
|
||||
"@superset-ui/plugin-chart-echarts": "^0.18.2",
|
||||
"@superset-ui/plugin-chart-pivot-table": "^0.18.2",
|
||||
"@superset-ui/plugin-chart-table": "^0.18.2",
|
||||
"@superset-ui/plugin-chart-word-cloud": "^0.18.2",
|
||||
"@superset-ui/preset-chart-xy": "^0.18.2",
|
||||
"@vx/responsive": "^0.0.195",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"antd": "^4.9.4",
|
||||
@@ -147,6 +148,7 @@
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-draggable": "^4.4.3",
|
||||
"react-gravatar": "^2.6.1",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"react-js-cron": "^1.2.0",
|
||||
@@ -299,7 +301,6 @@
|
||||
"storybook-addon-jsx": "^7.3.3",
|
||||
"storybook-addon-paddings": "^3.2.0",
|
||||
"style-loader": "^1.0.0",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"thread-loader": "^1.2.0",
|
||||
"transform-loader": "^0.2.3",
|
||||
"ts-jest": "^26.4.2",
|
||||
|
||||
38
superset-frontend/spec/fixtures/mockReportState.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 dashboardInfo from './mockDashboardInfo';
|
||||
import { user } from '../javascripts/sqllab/fixtures';
|
||||
|
||||
export default {
|
||||
active: true,
|
||||
creation_method: 'dashboards',
|
||||
crontab: '0 12 * * 1',
|
||||
dashboard: dashboardInfo.id,
|
||||
name: 'Weekly Report',
|
||||
owners: [user.userId],
|
||||
recipients: [
|
||||
{
|
||||
recipient_config_json: {
|
||||
target: user.email,
|
||||
},
|
||||
type: 'Email',
|
||||
},
|
||||
],
|
||||
type: 'Report',
|
||||
};
|
||||
46
superset-frontend/spec/fixtures/mockStateWithoutUser.tsx
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 datasources from 'spec/fixtures/mockDatasource';
|
||||
import messageToasts from 'spec/javascripts/messageToasts/mockMessageToasts';
|
||||
import {
|
||||
nativeFiltersInfo,
|
||||
mockDataMaskInfo,
|
||||
} from 'spec/javascripts/dashboard/fixtures/mockNativeFilters';
|
||||
import chartQueries from 'spec/fixtures/mockChartQueries';
|
||||
import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
|
||||
import dashboardInfo from 'spec/fixtures/mockDashboardInfo';
|
||||
import { emptyFilters } from 'spec/fixtures/mockDashboardFilters';
|
||||
import dashboardState from 'spec/fixtures/mockDashboardState';
|
||||
import { sliceEntitiesForChart } from 'spec/fixtures/mockSliceEntities';
|
||||
import reports from 'spec/fixtures/mockReportState';
|
||||
|
||||
export default {
|
||||
datasources,
|
||||
sliceEntities: sliceEntitiesForChart,
|
||||
charts: chartQueries,
|
||||
nativeFilters: nativeFiltersInfo,
|
||||
dataMask: mockDataMaskInfo,
|
||||
dashboardInfo,
|
||||
dashboardFilters: emptyFilters,
|
||||
dashboardState,
|
||||
dashboardLayout,
|
||||
messageToasts,
|
||||
impressionId: 'mock_impression_id',
|
||||
reports,
|
||||
};
|
||||
@@ -30,6 +30,7 @@ import saveModal from 'src/explore/reducers/saveModalReducer';
|
||||
import explore from 'src/explore/reducers/exploreReducer';
|
||||
import sqlLab from 'src/SqlLab/reducers/sqlLab';
|
||||
import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
|
||||
import reports from 'src/reports/reducers/reports';
|
||||
|
||||
const impressionId = (state = '') => state;
|
||||
|
||||
@@ -53,5 +54,6 @@ export default {
|
||||
explore,
|
||||
sqlLab,
|
||||
localStorageUsageInKilobytes,
|
||||
reports,
|
||||
common: () => common,
|
||||
};
|
||||
|
||||
@@ -23,3 +23,5 @@ import { configure as configureTestingLibrary } from '@testing-library/react';
|
||||
configureTestingLibrary({
|
||||
testIdAttribute: 'data-test',
|
||||
});
|
||||
|
||||
document.body.innerHTML = '<div id="app" data-bootstrap="{}"></div>';
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('RefreshIntervalModal', () => {
|
||||
});
|
||||
it('should change refreshFrequency with edit mode', () => {
|
||||
const wrapper = getMountWrapper(mockedProps);
|
||||
wrapper.instance().handleFrequencyChange({ value: 30 });
|
||||
wrapper.instance().handleFrequencyChange(30);
|
||||
wrapper.instance().onSave();
|
||||
expect(mockedProps.onChange).toHaveBeenCalled();
|
||||
expect(mockedProps.onChange).toHaveBeenCalledWith(30, mockedProps.editMode);
|
||||
@@ -69,11 +69,11 @@ describe('RefreshIntervalModal', () => {
|
||||
const wrapper = getMountWrapper(props);
|
||||
wrapper.find('span[role="button"]').simulate('click');
|
||||
|
||||
wrapper.instance().handleFrequencyChange({ value: 30 });
|
||||
wrapper.instance().handleFrequencyChange(30);
|
||||
wrapper.update();
|
||||
expect(wrapper.find(ModalTrigger).find(Alert)).toExist();
|
||||
|
||||
wrapper.instance().handleFrequencyChange({ value: 3601 });
|
||||
wrapper.instance().handleFrequencyChange(3601);
|
||||
wrapper.update();
|
||||
expect(wrapper.find(ModalTrigger).find(Alert)).not.toExist();
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
import Chart from 'src/dashboard/containers/Chart';
|
||||
import ChartHolder from 'src/dashboard/components/gridComponents/ChartHolder';
|
||||
import ChartHolderConnected from 'src/dashboard/components/gridComponents/ChartHolder';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
||||
@@ -71,7 +71,7 @@ describe('ChartHolder', () => {
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ChartHolder {...props} {...overrideProps} />
|
||||
<ChartHolderConnected {...props} {...overrideProps} />
|
||||
</DndProvider>
|
||||
</Provider>,
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MarkdownEditor } from 'src/components/AsyncAceEditor';
|
||||
import Markdown from 'src/dashboard/components/gridComponents/Markdown';
|
||||
import MarkdownConnected from 'src/dashboard/components/gridComponents/Markdown';
|
||||
import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
@@ -66,7 +66,7 @@ describe('Markdown', () => {
|
||||
const wrapper = mount(
|
||||
<Provider store={mockStore}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Markdown {...props} {...overrideProps} />
|
||||
<MarkdownConnected {...props} {...overrideProps} />
|
||||
</DndProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ExploreChartHeader } from 'src/explore/components/ExploreChartHeader';
|
||||
import ExploreActionButtons from 'src/explore/components/ExploreActionButtons';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
|
||||
const saveSliceStub = jest.fn();
|
||||
const updateChartTitleStub = jest.fn();
|
||||
const fetchUISpecificReportStub = jest.fn();
|
||||
const mockProps = {
|
||||
actions: {
|
||||
saveSlice: saveSliceStub,
|
||||
updateChartTitle: updateChartTitleStub,
|
||||
},
|
||||
can_overwrite: true,
|
||||
can_download: true,
|
||||
isStarred: true,
|
||||
slice: {
|
||||
form_data: {
|
||||
viz_type: 'line',
|
||||
},
|
||||
},
|
||||
table_name: 'foo',
|
||||
form_data: {
|
||||
viz_type: 'table',
|
||||
},
|
||||
user: {
|
||||
createdOn: '2021-04-27T18:12:38.952304',
|
||||
email: 'admin',
|
||||
firstName: 'admin',
|
||||
isActive: true,
|
||||
lastName: 'admin',
|
||||
permissions: {},
|
||||
roles: { Admin: Array(173) },
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
},
|
||||
timeout: 1000,
|
||||
chart: {
|
||||
id: 0,
|
||||
queryResponse: {},
|
||||
},
|
||||
fetchUISpecificReport: fetchUISpecificReportStub,
|
||||
chartHeight: '30px',
|
||||
};
|
||||
|
||||
describe('ExploreChartHeader', () => {
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ExploreChartHeader {...mockProps} />);
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(<ExploreChartHeader {...mockProps} />)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(EditableTitle)).toExist();
|
||||
expect(wrapper.find(ExploreActionButtons)).toExist();
|
||||
});
|
||||
|
||||
it('should update title but not save', () => {
|
||||
const editableTitle = wrapper.find(EditableTitle);
|
||||
expect(editableTitle.props().onSaveTitle).toBe(updateChartTitleStub);
|
||||
});
|
||||
});
|
||||
@@ -42,14 +42,16 @@ describe('SaveModal', () => {
|
||||
dashboards: [],
|
||||
},
|
||||
explore: {
|
||||
can_overwrite: true,
|
||||
user_id: '1',
|
||||
datasource: {},
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
slice_name: 'title',
|
||||
owners: [1],
|
||||
},
|
||||
alert: null,
|
||||
user: {
|
||||
userId: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = mockStore(initialState);
|
||||
@@ -104,7 +106,7 @@ describe('SaveModal', () => {
|
||||
|
||||
it('disable overwrite option for non-owner', () => {
|
||||
const wrapperForNonOwner = getWrapper();
|
||||
wrapperForNonOwner.setProps({ can_overwrite: false });
|
||||
wrapperForNonOwner.setProps({ userId: 2 });
|
||||
const overwriteRadio = wrapperForNonOwner.find('#overwrite-radio');
|
||||
expect(overwriteRadio).toHaveLength(1);
|
||||
expect(overwriteRadio.prop('disabled')).toBe(true);
|
||||
|
||||
@@ -520,7 +520,20 @@ export default function sqlLabReducer(state = {}, action) {
|
||||
if (changedQuery.changedOn > queriesLastUpdate) {
|
||||
queriesLastUpdate = changedQuery.changedOn;
|
||||
}
|
||||
newQueries[id] = { ...state.queries[id], ...changedQuery };
|
||||
const prevState = state.queries[id]?.state;
|
||||
const currentState = changedQuery.state;
|
||||
newQueries[id] = {
|
||||
...state.queries[id],
|
||||
...changedQuery,
|
||||
// race condition:
|
||||
// because of async behavior, sql lab may still poll a couple of seconds
|
||||
// when it started fetching or finished rendering results
|
||||
state:
|
||||
currentState === 'success' &&
|
||||
['fetching', 'success'].includes(prevState)
|
||||
? prevState
|
||||
: currentState,
|
||||
};
|
||||
change = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,11 +28,9 @@ import VizTypeGallery from 'src/explore/components/controls/VizTypeControl/VizTy
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import { act } from 'spec/helpers/testing-library';
|
||||
|
||||
const defaultProps = {
|
||||
datasources: [
|
||||
{ label: 'my first table', value: '1__table' },
|
||||
{ label: 'another great table', value: '2__table' },
|
||||
],
|
||||
const datasource = {
|
||||
value: '1',
|
||||
label: 'table',
|
||||
};
|
||||
|
||||
describe('AddSliceContainer', () => {
|
||||
@@ -43,7 +41,7 @@ describe('AddSliceContainer', () => {
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(<AddSliceContainer {...defaultProps} />) as ReactWrapper<
|
||||
wrapper = mount(<AddSliceContainer />) as ReactWrapper<
|
||||
AddSliceContainerProps,
|
||||
AddSliceContainerState,
|
||||
AddSliceContainer
|
||||
@@ -68,11 +66,8 @@ describe('AddSliceContainer', () => {
|
||||
});
|
||||
|
||||
it('renders an enabled button if datasource and viz type is selected', () => {
|
||||
const datasourceValue = defaultProps.datasources[0].value;
|
||||
wrapper.setState({
|
||||
datasourceValue,
|
||||
datasourceId: datasourceValue.split('__')[0],
|
||||
datasourceType: datasourceValue.split('__')[1],
|
||||
datasource,
|
||||
visType: 'table',
|
||||
});
|
||||
expect(
|
||||
@@ -81,15 +76,12 @@ describe('AddSliceContainer', () => {
|
||||
});
|
||||
|
||||
it('formats explore url', () => {
|
||||
const datasourceValue = defaultProps.datasources[0].value;
|
||||
wrapper.setState({
|
||||
datasourceValue,
|
||||
datasourceId: datasourceValue.split('__')[0],
|
||||
datasourceType: datasourceValue.split('__')[1],
|
||||
datasource,
|
||||
visType: 'table',
|
||||
});
|
||||
const formattedUrl =
|
||||
'/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221__table%22%7D';
|
||||
'/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
|
||||
expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,32 +17,34 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import rison from 'rison';
|
||||
import { styled, t, SupersetClient, JsonResponse } from '@superset-ui/core';
|
||||
import { Steps } from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { Select } from 'src/components';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { FormLabel } from 'src/components/Form';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
||||
import VizTypeGallery, {
|
||||
MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
|
||||
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
||||
|
||||
interface Datasource {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type AddSliceContainerProps = {
|
||||
datasources: Datasource[];
|
||||
type Dataset = {
|
||||
id: number;
|
||||
table_name: string;
|
||||
description: string;
|
||||
datasource_type: string;
|
||||
};
|
||||
|
||||
export type AddSliceContainerProps = {};
|
||||
|
||||
export type AddSliceContainerState = {
|
||||
datasourceId?: string;
|
||||
datasourceType?: string;
|
||||
datasourceValue?: string;
|
||||
datasource?: { label: string; value: string };
|
||||
visType: string | null;
|
||||
};
|
||||
|
||||
const ESTIMATED_NAV_HEIGHT = '56px';
|
||||
const ESTIMATED_NAV_HEIGHT = 56;
|
||||
const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${({ theme }) => `
|
||||
@@ -52,7 +54,7 @@ const StyledContainer = styled.div`
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: ${MAX_ADVISABLE_VIZ_GALLERY_WIDTH}px;
|
||||
max-height: calc(100vh - ${ESTIMATED_NAV_HEIGHT});
|
||||
max-height: calc(100vh - ${ESTIMATED_NAV_HEIGHT}px);
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
background-color: ${theme.colors.grayscale.light5};
|
||||
margin-left: auto;
|
||||
@@ -69,6 +71,7 @@ const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
|
||||
& > div {
|
||||
min-width: 200px;
|
||||
@@ -78,22 +81,100 @@ const StyledContainer = styled.div`
|
||||
& > span {
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
margin-left: ${theme.gridUnit * 4}px;
|
||||
margin-top: ${theme.gridUnit * 6}px;
|
||||
}
|
||||
}
|
||||
|
||||
& .viz-gallery {
|
||||
border: 1px solid ${theme.colors.grayscale.light2};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
margin: ${theme.gridUnit}px 0px;
|
||||
max-height: calc(100vh - ${ELEMENTS_EXCEPT_VIZ_GALLERY}px);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& .footer {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
& > span {
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
|
||||
/* The following extra ampersands (&&&&) are used to boost selector specificity */
|
||||
|
||||
&&&& .ant-steps-item-tail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&&&& .ant-steps-item-icon {
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
width: ${theme.gridUnit * 5}px;
|
||||
height: ${theme.gridUnit * 5}px;
|
||||
line-height: ${theme.gridUnit * 5}px;
|
||||
}
|
||||
|
||||
&&&& .ant-steps-item-title {
|
||||
line-height: ${theme.gridUnit * 5}px;
|
||||
}
|
||||
|
||||
&&&& .ant-steps-item-content {
|
||||
overflow: unset;
|
||||
|
||||
.ant-steps-item-description {
|
||||
margin-top: ${theme.gridUnit}px;
|
||||
}
|
||||
}
|
||||
|
||||
&&&& .ant-tooltip-open {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&&&& .ant-select-selector {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&&&& .ant-select-selection-placeholder {
|
||||
padding-left: ${theme.gridUnit * 3}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const cssStatic = css`
|
||||
flex: 0 0 auto;
|
||||
const TooltipContent = styled.div<{ hasDescription: boolean }>`
|
||||
${({ theme, hasDescription }) => `
|
||||
.tooltip-header {
|
||||
font-size: ${
|
||||
hasDescription ? theme.typography.sizes.l : theme.typography.sizes.s
|
||||
}px;
|
||||
font-weight: ${
|
||||
hasDescription
|
||||
? theme.typography.weights.bold
|
||||
: theme.typography.weights.normal
|
||||
};
|
||||
}
|
||||
|
||||
.tooltip-description {
|
||||
margin-top: ${theme.gridUnit * 2}px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 20;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledVizTypeGallery = styled(VizTypeGallery)`
|
||||
const StyledLabel = styled.span`
|
||||
${({ theme }) => `
|
||||
border: 1px solid ${theme.colors.grayscale.light2};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
margin: ${theme.gridUnit * 3}px 0px;
|
||||
flex: 1 1 auto;
|
||||
position: absolute;
|
||||
left: ${theme.gridUnit * 3}px;
|
||||
right: ${theme.gridUnit * 3}px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -110,13 +191,16 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
this.changeDatasource = this.changeDatasource.bind(this);
|
||||
this.changeVisType = this.changeVisType.bind(this);
|
||||
this.gotoSlice = this.gotoSlice.bind(this);
|
||||
this.newLabel = this.newLabel.bind(this);
|
||||
this.loadDatasources = this.loadDatasources.bind(this);
|
||||
this.handleFilterOption = this.handleFilterOption.bind(this);
|
||||
}
|
||||
|
||||
exploreUrl() {
|
||||
const formData = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
viz_type: this.state.visType,
|
||||
datasource: this.state.datasourceValue,
|
||||
datasource: this.state.datasource?.value,
|
||||
}),
|
||||
);
|
||||
return `/superset/explore/?form_data=${formData}`;
|
||||
@@ -126,11 +210,8 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
window.location.href = this.exploreUrl();
|
||||
}
|
||||
|
||||
changeDatasource(value: string) {
|
||||
this.setState({
|
||||
datasourceValue: value,
|
||||
datasourceId: value.split('__')[0],
|
||||
});
|
||||
changeDatasource(datasource: { label: string; value: string }) {
|
||||
this.setState({ datasource });
|
||||
}
|
||||
|
||||
changeVisType(visType: string | null) {
|
||||
@@ -138,55 +219,131 @@ export default class AddSliceContainer extends React.PureComponent<
|
||||
}
|
||||
|
||||
isBtnDisabled() {
|
||||
return !(this.state.datasourceId && this.state.visType);
|
||||
return !(this.state.datasource?.value && this.state.visType);
|
||||
}
|
||||
|
||||
newLabel(item: Dataset) {
|
||||
return (
|
||||
<Tooltip
|
||||
mouseEnterDelay={1}
|
||||
placement="right"
|
||||
title={
|
||||
<TooltipContent hasDescription={!!item.description}>
|
||||
<div className="tooltip-header">{item.table_name}</div>
|
||||
{item.description && (
|
||||
<div className="tooltip-description">{item.description}</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
}
|
||||
>
|
||||
<StyledLabel>{item.table_name}</StyledLabel>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
loadDatasources(search: string, page: number, pageSize: number) {
|
||||
const query = rison.encode({
|
||||
columns: ['id', 'table_name', 'description', 'datasource_type'],
|
||||
filters: [{ col: 'table_name', opr: 'ct', value: search }],
|
||||
page,
|
||||
page_size: pageSize,
|
||||
order_column: 'table_name',
|
||||
order_direction: 'asc',
|
||||
});
|
||||
return SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/?q=${query}`,
|
||||
}).then((response: JsonResponse) => {
|
||||
const list: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[] = response.json.result
|
||||
.map((item: Dataset) => ({
|
||||
value: `${item.id}__${item.datasource_type}`,
|
||||
label: this.newLabel(item),
|
||||
labelText: item.table_name,
|
||||
}))
|
||||
.sort((a: { labelText: string }, b: { labelText: string }) =>
|
||||
a.labelText.localeCompare(b.labelText),
|
||||
);
|
||||
return {
|
||||
data: list,
|
||||
totalCount: response.json.count,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterOption(
|
||||
search: string,
|
||||
option: { label: string; value: number; labelText: string },
|
||||
) {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
const { labelText } = option;
|
||||
return labelText.toLowerCase().includes(searchValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
const isButtonDisabled = this.isBtnDisabled();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<h3 css={cssStatic}>{t('Create a new chart')}</h3>
|
||||
<div className="dataset">
|
||||
<Select
|
||||
autoFocus
|
||||
ariaLabel={t('Dataset')}
|
||||
name="select-datasource"
|
||||
header={<FormLabel required>{t('Choose a dataset')}</FormLabel>}
|
||||
onChange={this.changeDatasource}
|
||||
options={this.props.datasources}
|
||||
placeholder={t('Choose a dataset')}
|
||||
showSearch
|
||||
value={this.state.datasourceValue}
|
||||
<h3>{t('Create a new chart')}</h3>
|
||||
<Steps direction="vertical" size="small">
|
||||
<Steps.Step
|
||||
title={<FormLabel>{t('Choose a dataset')}</FormLabel>}
|
||||
status={this.state.datasource?.value ? 'finish' : 'process'}
|
||||
description={
|
||||
<div className="dataset">
|
||||
<Select
|
||||
autoFocus
|
||||
ariaLabel={t('Dataset')}
|
||||
name="select-datasource"
|
||||
filterOption={this.handleFilterOption}
|
||||
onChange={this.changeDatasource}
|
||||
options={this.loadDatasources}
|
||||
placeholder={t('Choose a dataset')}
|
||||
showSearch
|
||||
value={this.state.datasource}
|
||||
/>
|
||||
<span>
|
||||
{t(
|
||||
'Instructions to add a dataset are available in the Superset tutorial.',
|
||||
)}{' '}
|
||||
<a
|
||||
href="https://superset.apache.org/docs/creating-charts-dashboards/first-dashboard#adding-a-new-table"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<i className="fa fa-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
{t(
|
||||
'Instructions to add a dataset are available in the Superset tutorial.',
|
||||
)}{' '}
|
||||
<a
|
||||
href="https://superset.apache.org/docs/creating-charts-dashboards/first-dashboard#adding-a-new-table"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<i className="fa fa-external-link" />
|
||||
</a>
|
||||
</span>
|
||||
<Steps.Step
|
||||
title={<FormLabel>{t('Choose chart type')}</FormLabel>}
|
||||
status={this.state.visType ? 'finish' : 'process'}
|
||||
description={
|
||||
<VizTypeGallery
|
||||
className="viz-gallery"
|
||||
onChange={this.changeVisType}
|
||||
selectedViz={this.state.visType}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Steps>
|
||||
<div className="footer">
|
||||
{isButtonDisabled && (
|
||||
<span>
|
||||
{t('Please select both a Dataset and a Chart type to proceed')}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
disabled={isButtonDisabled}
|
||||
onClick={this.gotoSlice}
|
||||
>
|
||||
{t('Create new chart')}
|
||||
</Button>
|
||||
</div>
|
||||
<StyledVizTypeGallery
|
||||
onChange={this.changeVisType}
|
||||
selectedViz={this.state.visType}
|
||||
/>
|
||||
<Button
|
||||
css={[
|
||||
cssStatic,
|
||||
css`
|
||||
align-self: flex-end;
|
||||
`,
|
||||
]}
|
||||
buttonStyle="primary"
|
||||
disabled={this.isBtnDisabled()}
|
||||
onClick={this.gotoSlice}
|
||||
>
|
||||
{t('Create new chart')}
|
||||
</Button>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ initFeatureFlags(bootstrapData.common.feature_flags);
|
||||
const App = () => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<DynamicPluginProvider>
|
||||
<AddSliceContainer datasources={bootstrapData.datasources} />
|
||||
<AddSliceContainer />
|
||||
</DynamicPluginProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -68,6 +68,9 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const BLANK = {};
|
||||
const NONEXISTENT_DATASET = t(
|
||||
'The dataset associated with this chart no longer exists',
|
||||
);
|
||||
|
||||
const defaultProps = {
|
||||
addFilter: () => BLANK,
|
||||
@@ -178,7 +181,11 @@ class Chart extends React.PureComponent {
|
||||
const message = chartAlert || queryResponse?.message;
|
||||
|
||||
// if datasource is still loading, don't render JS errors
|
||||
if (chartAlert && datasource === PLACEHOLDER_DATASOURCE) {
|
||||
if (
|
||||
chartAlert !== undefined &&
|
||||
chartAlert !== NONEXISTENT_DATASET &&
|
||||
datasource === PLACEHOLDER_DATASOURCE
|
||||
) {
|
||||
return (
|
||||
<Styles
|
||||
data-ui-anchor="chart"
|
||||
|
||||
@@ -49,6 +49,7 @@ export {
|
||||
Row,
|
||||
Space,
|
||||
Skeleton,
|
||||
Steps,
|
||||
Switch,
|
||||
Tag,
|
||||
Tabs,
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface ButtonProps {
|
||||
htmlType?: 'button' | 'submit' | 'reset';
|
||||
cta?: boolean;
|
||||
loading?: boolean | { delay?: number | undefined } | undefined;
|
||||
showMarginRight?: boolean;
|
||||
}
|
||||
|
||||
export default function Button(props: ButtonProps) {
|
||||
@@ -76,6 +77,7 @@ export default function Button(props: ButtonProps) {
|
||||
cta,
|
||||
children,
|
||||
href,
|
||||
showMarginRight = true,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
@@ -154,8 +156,8 @@ export default function Button(props: ButtonProps) {
|
||||
} else {
|
||||
renderedChildren = Children.toArray(children);
|
||||
}
|
||||
|
||||
const firstChildMargin = renderedChildren.length > 1 ? theme.gridUnit * 2 : 0;
|
||||
const firstChildMargin =
|
||||
showMarginRight && renderedChildren.length > 1 ? theme.gridUnit * 2 : 0;
|
||||
|
||||
const button = (
|
||||
<AntdButton
|
||||
|
||||
@@ -52,6 +52,14 @@ const StyledTooltip = styled(Tooltip)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTooltipTitle = styled.span`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 20;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const defaultOverlayStyle = {
|
||||
fontSize: '12px',
|
||||
lineHeight: '16px',
|
||||
@@ -69,7 +77,7 @@ export default function InfoTooltip({
|
||||
}: InfoTooltipProps) {
|
||||
return (
|
||||
<StyledTooltip
|
||||
title={tooltip}
|
||||
title={<StyledTooltipTitle>{tooltip}</StyledTooltipTitle>}
|
||||
placement={placement}
|
||||
trigger={trigger}
|
||||
overlayStyle={overlayStyle}
|
||||
|
||||
@@ -103,7 +103,6 @@ const mockedProps = {
|
||||
locale: 'en',
|
||||
version_string: '1.0.0',
|
||||
version_sha: 'randomSHA',
|
||||
build_number: 'randomBuildNumber',
|
||||
},
|
||||
settings: [
|
||||
{
|
||||
@@ -281,10 +280,10 @@ test('should render the Profile link when available', async () => {
|
||||
expect(profile).toHaveAttribute('href', user_profile_url);
|
||||
});
|
||||
|
||||
test('should render the About section and version_string, sha or build_number when available', async () => {
|
||||
test('should render the About section and version_string or sha when available', async () => {
|
||||
const {
|
||||
data: {
|
||||
navbar_right: { version_sha, version_string, build_number },
|
||||
navbar_right: { version_sha, version_string },
|
||||
},
|
||||
} = mockedProps;
|
||||
|
||||
@@ -293,11 +292,9 @@ test('should render the About section and version_string, sha or build_number wh
|
||||
const about = await screen.findByText('About');
|
||||
const version = await screen.findByText(`Version: ${version_string}`);
|
||||
const sha = await screen.findByText(`SHA: ${version_sha}`);
|
||||
const build = await screen.findByText(`Build: ${build_number}`);
|
||||
expect(about).toBeInTheDocument();
|
||||
expect(version).toBeInTheDocument();
|
||||
expect(sha).toBeInTheDocument();
|
||||
expect(build).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Documentation link when available', async () => {
|
||||
|
||||
@@ -44,7 +44,6 @@ export interface NavBarProps {
|
||||
bug_report_url?: string;
|
||||
version_string?: string;
|
||||
version_sha?: string;
|
||||
build_number?: string;
|
||||
documentation_url?: string;
|
||||
languages: Languages;
|
||||
show_language_picker: boolean;
|
||||
|
||||
@@ -146,9 +146,7 @@ const RightMenu = ({
|
||||
</Menu.Item>
|
||||
</Menu.ItemGroup>,
|
||||
]}
|
||||
{(navbarRight.version_string ||
|
||||
navbarRight.version_sha ||
|
||||
navbarRight.build_number) && [
|
||||
{(navbarRight.version_string || navbarRight.version_sha) && [
|
||||
<Menu.Divider key="version-info-divider" />,
|
||||
<Menu.ItemGroup key="about-section" title={t('About')}>
|
||||
<div className="about-section">
|
||||
@@ -167,11 +165,6 @@ const RightMenu = ({
|
||||
SHA: {navbarRight.version_sha}
|
||||
</div>
|
||||
)}
|
||||
{navbarRight.build_number && (
|
||||
<div css={versionInfoStyles}>
|
||||
Build: {navbarRight.build_number}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Menu.ItemGroup>,
|
||||
]}
|
||||
|
||||
@@ -34,6 +34,8 @@ InteractiveModal.args = {
|
||||
primaryButtonType: 'danger',
|
||||
show: true,
|
||||
title: "I'm a modal!",
|
||||
resizable: false,
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
InteractiveModal.argTypes = {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { isNil } from 'lodash';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { css } from '@emotion/react';
|
||||
@@ -25,6 +25,13 @@ import {
|
||||
ModalProps as AntdModalProps,
|
||||
} from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { Resizable, ResizableProps } from 're-resizable';
|
||||
import Draggable, {
|
||||
DraggableBounds,
|
||||
DraggableData,
|
||||
DraggableEvent,
|
||||
DraggableProps,
|
||||
} from 'react-draggable';
|
||||
|
||||
export interface ModalProps {
|
||||
className?: string;
|
||||
@@ -46,6 +53,11 @@ export interface ModalProps {
|
||||
wrapProps?: object;
|
||||
height?: string;
|
||||
closable?: boolean;
|
||||
resizable?: boolean;
|
||||
resizableConfig?: ResizableProps;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: DraggableProps;
|
||||
destroyOnClose?: boolean;
|
||||
}
|
||||
|
||||
interface StyledModalProps {
|
||||
@@ -53,8 +65,19 @@ interface StyledModalProps {
|
||||
responsive?: boolean;
|
||||
height?: string;
|
||||
hideFooter?: boolean;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
}
|
||||
|
||||
const MODAL_HEADER_HEIGHT = 55;
|
||||
const MODAL_MIN_CONTENT_HEIGHT = 54;
|
||||
const MODAL_FOOTER_HEIGHT = 65;
|
||||
|
||||
const RESIZABLE_MIN_HEIGHT = MODAL_HEADER_HEIGHT + MODAL_MIN_CONTENT_HEIGHT;
|
||||
const RESIZABLE_MIN_WIDTH = '380px';
|
||||
const RESIZABLE_MAX_HEIGHT = '100vh';
|
||||
const RESIZABLE_MAX_WIDTH = '100vw';
|
||||
|
||||
const BaseModal = (props: AntdModalProps) => (
|
||||
// Removes mask animation. Fixed in 4.6.0.
|
||||
// https://github.com/ant-design/ant-design/issues/27192
|
||||
@@ -100,9 +123,8 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
.ant-modal-body {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
overflow: auto;
|
||||
${({ height }) => height && `height: ${height};`}
|
||||
${({ resizable, height }) => !resizable && height && `height: ${height};`}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: ${({ theme }) => theme.gridUnit / 4}px solid
|
||||
${({ theme }) => theme.colors.grayscale.light2};
|
||||
@@ -128,6 +150,44 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${({ draggable, theme }) =>
|
||||
draggable &&
|
||||
`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`};
|
||||
|
||||
${({ resizable, hideFooter }) =>
|
||||
resizable &&
|
||||
`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
|
||||
.ant-modal-body {
|
||||
/* 100% - header height - footer height */
|
||||
height: ${
|
||||
hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px);`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px);`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const CustomModal = ({
|
||||
@@ -147,8 +207,33 @@ const CustomModal = ({
|
||||
footer,
|
||||
hideFooter,
|
||||
wrapProps,
|
||||
draggable = false,
|
||||
resizable = false,
|
||||
resizableConfig = {
|
||||
maxHeight: RESIZABLE_MAX_HEIGHT,
|
||||
maxWidth: RESIZABLE_MAX_WIDTH,
|
||||
minHeight: hideFooter
|
||||
? RESIZABLE_MIN_HEIGHT
|
||||
: RESIZABLE_MIN_HEIGHT + MODAL_FOOTER_HEIGHT,
|
||||
minWidth: RESIZABLE_MIN_WIDTH,
|
||||
enable: {
|
||||
bottom: true,
|
||||
bottomLeft: false,
|
||||
bottomRight: true,
|
||||
left: false,
|
||||
top: false,
|
||||
topLeft: false,
|
||||
topRight: false,
|
||||
right: true,
|
||||
},
|
||||
},
|
||||
draggableConfig,
|
||||
destroyOnClose,
|
||||
...rest
|
||||
}: ModalProps) => {
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [bounds, setBounds] = useState<DraggableBounds>();
|
||||
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
|
||||
const modalFooter = isNil(footer)
|
||||
? [
|
||||
<Button key="back" onClick={onHide} cta data-test="modal-cancel-button">
|
||||
@@ -168,6 +253,35 @@ const CustomModal = ({
|
||||
: footer;
|
||||
|
||||
const modalWidth = width || (responsive ? '100vw' : '600px');
|
||||
const shouldShowMask = !(resizable || draggable);
|
||||
|
||||
const onDragStart = (_: DraggableEvent, uiData: DraggableData) => {
|
||||
const { clientWidth, clientHeight } = window?.document?.documentElement;
|
||||
const targetRect = draggableRef?.current?.getBoundingClientRect();
|
||||
|
||||
if (targetRect) {
|
||||
setBounds({
|
||||
left: -targetRect?.left + uiData?.x,
|
||||
right: clientWidth - (targetRect?.right - uiData?.x),
|
||||
top: -targetRect?.top + uiData?.y,
|
||||
bottom: clientHeight - (targetRect?.bottom - uiData?.y),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const ModalTitle = () =>
|
||||
draggable ? (
|
||||
<div
|
||||
className="draggable-trigger"
|
||||
onMouseOver={() => dragDisabled && setDragDisabled(false)}
|
||||
onMouseOut={() => !dragDisabled && setDragDisabled(true)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<>{title}</>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
centered={!!centered}
|
||||
@@ -177,14 +291,41 @@ const CustomModal = ({
|
||||
maxWidth={maxWidth}
|
||||
responsive={responsive}
|
||||
visible={show}
|
||||
title={title}
|
||||
title={<ModalTitle />}
|
||||
closeIcon={
|
||||
<span className="close" aria-hidden="true">
|
||||
×
|
||||
</span>
|
||||
}
|
||||
footer={!hideFooter ? modalFooter : null}
|
||||
hideFooter={hideFooter}
|
||||
wrapProps={{ 'data-test': `${name || title}-modal`, ...wrapProps }}
|
||||
modalRender={modal =>
|
||||
resizable || draggable ? (
|
||||
<Draggable
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
<Resizable className="resizable" {...resizableConfig}>
|
||||
<div className="resizable-wrapper" ref={draggableRef}>
|
||||
{modal}
|
||||
</div>
|
||||
</Resizable>
|
||||
) : (
|
||||
<div ref={draggableRef}>{modal}</div>
|
||||
)}
|
||||
</Draggable>
|
||||
) : (
|
||||
modal
|
||||
)
|
||||
}
|
||||
mask={shouldShowMask}
|
||||
draggable={draggable}
|
||||
resizable={resizable}
|
||||
destroyOnClose={destroyOnClose || resizable || draggable}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -33,6 +33,8 @@ interface IModalTriggerProps {
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
responsive?: boolean;
|
||||
draggable?: boolean;
|
||||
resizable?: boolean;
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -53,4 +55,6 @@ InteractiveModalTrigger.args = {
|
||||
width: '600px',
|
||||
maxWidth: '1000px',
|
||||
responsive: true,
|
||||
draggable: false,
|
||||
resizable: false,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,10 @@ const propTypes = {
|
||||
width: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
responsive: PropTypes.bool,
|
||||
resizable: PropTypes.bool,
|
||||
resizableConfig: PropTypes.object,
|
||||
draggable: PropTypes.bool,
|
||||
draggableConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -43,6 +47,8 @@ const defaultProps = {
|
||||
isButton: false,
|
||||
className: '',
|
||||
modalTitle: '',
|
||||
resizable: false,
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
export default class ModalTrigger extends React.Component {
|
||||
@@ -79,6 +85,10 @@ export default class ModalTrigger extends React.Component {
|
||||
width={this.props.width}
|
||||
maxWidth={this.props.maxWidth}
|
||||
responsive={this.props.responsive}
|
||||
resizable={this.props.resizable}
|
||||
resizableConfig={this.props.resizableConfig}
|
||||
draggable={this.props.draggable}
|
||||
draggableConfig={this.props.draggableConfig}
|
||||
>
|
||||
{this.props.modalBody}
|
||||
</Modal>
|
||||
|
||||
@@ -25,16 +25,16 @@ import OmniContainer from './index';
|
||||
jest.mock('src/featureFlags', () => ({
|
||||
isFeatureEnabled: jest.fn(),
|
||||
FeatureFlag: { OMNIBAR: 'OMNIBAR' },
|
||||
initFeatureFlags: jest.fn(),
|
||||
}));
|
||||
|
||||
test('Do not open Omnibar with the featureflag disabled', () => {
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(
|
||||
(ff: string) => !(ff === 'OMNIBAR'),
|
||||
);
|
||||
const logEvent = jest.fn();
|
||||
render(
|
||||
<div data-test="test">
|
||||
<OmniContainer logEvent={logEvent} />
|
||||
<OmniContainer />
|
||||
</div>,
|
||||
);
|
||||
|
||||
@@ -54,10 +54,9 @@ test('Open Omnibar with ctrl + k with featureflag enabled', () => {
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(
|
||||
(ff: string) => ff === 'OMNIBAR',
|
||||
);
|
||||
const logEvent = jest.fn();
|
||||
render(
|
||||
<div data-test="test">
|
||||
<OmniContainer logEvent={logEvent} />
|
||||
<OmniContainer />
|
||||
</div>,
|
||||
);
|
||||
|
||||
@@ -79,53 +78,18 @@ test('Open Omnibar with ctrl + k with featureflag enabled', () => {
|
||||
ctrlKey: true,
|
||||
code: 'KeyK',
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Open Omnibar with ctrl + s with featureflag enabled', () => {
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(
|
||||
(ff: string) => ff === 'OMNIBAR',
|
||||
);
|
||||
const logEvent = jest.fn();
|
||||
render(
|
||||
<div data-test="test">
|
||||
<OmniContainer logEvent={logEvent} />
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// show Omnibar
|
||||
fireEvent.keyDown(screen.getByTestId('test'), {
|
||||
ctrlKey: true,
|
||||
code: 'KeyS',
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// hide Omnibar
|
||||
fireEvent.keyDown(screen.getByTestId('test'), {
|
||||
ctrlKey: true,
|
||||
code: 'KeyS',
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Open Omnibar with Command + k with featureflag enabled', () => {
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(
|
||||
(ff: string) => ff === 'OMNIBAR',
|
||||
);
|
||||
const logEvent = jest.fn();
|
||||
render(
|
||||
<div data-test="test">
|
||||
<OmniContainer logEvent={logEvent} />
|
||||
<OmniContainer />
|
||||
</div>,
|
||||
);
|
||||
|
||||
@@ -149,17 +113,16 @@ test('Open Omnibar with Command + k with featureflag enabled', () => {
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).not.toBeVisible();
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Open Omnibar with Command + s with featureflag enabled', () => {
|
||||
test('Open Omnibar with Cmd/Ctrl-K and close with ESC', () => {
|
||||
(isFeatureEnabled as jest.Mock).mockImplementation(
|
||||
(ff: string) => ff === 'OMNIBAR',
|
||||
);
|
||||
const logEvent = jest.fn();
|
||||
render(
|
||||
<div data-test="test">
|
||||
<OmniContainer logEvent={logEvent} />
|
||||
<OmniContainer />
|
||||
</div>,
|
||||
);
|
||||
|
||||
@@ -169,19 +132,19 @@ test('Open Omnibar with Command + s with featureflag enabled', () => {
|
||||
|
||||
// show Omnibar
|
||||
fireEvent.keyDown(screen.getByTestId('test'), {
|
||||
metaKey: true,
|
||||
code: 'KeyS',
|
||||
ctrlKey: true,
|
||||
code: 'KeyK',
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// hide Omnibar
|
||||
// Close Omnibar
|
||||
fireEvent.keyDown(screen.getByTestId('test'), {
|
||||
metaKey: true,
|
||||
code: 'KeyS',
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
});
|
||||
expect(
|
||||
screen.queryByPlaceholderText('Search all dashboards'),
|
||||
).not.toBeVisible();
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -38,7 +38,8 @@ export function Omnibar({ extensions, placeholder, id }: Props) {
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
extensions={extensions}
|
||||
// autoFocus // I tried to use this prop (autoFocus) but it only works the first time that Omnibar is shown
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
*/
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import Modal from 'src/components/Modal';
|
||||
import { useComponentDidMount } from 'src/common/hooks/useComponentDidMount';
|
||||
import { logEvent } from 'src/logger/actions';
|
||||
import { Omnibar } from './Omnibar';
|
||||
import { LOG_ACTIONS_OMNIBAR_TRIGGERED } from '../../logger/LogUtils';
|
||||
import { getDashboards } from './getDashboards';
|
||||
@@ -31,37 +32,59 @@ const OmniModal = styled(Modal)`
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
logEvent: (log: string, object: object) => void;
|
||||
}
|
||||
|
||||
export default function OmniContainer({ logEvent }: Props) {
|
||||
export default function OmniContainer() {
|
||||
const showOmni = useRef<boolean>();
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const handleLogEvent = (show: boolean) =>
|
||||
logEvent(LOG_ACTIONS_OMNIBAR_TRIGGERED, {
|
||||
show_omni: show,
|
||||
});
|
||||
const handleClose = () => {
|
||||
showOmni.current = false;
|
||||
setShowModal(false);
|
||||
handleLogEvent(false);
|
||||
};
|
||||
|
||||
useComponentDidMount(() => {
|
||||
showOmni.current = false;
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!isFeatureEnabled(FeatureFlag.OMNIBAR)) return;
|
||||
const controlOrCommand = event.ctrlKey || event.metaKey;
|
||||
const isOk = ['KeyK', 'KeyS'].includes(event.code); // valid keys "s" or "k"
|
||||
const isOk = ['KeyK'].includes(event.code);
|
||||
const isEsc = event.key === 'Escape';
|
||||
|
||||
if (isEsc && showOmni.current) {
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
if (controlOrCommand && isOk) {
|
||||
logEvent(LOG_ACTIONS_OMNIBAR_TRIGGERED, {
|
||||
show_omni: !!showOmni.current,
|
||||
});
|
||||
showOmni.current = !showOmni.current;
|
||||
setShowModal(showOmni.current);
|
||||
if (showOmni.current) {
|
||||
document.getElementById('InputOmnibar')?.focus();
|
||||
}
|
||||
handleLogEvent(!!showOmni.current);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
modalRef.current &&
|
||||
!modalRef.current.contains(event.target as Node)
|
||||
) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
return () => document.removeEventListener('keydown', handleKeydown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -71,12 +94,15 @@ export default function OmniContainer({ logEvent }: Props) {
|
||||
hideFooter
|
||||
closable={false}
|
||||
onHide={() => {}}
|
||||
destroyOnClose
|
||||
>
|
||||
<Omnibar
|
||||
id="InputOmnibar"
|
||||
placeholder="Search all dashboards"
|
||||
extensions={[getDashboards]}
|
||||
/>
|
||||
<div ref={modalRef}>
|
||||
<Omnibar
|
||||
id="InputOmnibar"
|
||||
placeholder={t('Search all dashboards')}
|
||||
extensions={[getDashboards]}
|
||||
/>
|
||||
</div>
|
||||
</OmniModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,13 +55,17 @@ describe('Email Report Modal', () => {
|
||||
(featureFlag: FeatureFlag) => featureFlag === FeatureFlag.ALERT_REPORTS,
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
render(<ReportModal {...defaultProps} />, { useRedux: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
isFeatureEnabledMock.restore();
|
||||
});
|
||||
it('inputs respond correctly', () => {
|
||||
render(<ReportModal {...defaultProps} />, { useRedux: true });
|
||||
|
||||
it('inputs respond correctly', () => {
|
||||
// ----- Report name textbox
|
||||
// Initial value
|
||||
const reportNameTextbox = screen.getByTestId('report-name-test');
|
||||
@@ -86,4 +90,21 @@ describe('Email Report Modal', () => {
|
||||
const crontabInputs = screen.getAllByRole('combobox');
|
||||
expect(crontabInputs).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('does not allow user to create a report without a name', () => {
|
||||
// Grab name textbox and add button
|
||||
const reportNameTextbox = screen.getByTestId('report-name-test');
|
||||
const addButton = screen.getByRole('button', { name: /add/i });
|
||||
|
||||
// Add button should be enabled while name textbox has text
|
||||
expect(reportNameTextbox).toHaveDisplayValue('Weekly Report');
|
||||
expect(addButton).toBeEnabled();
|
||||
|
||||
// Clear the text from the name textbox
|
||||
userEvent.clear(reportNameTextbox);
|
||||
|
||||
// Add button should now be disabled, blocking user from creation
|
||||
expect(reportNameTextbox).toHaveDisplayValue('');
|
||||
expect(addButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
StyledRadioGroup,
|
||||
} from './styles';
|
||||
|
||||
interface ReportObject {
|
||||
export interface ReportObject {
|
||||
id?: number;
|
||||
active: boolean;
|
||||
crontab: string;
|
||||
@@ -125,7 +125,6 @@ type ReportActionType =
|
||||
type: ActionType.reset;
|
||||
};
|
||||
|
||||
const DEFAULT_NOTIFICATION_FORMAT = 'TEXT';
|
||||
const TEXT_BASED_VISUALIZATION_TYPES = [
|
||||
'pivot_table',
|
||||
'pivot_table_v2',
|
||||
@@ -133,28 +132,34 @@ const TEXT_BASED_VISUALIZATION_TYPES = [
|
||||
'paired_ttest',
|
||||
];
|
||||
|
||||
const NOTIFICATION_FORMATS = {
|
||||
TEXT: 'TEXT',
|
||||
PNG: 'PNG',
|
||||
CSV: 'CSV',
|
||||
};
|
||||
|
||||
const reportReducer = (
|
||||
state: Partial<ReportObject> | null,
|
||||
action: ReportActionType,
|
||||
): Partial<ReportObject> | null => {
|
||||
const initialState = {
|
||||
name: state?.name || 'Weekly Report',
|
||||
report_format: state?.report_format || DEFAULT_NOTIFICATION_FORMAT,
|
||||
...(state || {}),
|
||||
name: 'Weekly Report',
|
||||
};
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType.inputChange:
|
||||
return {
|
||||
...initialState,
|
||||
...state,
|
||||
[action.payload.name]: action.payload.value,
|
||||
};
|
||||
case ActionType.fetched:
|
||||
return {
|
||||
...initialState,
|
||||
...action.payload,
|
||||
};
|
||||
case ActionType.reset:
|
||||
return null;
|
||||
return { ...initialState };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -167,6 +172,11 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
...props
|
||||
}) => {
|
||||
const vizType = props.props.chart?.sliceFormData?.viz_type;
|
||||
const isChart = !!props.props.chart;
|
||||
const defaultNotificationFormat =
|
||||
isChart && TEXT_BASED_VISUALIZATION_TYPES.includes(vizType)
|
||||
? NOTIFICATION_FORMATS.TEXT
|
||||
: NOTIFICATION_FORMATS.PNG;
|
||||
const [currentReport, setCurrentReport] = useReducer<
|
||||
Reducer<Partial<ReportObject> | null, ReportActionType>
|
||||
>(reportReducer, null);
|
||||
@@ -179,6 +189,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
// Report fetch logic
|
||||
const reports = useSelector<any, AlertObject>(state => state.reports);
|
||||
const isEditMode = reports && Object.keys(reports).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode) {
|
||||
const reportsIds = Object.keys(reports);
|
||||
@@ -214,7 +225,8 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
type: 'Report',
|
||||
creation_method: props.props.creationMethod,
|
||||
active: true,
|
||||
report_format: currentReport?.report_format,
|
||||
report_format: currentReport?.report_format || defaultNotificationFormat,
|
||||
timezone: currentReport?.timezone,
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
@@ -270,17 +282,17 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
value: event.target.value,
|
||||
});
|
||||
}}
|
||||
value={currentReport?.report_format || DEFAULT_NOTIFICATION_FORMAT}
|
||||
value={currentReport?.report_format || defaultNotificationFormat}
|
||||
>
|
||||
{TEXT_BASED_VISUALIZATION_TYPES.includes(vizType) && (
|
||||
<StyledRadio value="TEXT">
|
||||
<StyledRadio value={NOTIFICATION_FORMATS.TEXT}>
|
||||
{t('Text embedded in email')}
|
||||
</StyledRadio>
|
||||
)}
|
||||
<StyledRadio value="PNG">
|
||||
<StyledRadio value={NOTIFICATION_FORMATS.PNG}>
|
||||
{t('Image (PNG) embedded in email')}
|
||||
</StyledRadio>
|
||||
<StyledRadio value="CSV">
|
||||
<StyledRadio value={NOTIFICATION_FORMATS.CSV}>
|
||||
{t('Formatted CSV attached in email')}
|
||||
</StyledRadio>
|
||||
</StyledRadioGroup>
|
||||
@@ -374,7 +386,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
|
||||
}}
|
||||
timezone={currentReport?.timezone}
|
||||
/>
|
||||
{props.props.chart && renderMessageContentSection}
|
||||
{isChart && renderMessageContentSection}
|
||||
</StyledBottomSection>
|
||||
</StyledModal>
|
||||
);
|
||||
|
||||
@@ -249,15 +249,8 @@ function styled<
|
||||
if (forceOverflow) {
|
||||
Object.assign(restProps, {
|
||||
closeMenuOnScroll: (e: Event) => {
|
||||
// ensure menu is open
|
||||
const menuIsOpen = (stateManager as BasicSelect<OptionType>)?.state
|
||||
?.menuIsOpen;
|
||||
const target = e.target as HTMLElement;
|
||||
return (
|
||||
menuIsOpen &&
|
||||
target &&
|
||||
!target.classList?.contains('Select__menu-list')
|
||||
);
|
||||
return target && !target.classList?.contains('Select__menu-list');
|
||||
},
|
||||
menuPosition: 'fixed',
|
||||
});
|
||||
|
||||
@@ -300,6 +300,7 @@ const USERS = [
|
||||
];
|
||||
|
||||
export const AsyncSelect = ({
|
||||
fetchOnlyOnSearch,
|
||||
withError,
|
||||
withInitialValue,
|
||||
responseTime,
|
||||
@@ -381,7 +382,9 @@ export const AsyncSelect = ({
|
||||
>
|
||||
<Select
|
||||
{...rest}
|
||||
fetchOnlyOnSearch={fetchOnlyOnSearch}
|
||||
options={withError ? fetchUserListError : fetchUserListPage}
|
||||
placeholder={fetchOnlyOnSearch ? 'Type anything' : 'Select...'}
|
||||
value={
|
||||
withInitialValue
|
||||
? { label: 'Valentina', value: 'Valentina' }
|
||||
|
||||
@@ -49,8 +49,12 @@ type PickedSelectProps = Pick<
|
||||
| 'autoFocus'
|
||||
| 'disabled'
|
||||
| 'filterOption'
|
||||
| 'labelInValue'
|
||||
| 'loading'
|
||||
| 'notFoundContent'
|
||||
| 'onChange'
|
||||
| 'onClear'
|
||||
| 'onFocus'
|
||||
| 'placeholder'
|
||||
| 'showSearch'
|
||||
| 'value'
|
||||
@@ -73,6 +77,7 @@ export interface SelectProps extends PickedSelectProps {
|
||||
allowNewOptions?: boolean;
|
||||
ariaLabel: string;
|
||||
header?: ReactNode;
|
||||
lazyLoading?: boolean;
|
||||
mode?: 'single' | 'multiple';
|
||||
name?: string; // discourage usage
|
||||
options: OptionsType | OptionsPagePromise;
|
||||
@@ -84,15 +89,11 @@ export interface SelectProps extends PickedSelectProps {
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(AntdSelect, {
|
||||
shouldForwardProp: prop => prop !== 'hasHeader',
|
||||
})<{ hasHeader: boolean }>`
|
||||
${({ theme, hasHeader }) => `
|
||||
width: 100%;
|
||||
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
|
||||
|
||||
const StyledSelect = styled(AntdSelect)`
|
||||
${({ theme }) => `
|
||||
&& .ant-select-selector {
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
}
|
||||
@@ -128,10 +129,22 @@ const StyledError = styled.div`
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledSpin = styled(Spin)`
|
||||
margin-top: ${({ theme }) => -theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
const StyledLoadingText = styled.span`
|
||||
${({ theme }) => `
|
||||
margin-left: ${theme.gridUnit * 3}px;
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
`}
|
||||
`;
|
||||
|
||||
const MAX_TAG_COUNT = 4;
|
||||
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
||||
const DEBOUNCE_TIMEOUT = 500;
|
||||
@@ -140,7 +153,7 @@ const EMPTY_OPTIONS: OptionsType = [];
|
||||
|
||||
const Error = ({ error }: { error: string }) => (
|
||||
<StyledError>
|
||||
<Icons.ErrorSolid /> {error}
|
||||
<Icons.ErrorSolid /> <StyledErrorMessage>{error}</StyledErrorMessage>
|
||||
</StyledError>
|
||||
);
|
||||
|
||||
@@ -151,8 +164,12 @@ const Select = ({
|
||||
filterOption = true,
|
||||
header = null,
|
||||
invertSelection = false,
|
||||
labelInValue = false,
|
||||
lazyLoading = true,
|
||||
loading = false,
|
||||
mode = 'single',
|
||||
name,
|
||||
onChange,
|
||||
options,
|
||||
pageSize = DEFAULT_PAGE_SIZE,
|
||||
placeholder = t('Select ...'),
|
||||
@@ -170,12 +187,13 @@ const Select = ({
|
||||
);
|
||||
const [selectValue, setSelectValue] = useState(value);
|
||||
const [searchedValue, setSearchedValue] = useState('');
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(loading);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||
const [page, setPage] = useState(0);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loadingEnabled, setLoadingEnabled] = useState(false);
|
||||
const [loadingEnabled, setLoadingEnabled] = useState(!lazyLoading);
|
||||
const fetchedQueries = useRef(new Map<string, number>());
|
||||
const mappedMode = isSingleMode
|
||||
? undefined
|
||||
@@ -184,16 +202,21 @@ const Select = ({
|
||||
: 'multiple';
|
||||
|
||||
useEffect(() => {
|
||||
fetchedQueries.current.clear();
|
||||
setSelectOptions(
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||
);
|
||||
}, [options]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAsync && value) {
|
||||
const array: AntdLabeledValue[] = Array.isArray(value)
|
||||
? (value as AntdLabeledValue[])
|
||||
: [value as AntdLabeledValue];
|
||||
setSelectValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAsync && selectValue) {
|
||||
const array: AntdLabeledValue[] = Array.isArray(selectValue)
|
||||
? (selectValue as AntdLabeledValue[])
|
||||
: [selectValue as AntdLabeledValue];
|
||||
const options: AntdLabeledValue[] = [];
|
||||
array.forEach(element => {
|
||||
const found = selectOptions.find(
|
||||
@@ -207,23 +230,20 @@ const Select = ({
|
||||
setSelectOptions([...selectOptions, ...options]);
|
||||
}
|
||||
}
|
||||
}, [isAsync, selectOptions, value]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectValue(value);
|
||||
}, [value]);
|
||||
}, [isAsync, selectOptions, selectValue]);
|
||||
|
||||
const handleTopOptions = useCallback(
|
||||
(selectedValue: AntdSelectValue | undefined) => {
|
||||
// bringing selected options to the top of the list
|
||||
if (selectedValue) {
|
||||
if (selectedValue !== undefined && selectedValue !== null) {
|
||||
const isLabeledValue = isAsync || labelInValue;
|
||||
const topOptions: OptionsType = [];
|
||||
const otherOptions: OptionsType = [];
|
||||
|
||||
selectOptions.forEach(opt => {
|
||||
let found = false;
|
||||
if (Array.isArray(selectedValue)) {
|
||||
if (isAsync) {
|
||||
if (isLabeledValue) {
|
||||
found =
|
||||
(selectedValue as AntdLabeledValue[]).find(
|
||||
element => element.value === opt.value,
|
||||
@@ -232,7 +252,7 @@ const Select = ({
|
||||
found = selectedValue.includes(opt.value);
|
||||
}
|
||||
} else {
|
||||
found = isAsync
|
||||
found = isLabeledValue
|
||||
? (selectedValue as AntdLabeledValue).value === opt.value
|
||||
: selectedValue === opt.value;
|
||||
}
|
||||
@@ -252,10 +272,10 @@ const Select = ({
|
||||
!topOptions.find(
|
||||
tOpt =>
|
||||
tOpt.value ===
|
||||
(isAsync ? (val as AntdLabeledValue)?.value : val),
|
||||
(isLabeledValue ? (val as AntdLabeledValue)?.value : val),
|
||||
)
|
||||
) {
|
||||
if (isAsync) {
|
||||
if (isLabeledValue) {
|
||||
const labelValue = val as AntdLabeledValue;
|
||||
topOptions.push({
|
||||
label: labelValue.label,
|
||||
@@ -275,7 +295,7 @@ const Select = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[isAsync, isSingleMode, selectOptions],
|
||||
[isAsync, isSingleMode, labelInValue, selectOptions],
|
||||
);
|
||||
|
||||
const handleOnSelect = (
|
||||
@@ -345,9 +365,10 @@ const Select = ({
|
||||
const cachedCount = fetchedQueries.current.get(key);
|
||||
if (cachedCount) {
|
||||
setTotalCount(cachedCount);
|
||||
setIsTyping(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setIsLoading(true);
|
||||
const fetchOptions = options as OptionsPagePromise;
|
||||
fetchOptions(value, page, pageSize)
|
||||
.then(({ data, totalCount }: OptionsTypePage) => {
|
||||
@@ -356,39 +377,61 @@ const Select = ({
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch(onError)
|
||||
.finally(() => setLoading(false));
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setIsTyping(false);
|
||||
});
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const handleOnSearch = debounce((search: string) => {
|
||||
const searchValue = search.trim();
|
||||
// enables option creation
|
||||
if (allowNewOptions && isSingleMode) {
|
||||
const firstOption = selectOptions.length > 0 && selectOptions[0].value;
|
||||
// replaces the last search value entered with the new one
|
||||
// only when the value wasn't part of the original options
|
||||
if (
|
||||
searchValue &&
|
||||
firstOption === searchedValue &&
|
||||
!initialOptions.find(o => o.value === searchedValue)
|
||||
) {
|
||||
selectOptions.shift();
|
||||
setSelectOptions(selectOptions);
|
||||
}
|
||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
};
|
||||
// adds a custom option
|
||||
const newOptions = [...selectOptions, newOption];
|
||||
setSelectOptions(newOptions);
|
||||
setSelectValue(searchValue);
|
||||
}
|
||||
}
|
||||
setSearchedValue(searchValue);
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
const handleOnSearch = useMemo(
|
||||
() =>
|
||||
debounce((search: string) => {
|
||||
const searchValue = search.trim();
|
||||
// enables option creation
|
||||
if (allowNewOptions && isSingleMode) {
|
||||
const firstOption =
|
||||
selectOptions.length > 0 && selectOptions[0].value;
|
||||
// replaces the last search value entered with the new one
|
||||
// only when the value wasn't part of the original options
|
||||
if (
|
||||
searchValue &&
|
||||
firstOption === searchedValue &&
|
||||
!initialOptions.find(o => o.value === searchedValue)
|
||||
) {
|
||||
selectOptions.shift();
|
||||
setSelectOptions(selectOptions);
|
||||
}
|
||||
if (searchValue && !hasOption(searchValue, selectOptions)) {
|
||||
const newOption = {
|
||||
label: searchValue,
|
||||
value: searchValue,
|
||||
};
|
||||
// adds a custom option
|
||||
const newOptions = [...selectOptions, newOption];
|
||||
setSelectOptions(newOptions);
|
||||
setSelectValue(searchValue);
|
||||
|
||||
if (onChange) {
|
||||
onChange(searchValue, newOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
setSearchedValue(searchValue);
|
||||
if (!searchValue) {
|
||||
setIsTyping(false);
|
||||
}
|
||||
}, DEBOUNCE_TIMEOUT),
|
||||
[
|
||||
allowNewOptions,
|
||||
initialOptions,
|
||||
isSingleMode,
|
||||
onChange,
|
||||
searchedValue,
|
||||
selectOptions,
|
||||
],
|
||||
);
|
||||
|
||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||
const vScroll = e.currentTarget;
|
||||
@@ -458,15 +501,30 @@ const Select = ({
|
||||
}
|
||||
}, [handleTopOptions, isSingleMode, selectValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== undefined && loading !== isLoading) {
|
||||
setIsLoading(loading);
|
||||
}
|
||||
}, [isLoading, loading]);
|
||||
|
||||
const dropdownRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) => {
|
||||
if (!isDropdownVisible) {
|
||||
originNode.ref?.current?.scrollTo({ top: 0 });
|
||||
}
|
||||
if ((isLoading && selectOptions.length === 0) || isTyping) {
|
||||
return <StyledLoadingText>{t('Loading...')}</StyledLoadingText>;
|
||||
}
|
||||
return error ? <Error error={error} /> : originNode;
|
||||
};
|
||||
|
||||
const onInputKeyDown = () => {
|
||||
if (isAsync && !isTyping) {
|
||||
setIsTyping(true);
|
||||
}
|
||||
};
|
||||
|
||||
const SuffixIcon = () => {
|
||||
if (isLoading) {
|
||||
return <StyledSpin size="small" />;
|
||||
@@ -481,20 +539,21 @@ const Select = ({
|
||||
<StyledContainer>
|
||||
{header}
|
||||
<StyledSelect
|
||||
hasHeader={!!header}
|
||||
aria-label={ariaLabel || name}
|
||||
dropdownRender={dropdownRender}
|
||||
filterOption={handleFilterOption}
|
||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||
labelInValue={isAsync}
|
||||
labelInValue={isAsync || labelInValue}
|
||||
maxTagCount={MAX_TAG_COUNT}
|
||||
mode={mappedMode}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onInputKeyDown={onInputKeyDown}
|
||||
onPopupScroll={isAsync ? handlePagination : undefined}
|
||||
onSearch={shouldShowSearch ? handleOnSearch : undefined}
|
||||
onSelect={handleOnSelect}
|
||||
onClear={() => setSelectValue(undefined)}
|
||||
onChange={onChange}
|
||||
options={selectOptions}
|
||||
placeholder={placeholder}
|
||||
showSearch={shouldShowSearch}
|
||||
|
||||
@@ -63,7 +63,7 @@ const TableViewStyles = styled.div<{
|
||||
${({ scrollTable, theme }) =>
|
||||
scrollTable &&
|
||||
`
|
||||
height: 380px;
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
overflow: auto;
|
||||
`}
|
||||
@@ -88,22 +88,24 @@ const TableViewStyles = styled.div<{
|
||||
`${theme.gridUnit - 2}px solid ${theme.colors.grayscale.light2}`};
|
||||
${({ small }) => small && `padding-bottom: 0;`}
|
||||
}
|
||||
`;
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
const PaginationStyles = styled.div<{
|
||||
isPaginationSticky?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
|
||||
${({ isPaginationSticky }) =>
|
||||
isPaginationSticky &&
|
||||
`
|
||||
${({ isPaginationSticky }) =>
|
||||
isPaginationSticky &&
|
||||
`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`};
|
||||
}
|
||||
|
||||
.row-count-container {
|
||||
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
@@ -188,32 +190,38 @@ const TableView = ({
|
||||
}
|
||||
|
||||
const isEmpty = !loading && content.length === 0;
|
||||
const hasPagination = pageCount > 1 && withPagination;
|
||||
|
||||
return (
|
||||
<TableViewStyles {...props}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<EmptyWrapperComponent>
|
||||
{noDataText ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={noDataText}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</EmptyWrapperComponent>
|
||||
)}
|
||||
{pageCount > 1 && withPagination && (
|
||||
<div className="pagination-container">
|
||||
<>
|
||||
<TableViewStyles {...props}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<EmptyWrapperComponent>
|
||||
{noDataText ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={noDataText}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</EmptyWrapperComponent>
|
||||
)}
|
||||
</TableViewStyles>
|
||||
{hasPagination && (
|
||||
<PaginationStyles
|
||||
className="pagination-container"
|
||||
isPaginationSticky={props.isPaginationSticky}
|
||||
>
|
||||
<Pagination
|
||||
totalPages={pageCount || 0}
|
||||
currentPage={pageCount ? pageIndex + 1 : 0}
|
||||
@@ -231,9 +239,9 @@ const TableView = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PaginationStyles>
|
||||
)}
|
||||
</TableViewStyles>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { NativeGraySelect as Select } from 'src/components/Select';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select } from 'src/components';
|
||||
|
||||
const DEFAULT_TIMEZONE = 'GMT Standard Time';
|
||||
const MIN_SELECT_WIDTH = '400px';
|
||||
@@ -92,12 +92,6 @@ const TIMEZONE_OPTIONS = TIMEZONES.sort(
|
||||
offsets: getOffsetKey(zone.name),
|
||||
}));
|
||||
|
||||
const timezoneOptions = TIMEZONE_OPTIONS.map(option => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
));
|
||||
|
||||
const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
|
||||
const prevTimezone = useRef(timezone);
|
||||
const matchTimezoneToOptions = (timezone: string) =>
|
||||
@@ -120,12 +114,12 @@ const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
|
||||
|
||||
return (
|
||||
<Select
|
||||
ariaLabel={t('Timezone')}
|
||||
css={{ minWidth: MIN_SELECT_WIDTH }} // smallest size for current values
|
||||
onChange={onTimezoneChange}
|
||||
value={timezone || DEFAULT_TIMEZONE}
|
||||
>
|
||||
{timezoneOptions}
|
||||
</Select>
|
||||
options={TIMEZONE_OPTIONS}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,17 +17,32 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useTheme } from '@superset-ui/core';
|
||||
import { useTheme, css } from '@superset-ui/core';
|
||||
import { Tooltip as AntdTooltip } from 'antd';
|
||||
import { TooltipProps } from 'antd/lib/tooltip';
|
||||
import { Global } from '@emotion/react';
|
||||
|
||||
export const Tooltip = (props: TooltipProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<AntdTooltip
|
||||
overlayStyle={{ fontSize: theme.typography.sizes.s, lineHeight: '1.6' }}
|
||||
color={`${theme.colors.grayscale.dark2}e6`}
|
||||
{...props}
|
||||
/>
|
||||
<>
|
||||
{/* Safari hack to hide browser default tooltips */}
|
||||
<Global
|
||||
styles={css`
|
||||
.ant-tooltip-open {
|
||||
display: inline-block;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<AntdTooltip
|
||||
overlayStyle={{ fontSize: theme.typography.sizes.s, lineHeight: '1.6' }}
|
||||
color={`${theme.colors.grayscale.dark2}e6`}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -371,8 +371,8 @@ export const hydrateDashboard = (dashboardData, chartData) => (
|
||||
// only persistent refreshFrequency will be saved to backend
|
||||
shouldPersistRefreshFrequency: false,
|
||||
css: dashboardData.css || '',
|
||||
colorNamespace: metadata?.color_namespace || null,
|
||||
colorScheme: metadata?.color_scheme || null,
|
||||
colorNamespace: metadata?.color_namespace,
|
||||
colorScheme: metadata?.color_scheme,
|
||||
editMode: canEdit && editMode,
|
||||
isPublished: dashboardData.published,
|
||||
hasUnsavedChanges: false,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor';
|
||||
import { AceEditorProps } from 'react-ace';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -32,6 +32,12 @@ jest.mock('src/components/AsyncAceEditor', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const templates = [
|
||||
{ label: 'Template A', css: 'background-color: red;' },
|
||||
{ label: 'Template B', css: 'background-color: blue;' },
|
||||
{ label: 'Template C', css: 'background-color: yellow;' },
|
||||
];
|
||||
|
||||
AceCssEditor.preload = () => new Promise(() => {});
|
||||
|
||||
test('renders with default props', () => {
|
||||
@@ -46,14 +52,15 @@ test('renders with initial CSS', () => {
|
||||
expect(screen.getByText(initialCss)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with templates', () => {
|
||||
const templates = ['Template A', 'Template B', 'Template C'];
|
||||
test('renders with templates', async () => {
|
||||
render(<CssEditor triggerNode={<>Click</>} templates={templates} />);
|
||||
userEvent.click(screen.getByRole('button', { name: 'Click' }));
|
||||
userEvent.click(screen.getByText('Load a CSS template'));
|
||||
templates.forEach(template =>
|
||||
expect(screen.getByText(template)).toBeInTheDocument(),
|
||||
);
|
||||
userEvent.hover(screen.getByText('Load a CSS template'));
|
||||
await waitFor(() => {
|
||||
templates.forEach(template =>
|
||||
expect(screen.getByText(template.label)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('triggers onChange when using the editor', () => {
|
||||
@@ -73,9 +80,8 @@ test('triggers onChange when using the editor', () => {
|
||||
expect(onChange).toHaveBeenLastCalledWith(initialCss.concat(additionalCss));
|
||||
});
|
||||
|
||||
test('triggers onChange when selecting a template', () => {
|
||||
test('triggers onChange when selecting a template', async () => {
|
||||
const onChange = jest.fn();
|
||||
const templates = ['Template A', 'Template B', 'Template C'];
|
||||
render(
|
||||
<CssEditor
|
||||
triggerNode={<>Click</>}
|
||||
@@ -86,6 +92,6 @@ test('triggers onChange when selecting a template', () => {
|
||||
userEvent.click(screen.getByRole('button', { name: 'Click' }));
|
||||
userEvent.click(screen.getByText('Load a CSS template'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
userEvent.click(screen.getByText('Template A'));
|
||||
userEvent.click(await screen.findByText('Template A'));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -18,11 +18,30 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'src/components/Select';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Menu, Dropdown } from 'src/common/components';
|
||||
import Button from 'src/components/Button';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
${({ theme }) => `
|
||||
.css-editor-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
|
||||
h5 {
|
||||
margin-top: ${theme.gridUnit}px;
|
||||
}
|
||||
}
|
||||
.css-editor {
|
||||
border: 1px solid ${theme.colors.grayscale.light1};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const propTypes = {
|
||||
initialCss: PropTypes.string,
|
||||
triggerNode: PropTypes.node.isRequired,
|
||||
@@ -55,21 +74,24 @@ class CssEditor extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
changeCssTemplate(opt) {
|
||||
this.changeCss(opt.css);
|
||||
changeCssTemplate({ key }) {
|
||||
this.changeCss(key);
|
||||
}
|
||||
|
||||
renderTemplateSelector() {
|
||||
if (this.props.templates) {
|
||||
const menu = (
|
||||
<Menu onClick={this.changeCssTemplate}>
|
||||
{this.props.templates.map(template => (
|
||||
<Menu.Item key={template.css}>{template.label}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ zIndex: 10 }}>
|
||||
<h5>{t('Load a template')}</h5>
|
||||
<Select
|
||||
options={this.props.templates}
|
||||
placeholder={t('Load a CSS template')}
|
||||
onChange={this.changeCssTemplate}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown overlay={menu} placement="bottomRight">
|
||||
<Button>{t('Load a CSS template')}</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -81,24 +103,23 @@ class CssEditor extends React.PureComponent {
|
||||
triggerNode={this.props.triggerNode}
|
||||
modalTitle={t('CSS')}
|
||||
modalBody={
|
||||
<div>
|
||||
{this.renderTemplateSelector()}
|
||||
<div style={{ zIndex: 1 }}>
|
||||
<StyledWrapper>
|
||||
<div className="css-editor-header">
|
||||
<h5>{t('Live CSS editor')}</h5>
|
||||
<div style={{ border: 'solid 1px grey' }}>
|
||||
<AceCssEditor
|
||||
minLines={12}
|
||||
maxLines={30}
|
||||
onChange={this.changeCss}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={this.state.css || ''}
|
||||
/>
|
||||
</div>
|
||||
{this.renderTemplateSelector()}
|
||||
</div>
|
||||
</div>
|
||||
<AceCssEditor
|
||||
className="css-editor"
|
||||
minLines={12}
|
||||
maxLines={30}
|
||||
onChange={this.changeCss}
|
||||
height="200px"
|
||||
width="100%"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
value={this.state.css || ''}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -289,7 +289,7 @@ class Dashboard extends React.PureComponent {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<OmniContainer logEvent={this.props.actions.logEvent} />
|
||||
<OmniContainer />
|
||||
<DashboardBuilder />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
/* eslint-env browser */
|
||||
import cx from 'classnames';
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { JsonObject, styled, css } from '@superset-ui/core';
|
||||
import ErrorBoundary from 'src/components/ErrorBoundary';
|
||||
import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane';
|
||||
@@ -157,15 +157,14 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
||||
state => state.dashboardState.fullSizeChartId,
|
||||
);
|
||||
|
||||
const handleChangeTab = ({
|
||||
pathToTabIndex,
|
||||
}: {
|
||||
pathToTabIndex: string[];
|
||||
}) => {
|
||||
dispatch(setDirectPathToChild(pathToTabIndex));
|
||||
};
|
||||
const handleChangeTab = useCallback(
|
||||
({ pathToTabIndex }: { pathToTabIndex: string[] }) => {
|
||||
dispatch(setDirectPathToChild(pathToTabIndex));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleDeleteTopLevelTabs = () => {
|
||||
const handleDeleteTopLevelTabs = useCallback(() => {
|
||||
dispatch(deleteTopLevelTabs());
|
||||
|
||||
const firstTab = getDirectPathToTabIndex(
|
||||
@@ -173,7 +172,12 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
||||
0,
|
||||
);
|
||||
dispatch(setDirectPathToChild(firstTab));
|
||||
};
|
||||
}, [dashboardLayout, dispatch]);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
dropResult => dispatch(handleComponentDrop(dropResult)),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID];
|
||||
const rootChildId = dashboardRoot.children[0];
|
||||
@@ -217,6 +221,54 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
||||
const filterBarHeight = `calc(100vh - ${offset}px)`;
|
||||
const filterBarOffset = dashboardFiltersOpen ? 0 : barTopOffset + 20;
|
||||
|
||||
const draggableStyle = useMemo(
|
||||
() => ({
|
||||
marginLeft: dashboardFiltersOpen || editMode ? 0 : -32,
|
||||
}),
|
||||
[dashboardFiltersOpen, editMode],
|
||||
);
|
||||
|
||||
const renderDraggableContent = useCallback(
|
||||
({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
|
||||
<div>
|
||||
{!hideDashboardHeader && <DashboardHeader />}
|
||||
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
||||
{!isReport && topLevelTabs && (
|
||||
<WithPopoverMenu
|
||||
shouldFocus={shouldFocusTabs}
|
||||
menuItems={[
|
||||
<IconButton
|
||||
icon={<Icons.FallOutlined iconSize="xl" />}
|
||||
label="Collapse tab content"
|
||||
onClick={handleDeleteTopLevelTabs}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
<DashboardComponent
|
||||
id={topLevelTabs?.id}
|
||||
parentId={DASHBOARD_ROOT_ID}
|
||||
depth={DASHBOARD_ROOT_DEPTH + 1}
|
||||
index={0}
|
||||
renderTabContent={false}
|
||||
renderHoverMenu={false}
|
||||
onChangeTab={handleChangeTab}
|
||||
/>
|
||||
</WithPopoverMenu>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
editMode,
|
||||
handleChangeTab,
|
||||
handleDeleteTopLevelTabs,
|
||||
hideDashboardHeader,
|
||||
isReport,
|
||||
topLevelTabs,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv>
|
||||
{nativeFiltersEnabled && !editMode && (
|
||||
@@ -244,45 +296,13 @@ const DashboardBuilder: FC<DashboardBuilderProps> = () => {
|
||||
depth={DASHBOARD_ROOT_DEPTH}
|
||||
index={0}
|
||||
orientation="column"
|
||||
onDrop={dropResult => dispatch(handleComponentDrop(dropResult))}
|
||||
onDrop={handleDrop}
|
||||
editMode={editMode}
|
||||
// you cannot drop on/displace tabs if they already exist
|
||||
disableDragDrop={!!topLevelTabs}
|
||||
style={{
|
||||
marginLeft: dashboardFiltersOpen || editMode ? 0 : -32,
|
||||
}}
|
||||
style={draggableStyle}
|
||||
>
|
||||
{({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => (
|
||||
<div>
|
||||
{!hideDashboardHeader && <DashboardHeader />}
|
||||
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
||||
{!isReport && topLevelTabs && (
|
||||
<WithPopoverMenu
|
||||
shouldFocus={shouldFocusTabs}
|
||||
menuItems={[
|
||||
<IconButton
|
||||
icon={<Icons.FallOutlined iconSize="xl" />}
|
||||
label="Collapse tab content"
|
||||
onClick={handleDeleteTopLevelTabs}
|
||||
/>,
|
||||
]}
|
||||
editMode={editMode}
|
||||
>
|
||||
{/*
|
||||
// @ts-ignore */}
|
||||
<DashboardComponent
|
||||
id={topLevelTabs?.id}
|
||||
parentId={DASHBOARD_ROOT_ID}
|
||||
depth={DASHBOARD_ROOT_DEPTH + 1}
|
||||
index={0}
|
||||
renderTabContent={false}
|
||||
renderHoverMenu={false}
|
||||
onChangeTab={handleChangeTab}
|
||||
/>
|
||||
</WithPopoverMenu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderDraggableContent}
|
||||
</DragDroppable>
|
||||
</StyledHeader>
|
||||
<StyledContent fullSizeChartId={fullSizeChartId}>
|
||||
|
||||
@@ -38,6 +38,16 @@ const propTypes = {
|
||||
|
||||
const defaultProps = {};
|
||||
|
||||
const renderDraggableContentBottom = dropProps =>
|
||||
dropProps.dropIndicatorProps && (
|
||||
<div className="drop-indicator drop-indicator--bottom" />
|
||||
);
|
||||
|
||||
const renderDraggableContentTop = dropProps =>
|
||||
dropProps.dropIndicatorProps && (
|
||||
<div className="drop-indicator drop-indicator--top" />
|
||||
);
|
||||
|
||||
class DashboardGrid extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -144,11 +154,7 @@ class DashboardGrid extends React.PureComponent {
|
||||
className="empty-droptarget"
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && (
|
||||
<div className="drop-indicator drop-indicator--bottom" />
|
||||
)
|
||||
}
|
||||
{renderDraggableContentBottom}
|
||||
</DragDroppable>
|
||||
)}
|
||||
|
||||
@@ -181,11 +187,7 @@ class DashboardGrid extends React.PureComponent {
|
||||
className="empty-droptarget"
|
||||
editMode
|
||||
>
|
||||
{({ dropIndicatorProps }) =>
|
||||
dropIndicatorProps && (
|
||||
<div className="drop-indicator drop-indicator--top" />
|
||||
)
|
||||
}
|
||||
{renderDraggableContentTop}
|
||||
</DragDroppable>
|
||||
)}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ const sortByStatus = (indicators: Indicator[]): Indicator[] => {
|
||||
);
|
||||
};
|
||||
|
||||
const indicatorsInitialState: Indicator[] = [];
|
||||
|
||||
export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const datasources = useSelector<RootState, any>(state => state.datasources);
|
||||
@@ -77,9 +79,11 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
||||
state => state.dataMask,
|
||||
);
|
||||
|
||||
const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>([]);
|
||||
const [nativeIndicators, setNativeIndicators] = useState<Indicator[]>(
|
||||
indicatorsInitialState,
|
||||
);
|
||||
const [dashboardIndicators, setDashboardIndicators] = useState<Indicator[]>(
|
||||
[],
|
||||
indicatorsInitialState,
|
||||
);
|
||||
|
||||
const onHighlightFilterSource = useCallback(
|
||||
@@ -90,46 +94,79 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
||||
);
|
||||
|
||||
const chart = charts[chartId];
|
||||
const prevChartStatus = usePrevious(chart?.chartStatus);
|
||||
const prevChart = usePrevious(chart);
|
||||
const prevChartStatus = prevChart?.chartStatus;
|
||||
const prevDashboardFilters = usePrevious(dashboardFilters);
|
||||
const prevDatasources = usePrevious(datasources);
|
||||
const showIndicators =
|
||||
chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus);
|
||||
|
||||
const showIndicators = useCallback(
|
||||
() =>
|
||||
chart?.chartStatus && ['rendered', 'success'].includes(chart.chartStatus),
|
||||
[chart.chartStatus],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!showIndicators) {
|
||||
setDashboardIndicators([]);
|
||||
}
|
||||
if (prevChartStatus !== 'success') {
|
||||
setDashboardIndicators(
|
||||
selectIndicatorsForChart(chartId, dashboardFilters, datasources, chart),
|
||||
);
|
||||
if (!showIndicators && dashboardIndicators.length > 0) {
|
||||
setDashboardIndicators(indicatorsInitialState);
|
||||
} else if (prevChartStatus !== 'success') {
|
||||
if (
|
||||
chart?.queriesResponse?.[0]?.rejected_filters !==
|
||||
prevChart?.queriesResponse?.[0]?.rejected_filters ||
|
||||
chart?.queriesResponse?.[0]?.applied_filters !==
|
||||
prevChart?.queriesResponse?.[0]?.applied_filters ||
|
||||
dashboardFilters !== prevDashboardFilters ||
|
||||
datasources !== prevDatasources
|
||||
) {
|
||||
setDashboardIndicators(
|
||||
selectIndicatorsForChart(
|
||||
chartId,
|
||||
dashboardFilters,
|
||||
datasources,
|
||||
chart,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
chart,
|
||||
chartId,
|
||||
dashboardFilters,
|
||||
dashboardIndicators.length,
|
||||
datasources,
|
||||
prevChart?.queriesResponse,
|
||||
prevChartStatus,
|
||||
prevDashboardFilters,
|
||||
prevDatasources,
|
||||
showIndicators,
|
||||
]);
|
||||
|
||||
const prevNativeFilters = usePrevious(nativeFilters);
|
||||
const prevDashboardLayout = usePrevious(present);
|
||||
const prevDataMask = usePrevious(dataMask);
|
||||
const prevChartConfig = usePrevious(
|
||||
dashboardInfo.metadata?.chart_configuration,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!showIndicators) {
|
||||
setNativeIndicators([]);
|
||||
}
|
||||
if (prevChartStatus !== 'success') {
|
||||
setNativeIndicators(
|
||||
selectNativeIndicatorsForChart(
|
||||
nativeFilters,
|
||||
dataMask,
|
||||
chartId,
|
||||
chart,
|
||||
present,
|
||||
dashboardInfo.metadata?.chart_configuration,
|
||||
),
|
||||
);
|
||||
if (!showIndicators && nativeIndicators.length > 0) {
|
||||
setNativeIndicators(indicatorsInitialState);
|
||||
} else if (prevChartStatus !== 'success') {
|
||||
if (
|
||||
chart?.queriesResponse?.[0]?.rejected_filters !==
|
||||
prevChart?.queriesResponse?.[0]?.rejected_filters ||
|
||||
chart?.queriesResponse?.[0]?.applied_filters !==
|
||||
prevChart?.queriesResponse?.[0]?.applied_filters ||
|
||||
nativeFilters !== prevNativeFilters ||
|
||||
present !== prevDashboardLayout ||
|
||||
dataMask !== prevDataMask ||
|
||||
prevChartConfig !== dashboardInfo.metadata?.chart_configuration
|
||||
) {
|
||||
setNativeIndicators(
|
||||
selectNativeIndicatorsForChart(
|
||||
nativeFilters,
|
||||
dataMask,
|
||||
chartId,
|
||||
chart,
|
||||
present,
|
||||
dashboardInfo.metadata?.chart_configuration,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
chart,
|
||||
@@ -137,8 +174,14 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
||||
dashboardInfo.metadata?.chart_configuration,
|
||||
dataMask,
|
||||
nativeFilters,
|
||||
nativeIndicators.length,
|
||||
present,
|
||||
prevChart?.queriesResponse,
|
||||
prevChartConfig,
|
||||
prevChartStatus,
|
||||
prevDashboardLayout,
|
||||
prevDataMask,
|
||||
prevNativeFilters,
|
||||
showIndicators,
|
||||
]);
|
||||
|
||||
@@ -155,17 +198,33 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
||||
[dashboardIndicators, nativeIndicators],
|
||||
);
|
||||
|
||||
const appliedCrossFilterIndicators = indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
|
||||
const appliedCrossFilterIndicators = useMemo(
|
||||
() =>
|
||||
indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.CrossFilterApplied,
|
||||
),
|
||||
[indicators],
|
||||
);
|
||||
const appliedIndicators = indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Applied,
|
||||
const appliedIndicators = useMemo(
|
||||
() =>
|
||||
indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Applied,
|
||||
),
|
||||
[indicators],
|
||||
);
|
||||
const unsetIndicators = indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Unset,
|
||||
const unsetIndicators = useMemo(
|
||||
() =>
|
||||
indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Unset,
|
||||
),
|
||||
[indicators],
|
||||
);
|
||||
const incompatibleIndicators = indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Incompatible,
|
||||
const incompatibleIndicators = useMemo(
|
||||
() =>
|
||||
indicators.filter(
|
||||
indicator => indicator.status === IndicatorStatus.Incompatible,
|
||||
),
|
||||
[indicators],
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
|
||||
import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
|
||||
import {
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
FilterState,
|
||||
isFeatureEnabled,
|
||||
} from '@superset-ui/core';
|
||||
import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import { ChartConfiguration, Filters } from 'src/dashboard/reducers/types';
|
||||
import { DataMaskStateWithId, DataMaskType } from 'src/dataMask/types';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { Layout } from '../../types';
|
||||
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
|
||||
|
||||
@@ -154,6 +155,8 @@ export type Indicator = {
|
||||
path?: string[];
|
||||
};
|
||||
|
||||
const cachedIndicatorsForChart = {};
|
||||
const cachedDashboardFilterDataForChart = {};
|
||||
// inspects redux state to find what the filter indicators should be shown for a given chart
|
||||
export const selectIndicatorsForChart = (
|
||||
chartId: number,
|
||||
@@ -165,37 +168,75 @@ export const selectIndicatorsForChart = (
|
||||
// so grab the columns from the applied/rejected filters
|
||||
const appliedColumns = getAppliedColumns(chart);
|
||||
const rejectedColumns = getRejectedColumns(chart);
|
||||
const matchingFilters = Object.values(filters).filter(
|
||||
filter => filter.chartId !== chartId,
|
||||
);
|
||||
const matchingDatasources = Object.entries(datasources)
|
||||
.filter(([key]) =>
|
||||
matchingFilters.find(filter => filter.datasourceId === key),
|
||||
)
|
||||
.map(([, datasource]) => datasource);
|
||||
|
||||
const indicators = Object.values(filters)
|
||||
.filter(filter => filter.chartId !== chartId)
|
||||
.reduce(
|
||||
(acc, filter) =>
|
||||
acc.concat(
|
||||
selectIndicatorsForChartFromFilter(
|
||||
chartId,
|
||||
filter,
|
||||
datasources[filter.datasourceId] || {},
|
||||
appliedColumns,
|
||||
rejectedColumns,
|
||||
),
|
||||
const cachedFilterData = cachedDashboardFilterDataForChart[chartId];
|
||||
if (
|
||||
cachedIndicatorsForChart[chartId] &&
|
||||
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
|
||||
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
|
||||
areObjectsEqual(cachedFilterData?.matchingFilters, matchingFilters) &&
|
||||
areObjectsEqual(cachedFilterData?.matchingDatasources, matchingDatasources)
|
||||
) {
|
||||
return cachedIndicatorsForChart[chartId];
|
||||
}
|
||||
const indicators = matchingFilters.reduce(
|
||||
(acc, filter) =>
|
||||
acc.concat(
|
||||
selectIndicatorsForChartFromFilter(
|
||||
chartId,
|
||||
filter,
|
||||
datasources[filter.datasourceId] || {},
|
||||
appliedColumns,
|
||||
rejectedColumns,
|
||||
),
|
||||
[] as Indicator[],
|
||||
);
|
||||
),
|
||||
[] as Indicator[],
|
||||
);
|
||||
indicators.sort((a, b) => a.name.localeCompare(b.name));
|
||||
cachedIndicatorsForChart[chartId] = indicators;
|
||||
cachedDashboardFilterDataForChart[chartId] = {
|
||||
appliedColumns,
|
||||
rejectedColumns,
|
||||
matchingFilters,
|
||||
matchingDatasources,
|
||||
};
|
||||
return indicators;
|
||||
};
|
||||
|
||||
const cachedNativeIndicatorsForChart = {};
|
||||
let cachedNativeFilterDataForChart: any = {};
|
||||
const defaultChartConfig = {};
|
||||
export const selectNativeIndicatorsForChart = (
|
||||
nativeFilters: Filters,
|
||||
dataMask: DataMaskStateWithId,
|
||||
chartId: number,
|
||||
chart: any,
|
||||
dashboardLayout: Layout,
|
||||
chartConfiguration: ChartConfiguration = {},
|
||||
chartConfiguration: ChartConfiguration = defaultChartConfig,
|
||||
): Indicator[] => {
|
||||
const appliedColumns = getAppliedColumns(chart);
|
||||
const rejectedColumns = getRejectedColumns(chart);
|
||||
|
||||
const cachedFilterData = cachedNativeFilterDataForChart[chartId];
|
||||
if (
|
||||
cachedNativeIndicatorsForChart[chartId] &&
|
||||
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
|
||||
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
|
||||
cachedNativeFilterDataForChart?.nativeFilters === nativeFilters &&
|
||||
cachedNativeFilterDataForChart?.dashboardLayout === dashboardLayout &&
|
||||
cachedNativeFilterDataForChart?.chartConfiguration === chartConfiguration &&
|
||||
cachedNativeFilterDataForChart?.dataMask === dataMask
|
||||
) {
|
||||
return cachedNativeIndicatorsForChart[chartId];
|
||||
}
|
||||
const getStatus = ({
|
||||
label,
|
||||
column,
|
||||
@@ -283,5 +324,18 @@ export const selectNativeIndicatorsForChart = (
|
||||
})
|
||||
.filter(filter => filter.status === IndicatorStatus.CrossFilterApplied);
|
||||
}
|
||||
return crossFilterIndicators.concat(nativeFilterIndicators);
|
||||
const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
|
||||
cachedNativeIndicatorsForChart[chartId] = indicators;
|
||||
cachedNativeFilterDataForChart = {
|
||||
...cachedNativeFilterDataForChart,
|
||||
nativeFilters,
|
||||
dashboardLayout,
|
||||
chartConfiguration,
|
||||
dataMask,
|
||||
};
|
||||
cachedNativeFilterDataForChart[chartId] = {
|
||||
appliedColumns,
|
||||
rejectedColumns,
|
||||
};
|
||||
return indicators;
|
||||
};
|
||||
|
||||