Compare commits
58 Commits
sidecar
...
remove-fav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2355c7162 | ||
|
|
320cc6831c | ||
|
|
133e686224 | ||
|
|
7d0a472d1e | ||
|
|
c2534f9155 | ||
|
|
088ecdd0bf | ||
|
|
e1a2e4843a | ||
|
|
15e8ffee1e | ||
|
|
19ddcb7e5c | ||
|
|
36daa2dc3f | ||
|
|
7fd5a7668b | ||
|
|
95333e34b1 | ||
|
|
a9fb853e3e | ||
|
|
dea9068647 | ||
|
|
3416bd1479 | ||
|
|
e729b2dbb4 | ||
|
|
06261f262b | ||
|
|
454ed1883f | ||
|
|
b42060c880 | ||
|
|
7bf16d805d | ||
|
|
529adebe1b | ||
|
|
eb4351af83 | ||
|
|
5a2411fa64 | ||
|
|
078c1701f4 | ||
|
|
a7d349a5c6 | ||
|
|
7a20a65a4d | ||
|
|
912ed2ba80 | ||
|
|
fedb3ca941 | ||
|
|
42b15b6840 | ||
|
|
2f64343186 | ||
|
|
65376c7baf | ||
|
|
4c2b27e7f0 | ||
|
|
15e4e8df94 | ||
|
|
c5f220a9ff | ||
|
|
b05def1a8a | ||
|
|
da7f6efea8 | ||
|
|
1c2b9db4f0 | ||
|
|
0fce5ecfa5 | ||
|
|
385471c34d | ||
|
|
bef1f4d045 | ||
|
|
5a3182ce21 | ||
|
|
9efb80dbf4 | ||
|
|
a20b236809 | ||
|
|
4e969d19d1 | ||
|
|
876257fb94 | ||
|
|
472e599f91 | ||
|
|
d826e90395 | ||
|
|
c65cb284e6 | ||
|
|
bc54b7970a | ||
|
|
ce74ae095d | ||
|
|
9424538bb1 | ||
|
|
031fb4b5a8 | ||
|
|
7fb7ac8bef | ||
|
|
569a7b33a5 | ||
|
|
59df0d6f15 | ||
|
|
2e4ccffc11 | ||
|
|
2e51d02806 | ||
|
|
8406a827dd |
14
.github/CODEOWNERS
vendored
@@ -33,10 +33,10 @@
|
||||
|
||||
# Notify PMC members of changes to extension-related files
|
||||
|
||||
/superset-core/ @michael-s-molina @villebro
|
||||
/superset-extensions-cli/ @michael-s-molina @villebro
|
||||
/superset/core/ @michael-s-molina @villebro
|
||||
/superset/extensions/ @michael-s-molina @villebro
|
||||
/superset-frontend/src/packages/superset-core/ @michael-s-molina @villebro
|
||||
/superset-frontend/src/core/ @michael-s-molina @villebro
|
||||
/superset-frontend/src/extensions/ @michael-s-molina @villebro
|
||||
/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset-extensions-cli/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset-frontend/src/packages/superset-core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset-frontend/src/core/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
/superset-frontend/src/extensions/ @michael-s-molina @villebro @geido @eschutho @rusackas @kgabryje
|
||||
|
||||
13
LLMS.md
@@ -136,6 +136,19 @@ curl -f http://localhost:8088/health || echo "❌ Setup required - see https://s
|
||||
- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
|
||||
- **Example**: `~Model.is_active` instead of `Model.is_active == False`
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
**When creating pull requests:**
|
||||
|
||||
1. **Read the current PR template**: Always check `.github/PULL_REQUEST_TEMPLATE.md` for the latest format
|
||||
2. **Use the template sections**: Include all sections from the template (SUMMARY, BEFORE/AFTER, TESTING INSTRUCTIONS, ADDITIONAL INFORMATION)
|
||||
3. **Follow PR title conventions**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
- Format: `type(scope): description`
|
||||
- Example: `fix(dashboard): load charts correctly`
|
||||
- Types: `fix`, `feat`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
|
||||
|
||||
**Important**: Always reference the actual template file at `.github/PULL_REQUEST_TEMPLATE.md` instead of using cached content, as the template may be updated over time.
|
||||
|
||||
## Pre-commit Validation
|
||||
|
||||
**Use pre-commit hooks for quality validation:**
|
||||
|
||||
@@ -23,6 +23,8 @@ This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
|
||||
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
|
||||
- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
|
||||
|
||||
@@ -162,8 +162,11 @@ services:
|
||||
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
|
||||
# configuring the dev-server to use the host.docker.internal to connect to the backend
|
||||
superset: "http://superset-light:8088"
|
||||
# Webpack dev server configuration
|
||||
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-127.0.0.1}"
|
||||
WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}"
|
||||
ports:
|
||||
- "127.0.0.1:${NODE_PORT:-9001}:9000" # Parameterized port
|
||||
- "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces
|
||||
command: ["/app/docker/docker-frontend.sh"]
|
||||
env_file:
|
||||
- path: docker/.env # default
|
||||
|
||||
@@ -138,7 +138,7 @@ try:
|
||||
from superset_config_docker import * # noqa: F403
|
||||
|
||||
logger.info(
|
||||
f"Loaded your Docker configuration at [{superset_config_docker.__file__}]"
|
||||
"Loaded your Docker configuration at [%s]", superset_config_docker.__file__
|
||||
)
|
||||
except ImportError:
|
||||
logger.info("Using default Docker config...")
|
||||
|
||||
@@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### Keycloak-Specific Configuration using Flask-OIDC
|
||||
|
||||
If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option.
|
||||
|
||||
Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency.
|
||||
|
||||
The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file.
|
||||
|
||||
```python
|
||||
from flask_appbuilder.security.manager import AUTH_OID
|
||||
from superset.security import SupersetSecurityManager
|
||||
from flask_oidc import OpenIDConnect
|
||||
from flask_appbuilder.security.views import AuthOIDView
|
||||
from flask_login import login_user
|
||||
from urllib.parse import quote
|
||||
from flask_appbuilder.views import ModelView, SimpleFormView, expose
|
||||
from flask import (
|
||||
redirect,
|
||||
request
|
||||
)
|
||||
import logging
|
||||
|
||||
class OIDCSecurityManager(SupersetSecurityManager):
|
||||
|
||||
def __init__(self, appbuilder):
|
||||
super(OIDCSecurityManager, self).__init__(appbuilder)
|
||||
if self.auth_type == AUTH_OID:
|
||||
self.oid = OpenIDConnect(self.appbuilder.get_app)
|
||||
self.authoidview = AuthOIDCView
|
||||
|
||||
class AuthOIDCView(AuthOIDView):
|
||||
|
||||
@expose('/login/', methods=['GET', 'POST'])
|
||||
def login(self, flag=True):
|
||||
sm = self.appbuilder.sm
|
||||
oidc = sm.oid
|
||||
|
||||
@self.appbuilder.sm.oid.require_login
|
||||
def handle_login():
|
||||
user = sm.auth_user_oid(oidc.user_getfield('email'))
|
||||
|
||||
if user is None:
|
||||
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
|
||||
user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'),
|
||||
info.get('email'), sm.find_role('Gamma'))
|
||||
|
||||
login_user(user, remember=False)
|
||||
return redirect(self.appbuilder.get_url_for_index)
|
||||
|
||||
return handle_login()
|
||||
|
||||
@expose('/logout/', methods=['GET', 'POST'])
|
||||
def logout(self):
|
||||
oidc = self.appbuilder.sm.oid
|
||||
|
||||
oidc.logout()
|
||||
super(AuthOIDCView, self).logout()
|
||||
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login
|
||||
|
||||
return redirect(
|
||||
oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
|
||||
```
|
||||
|
||||
Then add to your `superset_config.py` file:
|
||||
|
||||
```python
|
||||
from keycloak_security_manager import OIDCSecurityManager
|
||||
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
|
||||
import os
|
||||
|
||||
AUTH_TYPE = AUTH_OID
|
||||
SECRET_KEY: 'SomethingNotEntirelySecret'
|
||||
OIDC_CLIENT_SECRETS = '/path/to/client_secret.json'
|
||||
OIDC_ID_TOKEN_COOKIE_SECURE = False
|
||||
OIDC_OPENID_REALM: '<myRealm>'
|
||||
OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post'
|
||||
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
|
||||
|
||||
# Will allow user self registration, allowing to create Flask users from Authorized User
|
||||
AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
AUTH_USER_REGISTRATION_ROLE = 'Public'
|
||||
```
|
||||
|
||||
Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"<myOpenIDProvider>": {
|
||||
"issuer": "https://<myKeycloakDomain>/realms/<myRealm>",
|
||||
"auth_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/auth",
|
||||
"client_id": "https://<myKeycloakDomain>",
|
||||
"client_secret": "<myClientSecret>",
|
||||
"redirect_uris": [
|
||||
"https://<SupersetWebserver>/oauth-authorized/<myOpenIDProvider>"
|
||||
],
|
||||
"userinfo_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/userinfo",
|
||||
"token_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
FAB supports authenticating user credentials against an LDAP server.
|
||||
|
||||
@@ -165,6 +165,206 @@ Or in the CRUD interface theme JSON:
|
||||
|
||||
This feature works with the stock Docker image - no custom build required!
|
||||
|
||||
## ECharts Configuration Overrides
|
||||
|
||||
:::note
|
||||
Available since Superset 6.0
|
||||
:::
|
||||
|
||||
Superset provides fine-grained control over ECharts visualizations through theme-level configuration overrides. This allows you to customize the appearance and behavior of all ECharts-based charts without modifying individual chart configurations.
|
||||
|
||||
### Global ECharts Overrides
|
||||
|
||||
Apply settings to all ECharts visualizations using `echartsOptionsOverrides`:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"colorPrimary": "#2893B3",
|
||||
# ... other Ant Design tokens
|
||||
},
|
||||
"echartsOptionsOverrides": {
|
||||
"grid": {
|
||||
"left": "10%",
|
||||
"right": "10%",
|
||||
"top": "15%",
|
||||
"bottom": "15%"
|
||||
},
|
||||
"tooltip": {
|
||||
"backgroundColor": "rgba(0, 0, 0, 0.8)",
|
||||
"borderColor": "#ccc",
|
||||
"textStyle": {
|
||||
"color": "#fff"
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"fontSize": 14,
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Chart-Specific Overrides
|
||||
|
||||
Target specific chart types using `echartsOptionsOverridesByChartType`:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"colorPrimary": "#2893B3",
|
||||
# ... other tokens
|
||||
},
|
||||
"echartsOptionsOverridesByChartType": {
|
||||
"echarts_pie": {
|
||||
"legend": {
|
||||
"orient": "vertical",
|
||||
"right": 10,
|
||||
"top": "center"
|
||||
}
|
||||
},
|
||||
"echarts_timeseries": {
|
||||
"xAxis": {
|
||||
"axisLabel": {
|
||||
"rotate": 45,
|
||||
"fontSize": 12
|
||||
}
|
||||
},
|
||||
"dataZoom": [{
|
||||
"type": "slider",
|
||||
"show": True,
|
||||
"start": 0,
|
||||
"end": 100
|
||||
}]
|
||||
},
|
||||
"echarts_bubble": {
|
||||
"grid": {
|
||||
"left": "15%",
|
||||
"bottom": "20%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI Configuration
|
||||
|
||||
You can also configure ECharts overrides through the theme CRUD interface:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"colorPrimary": "#2893B3"
|
||||
},
|
||||
"echartsOptionsOverrides": {
|
||||
"grid": {
|
||||
"left": "10%",
|
||||
"right": "10%"
|
||||
},
|
||||
"tooltip": {
|
||||
"backgroundColor": "rgba(0, 0, 0, 0.8)"
|
||||
}
|
||||
},
|
||||
"echartsOptionsOverridesByChartType": {
|
||||
"echarts_pie": {
|
||||
"legend": {
|
||||
"orient": "vertical",
|
||||
"right": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Override Precedence
|
||||
|
||||
The system applies overrides in the following order (last wins):
|
||||
|
||||
1. **Base ECharts theme** - Default Superset styling
|
||||
2. **Plugin options** - Chart-specific configurations
|
||||
3. **Global overrides** - `echartsOptionsOverrides`
|
||||
4. **Chart-specific overrides** - `echartsOptionsOverridesByChartType[chartType]`
|
||||
|
||||
This ensures chart-specific overrides take precedence over global ones.
|
||||
|
||||
### Common Chart Types
|
||||
|
||||
Available chart types for `echartsOptionsOverridesByChartType`:
|
||||
|
||||
- `echarts_timeseries` - Time series/line charts
|
||||
- `echarts_pie` - Pie and donut charts
|
||||
- `echarts_bubble` - Bubble/scatter charts
|
||||
- `echarts_funnel` - Funnel charts
|
||||
- `echarts_gauge` - Gauge charts
|
||||
- `echarts_radar` - Radar charts
|
||||
- `echarts_boxplot` - Box plot charts
|
||||
- `echarts_treemap` - Treemap charts
|
||||
- `echarts_sunburst` - Sunburst charts
|
||||
- `echarts_graph` - Network/graph charts
|
||||
- `echarts_sankey` - Sankey diagrams
|
||||
- `echarts_heatmap` - Heatmaps
|
||||
- `echarts_mixed_timeseries` - Mixed time series
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Start with global overrides** for consistent styling across all charts
|
||||
2. **Use chart-specific overrides** for unique requirements per visualization type
|
||||
3. **Test thoroughly** as overrides use deep merge - nested objects are combined, but arrays are completely replaced
|
||||
4. **Document your overrides** to help team members understand custom styling
|
||||
5. **Consider performance** - complex overrides may impact chart rendering speed
|
||||
|
||||
### Example: Corporate Branding
|
||||
|
||||
```python
|
||||
# Complete corporate theme with ECharts customization
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"colorPrimary": "#1B4D3E",
|
||||
"fontFamily": "Corporate Sans, Arial, sans-serif"
|
||||
},
|
||||
"echartsOptionsOverrides": {
|
||||
"grid": {
|
||||
"left": "8%",
|
||||
"right": "8%",
|
||||
"top": "12%",
|
||||
"bottom": "12%"
|
||||
},
|
||||
"textStyle": {
|
||||
"fontFamily": "Corporate Sans, Arial, sans-serif"
|
||||
},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#1B4D3E",
|
||||
"fontSize": 18,
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
}
|
||||
},
|
||||
"echartsOptionsOverridesByChartType": {
|
||||
"echarts_timeseries": {
|
||||
"xAxis": {
|
||||
"axisLabel": {
|
||||
"color": "#666",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
},
|
||||
"echarts_pie": {
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"fontSize": 12
|
||||
},
|
||||
"itemGap": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This feature provides powerful theming capabilities while maintaining the flexibility of ECharts' extensive configuration options.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
|
||||
|
||||
@@ -620,10 +620,10 @@ See [how tos](/docs/contributing/howtos#linting)
|
||||
|
||||
:::tip
|
||||
`act` compatibility of Superset's GHAs is not fully tested. Running `act` locally may or may not
|
||||
work for different actions, and may require fine tunning and local secret-handling.
|
||||
work for different actions, and may require fine tuning and local secret-handling.
|
||||
For those more intricate GHAs that are tricky to run locally, we recommend iterating
|
||||
directly on GHA's infrastructure, by pushing directly on a branch and monitoring GHA logs.
|
||||
For more targetted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI.
|
||||
For more targeted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI.
|
||||
:::
|
||||
|
||||
For automation and CI/CD, Superset makes extensive use of GitHub Actions (GHA). You
|
||||
|
||||
@@ -232,7 +232,7 @@ CYPRESS_CONFIG=true docker compose up --build
|
||||
```
|
||||
`docker compose` will get to work and expose a Cypress-ready Superset app.
|
||||
This app uses a different database schema (`superset_cypress`) to keep it isolated from
|
||||
your other dev environmen(s)t, a specific set of examples, and a set of configurations that
|
||||
your other dev environment(s), a specific set of examples, and a set of configurations that
|
||||
aligns with the expectations within the end-to-end tests. Also note that it's served on a
|
||||
different port than the default port for the backend (`8088`).
|
||||
|
||||
@@ -627,7 +627,7 @@ feature flag to `true`, you can add the following line to the PR body/descriptio
|
||||
FEATURE_TAGGING_SYSTEM=true
|
||||
```
|
||||
|
||||
Simarly, it's possible to disable feature flags with:
|
||||
Similarly, it's possible to disable feature flags with:
|
||||
|
||||
```
|
||||
FEATURE_TAGGING_SYSTEM=false
|
||||
|
||||
@@ -282,5 +282,5 @@ address.
|
||||
When running `docker compose up`, docker will build what is required behind the scene, but
|
||||
may use the docker cache if assets already exist. Running `docker compose build` prior to
|
||||
`docker compose up` or the equivalent shortcut `docker compose up --build` ensures that your
|
||||
docker images matche the definition in the repository. This should only apply to the main
|
||||
docker images match the definition in the repository. This should only apply to the main
|
||||
docker-compose.yml file (default) and not to the alternative methods defined above.
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"storybook": "^8.6.11",
|
||||
"swagger-ui-react": "^5.27.1",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"ts-loader": "^9.5.2"
|
||||
"ts-loader": "^9.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
|
||||
@@ -363,110 +363,6 @@ CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
|
||||
]
|
||||
```
|
||||
|
||||
### Keycloak-Specific Configuration using Flask-OIDC
|
||||
|
||||
If you are using Keycloak as OpenID Connect 1.0 Provider, the above configuration based on [`Authlib`](https://authlib.org/) might not work. In this case using [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is a viable option.
|
||||
|
||||
Make sure the pip package [`Flask-OIDC`](https://pypi.org/project/flask-oidc/) is installed on the webserver. This was successfully tested using version 2.2.0. This package requires [`Flask-OpenID`](https://pypi.org/project/Flask-OpenID/) as a dependency.
|
||||
|
||||
The following code defines a new security manager. Add it to a new file named `keycloak_security_manager.py`, placed in the same directory as your `superset_config.py` file.
|
||||
|
||||
```python
|
||||
from flask_appbuilder.security.manager import AUTH_OID
|
||||
from superset.security import SupersetSecurityManager
|
||||
from flask_oidc import OpenIDConnect
|
||||
from flask_appbuilder.security.views import AuthOIDView
|
||||
from flask_login import login_user
|
||||
from urllib.parse import quote
|
||||
from flask_appbuilder.views import ModelView, SimpleFormView, expose
|
||||
from flask import (
|
||||
redirect,
|
||||
request
|
||||
)
|
||||
import logging
|
||||
|
||||
class OIDCSecurityManager(SupersetSecurityManager):
|
||||
|
||||
def __init__(self, appbuilder):
|
||||
super(OIDCSecurityManager, self).__init__(appbuilder)
|
||||
if self.auth_type == AUTH_OID:
|
||||
self.oid = OpenIDConnect(self.appbuilder.get_app)
|
||||
self.authoidview = AuthOIDCView
|
||||
|
||||
class AuthOIDCView(AuthOIDView):
|
||||
|
||||
@expose('/login/', methods=['GET', 'POST'])
|
||||
def login(self, flag=True):
|
||||
sm = self.appbuilder.sm
|
||||
oidc = sm.oid
|
||||
|
||||
@self.appbuilder.sm.oid.require_login
|
||||
def handle_login():
|
||||
user = sm.auth_user_oid(oidc.user_getfield('email'))
|
||||
|
||||
if user is None:
|
||||
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email'])
|
||||
user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'),
|
||||
info.get('email'), sm.find_role('Gamma'))
|
||||
|
||||
login_user(user, remember=False)
|
||||
return redirect(self.appbuilder.get_url_for_index)
|
||||
|
||||
return handle_login()
|
||||
|
||||
@expose('/logout/', methods=['GET', 'POST'])
|
||||
def logout(self):
|
||||
oidc = self.appbuilder.sm.oid
|
||||
|
||||
oidc.logout()
|
||||
super(AuthOIDCView, self).logout()
|
||||
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login
|
||||
|
||||
return redirect(
|
||||
oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url))
|
||||
```
|
||||
|
||||
Then add to your `superset_config.py` file:
|
||||
|
||||
```python
|
||||
from keycloak_security_manager import OIDCSecurityManager
|
||||
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
|
||||
import os
|
||||
|
||||
AUTH_TYPE = AUTH_OID
|
||||
SECRET_KEY: 'SomethingNotEntirelySecret'
|
||||
OIDC_CLIENT_SECRETS = '/path/to/client_secret.json'
|
||||
OIDC_ID_TOKEN_COOKIE_SECURE = False
|
||||
OIDC_OPENID_REALM: '<myRealm>'
|
||||
OIDC_INTROSPECTION_AUTH_METHOD: 'client_secret_post'
|
||||
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager
|
||||
|
||||
# Will allow user self registration, allowing to create Flask users from Authorized User
|
||||
AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
AUTH_USER_REGISTRATION_ROLE = 'Public'
|
||||
```
|
||||
|
||||
Store your client-specific OpenID information in a file called `client_secret.json`. Create this file in the same directory as `superset_config.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"<myOpenIDProvider>": {
|
||||
"issuer": "https://<myKeycloakDomain>/realms/<myRealm>",
|
||||
"auth_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/auth",
|
||||
"client_id": "https://<myKeycloakDomain>",
|
||||
"client_secret": "<myClientSecret>",
|
||||
"redirect_uris": [
|
||||
"https://<SupersetWebserver>/oauth-authorized/<myOpenIDProvider>"
|
||||
],
|
||||
"userinfo_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/userinfo",
|
||||
"token_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "https://<myKeycloakDomain>/realms/<myRealm>/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
FAB supports authenticating user credentials against an LDAP server.
|
||||
|
||||
@@ -620,10 +620,10 @@ See [how tos](/docs/contributing/howtos#linting)
|
||||
|
||||
:::tip
|
||||
`act` compatibility of Superset's GHAs is not fully tested. Running `act` locally may or may not
|
||||
work for different actions, and may require fine tunning and local secret-handling.
|
||||
work for different actions, and may require fine tuning and local secret-handling.
|
||||
For those more intricate GHAs that are tricky to run locally, we recommend iterating
|
||||
directly on GHA's infrastructure, by pushing directly on a branch and monitoring GHA logs.
|
||||
For more targetted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI.
|
||||
For more targeted iteration, see the `gh workflow run --ref {BRANCH}` subcommand of the GitHub CLI.
|
||||
:::
|
||||
|
||||
For automation and CI/CD, Superset makes extensive use of GitHub Actions (GHA). You
|
||||
|
||||
@@ -232,7 +232,7 @@ CYPRESS_CONFIG=true docker compose up --build
|
||||
```
|
||||
`docker compose` will get to work and expose a Cypress-ready Superset app.
|
||||
This app uses a different database schema (`superset_cypress`) to keep it isolated from
|
||||
your other dev environmen(s)t, a specific set of examples, and a set of configurations that
|
||||
your other dev environment(s), a specific set of examples, and a set of configurations that
|
||||
aligns with the expectations within the end-to-end tests. Also note that it's served on a
|
||||
different port than the default port for the backend (`8088`).
|
||||
|
||||
@@ -627,7 +627,7 @@ feature flag to `true`, you can add the following line to the PR body/descriptio
|
||||
FEATURE_TAGGING_SYSTEM=true
|
||||
```
|
||||
|
||||
Simarly, it's possible to disable feature flags with:
|
||||
Similarly, it's possible to disable feature flags with:
|
||||
|
||||
```
|
||||
FEATURE_TAGGING_SYSTEM=false
|
||||
|
||||
@@ -282,5 +282,5 @@ address.
|
||||
When running `docker compose up`, docker will build what is required behind the scene, but
|
||||
may use the docker cache if assets already exist. Running `docker compose build` prior to
|
||||
`docker compose up` or the equivalent shortcut `docker compose up --build` ensures that your
|
||||
docker images matche the definition in the repository. This should only apply to the main
|
||||
docker images match the definition in the repository. This should only apply to the main
|
||||
docker-compose.yml file (default) and not to the alternative methods defined above.
|
||||
|
||||
@@ -4102,7 +4102,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.40.0", "@typescript-eslint/parser@^8.37.0":
|
||||
"@typescript-eslint/parser@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.40.0.tgz#1bc9f3701ced29540eb76ff2d95ce0d52ddc7e69"
|
||||
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
|
||||
@@ -4113,6 +4113,17 @@
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/parser@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.42.0.tgz#20ea66f4867981fb5bb62cbe1454250fc4a440ab"
|
||||
integrity sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.42.0"
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/typescript-estree" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz#1b7ba6079ff580c3215882fe75a43e5d3ed166b9"
|
||||
@@ -4122,6 +4133,15 @@
|
||||
"@typescript-eslint/types" "^8.40.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.42.0.tgz#636eb3418b6c42c98554dce884943708bf41a583"
|
||||
integrity sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.42.0"
|
||||
"@typescript-eslint/types" "^8.42.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz#2fbfcc8643340d8cd692267e61548b946190be8a"
|
||||
@@ -4130,11 +4150,24 @@
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz#36016757bc85b46ea42bae47b61f9421eddedde3"
|
||||
integrity sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz#8e8fdb9b988854aedd04abdde3239c4bdd2d26e4"
|
||||
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.42.0", "@typescript-eslint/tsconfig-utils@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz#21a3e74396fd7443ff930bc41b27789ba7e9236e"
|
||||
integrity sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz#a7e4a1f0815dd0ba3e4eef945cc87193ca32c422"
|
||||
@@ -4146,11 +4179,16 @@
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0":
|
||||
"@typescript-eslint/types@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.40.0.tgz#0b580fdf643737aa5c01285314b5c6e9543846a9"
|
||||
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
|
||||
|
||||
"@typescript-eslint/types@8.42.0", "@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.42.0.tgz#ae15c09cebda20473772902033328e87372db008"
|
||||
integrity sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz#295149440ce7da81c790a4e14e327599a3a1e5c9"
|
||||
@@ -4167,6 +4205,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz#593c3af87d4462252c0d7239d1720b84a1b56864"
|
||||
integrity sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.42.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.42.0"
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.40.0.tgz#8d0c6430ed2f5dc350784bb0d8be514da1e54054"
|
||||
@@ -4185,6 +4239,14 @@
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz#87c6caaa1ac307bc73a87c1fc469f88f0162f27e"
|
||||
integrity sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
||||
@@ -4704,9 +4766,9 @@ available-typed-arrays@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.9.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6"
|
||||
integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3"
|
||||
integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.4"
|
||||
@@ -13139,10 +13201,10 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
|
||||
|
||||
ts-loader@^9.5.2:
|
||||
version "9.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.2.tgz#1f3d7f4bb709b487aaa260e8f19b301635d08020"
|
||||
integrity sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==
|
||||
ts-loader@^9.5.4:
|
||||
version "9.5.4"
|
||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.4.tgz#44b571165c10fb5a90744aa5b7e119233c4f4585"
|
||||
integrity sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
enhanced-resolve "^5.0.0"
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.8.1, <5.0.0",
|
||||
"flask-appbuilder>=5.0.0,<6",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -88,7 +88,7 @@ dependencies = [
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"python-geohash",
|
||||
"pyarrow>=16.1.0, <17", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyarrow>=16.1.0, <19", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.0, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=4.6.0, <5.0",
|
||||
@@ -313,6 +313,7 @@ select = [
|
||||
"E",
|
||||
"F",
|
||||
"F",
|
||||
"G",
|
||||
"I",
|
||||
"N",
|
||||
"PT",
|
||||
@@ -328,6 +329,7 @@ ignore = [
|
||||
"PT006",
|
||||
"T201",
|
||||
"N999",
|
||||
"G201",
|
||||
]
|
||||
extend-select = ["I"]
|
||||
|
||||
|
||||
@@ -114,11 +114,9 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
flask-babel==2.0.0
|
||||
flask-appbuilder==5.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==3.1.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -208,12 +208,11 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.8.1
|
||||
flask-appbuilder==5.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
flask-babel==2.0.0
|
||||
flask-babel==3.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-appbuilder
|
||||
|
||||
@@ -42,7 +42,7 @@ classifiers = [
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
dependencies = [
|
||||
"flask-appbuilder>=4.5.3, <5.0.0",
|
||||
"flask-appbuilder>=5.0.0,<6",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
426
superset-embedded-sdk/package-lock.json
generated
@@ -19,7 +19,6 @@
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.5.4",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"tscw-config": "^1.1.2",
|
||||
@@ -3233,24 +3232,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -3644,19 +3625,6 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -3843,18 +3811,6 @@
|
||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
@@ -4045,15 +4001,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
@@ -4063,20 +4010,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -4137,57 +4070,12 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -4628,42 +4516,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -4717,30 +4569,6 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
@@ -4750,19 +4578,6 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -4825,18 +4640,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -4865,45 +4668,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -7092,15 +6856,6 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -7414,12 +7169,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -10724,23 +10473,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -11023,16 +10755,6 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -11158,15 +10880,6 @@
|
||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
@@ -11299,29 +11012,12 @@
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"dev": true
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||
"dev": true
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.5.19",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
|
||||
@@ -11365,45 +11061,12 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-module-lexer": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
|
||||
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
|
||||
"dev": true
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -11712,25 +11375,6 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs-readdir-recursive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
|
||||
@@ -11768,40 +11412,12 @@
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -11844,12 +11460,6 @@
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -11871,30 +11481,6 @@
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -13505,12 +13091,6 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -13742,12 +13322,6 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.5.4",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"tscw-config": "^1.1.2",
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const axios = require('axios');
|
||||
const { name, version } = require('./package.json');
|
||||
|
||||
function log(...args) {
|
||||
@@ -36,9 +35,7 @@ function logError(...args) {
|
||||
// npm commands output a bunch of garbage in the edge cases,
|
||||
// and require sending semi-validated strings to the command line,
|
||||
// so let's just use good old http.
|
||||
const { status } = await axios.get(packageUrl, {
|
||||
validateStatus: (status) => true // we literally just want the status so any status is valid
|
||||
});
|
||||
const { status } = await fetch(packageUrl);
|
||||
|
||||
if (status === 200) {
|
||||
log('version already exists on npm, exiting');
|
||||
|
||||
@@ -160,7 +160,9 @@ def test_validate_npm_handles_file_not_found_exception(mock_run, mock_which):
|
||||
def test_validate_npm_does_not_catch_other_subprocess_exceptions(
|
||||
mock_run, mock_which, exception_type
|
||||
):
|
||||
"""Test validate_npm does not catch OSError and PermissionError (they propagate up)."""
|
||||
"""
|
||||
Test validate_npm does not catch OSError and PermissionError (they propagate up).
|
||||
"""
|
||||
mock_which.return_value = "/usr/bin/npm"
|
||||
mock_run.side_effect = exception_type("Test error")
|
||||
|
||||
|
||||
@@ -1,766 +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.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { Interception } from 'cypress/types/net-stubbing';
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import { SUPPORTED_CHARTS_DASHBOARD } from 'cypress/utils/urls';
|
||||
import {
|
||||
openTopLevelTab,
|
||||
SUPPORTED_TIER1_CHARTS,
|
||||
SUPPORTED_TIER2_CHARTS,
|
||||
} from './utils';
|
||||
import {
|
||||
interceptExploreJson,
|
||||
interceptV1ChartData,
|
||||
interceptFormDataKey,
|
||||
} from '../explore/utils';
|
||||
|
||||
const interceptDrillInfo = () => {
|
||||
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
result: {
|
||||
id: 1,
|
||||
changed_on_humanized: '2 days ago',
|
||||
created_on_humanized: 'a week ago',
|
||||
table_name: 'birth_names',
|
||||
changed_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
created_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
column_name: 'gender',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'state',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'name',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'ds',
|
||||
verbose_name: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).as('drillInfo');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="close-drill-by-modal"]').length) {
|
||||
cy.getBySel('close-drill-by-modal').click({ force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openTableContextMenu = (
|
||||
cellContent: string,
|
||||
tableSelector = "[data-test-viz-type='table']",
|
||||
) => {
|
||||
cy.get(tableSelector).scrollIntoView();
|
||||
cy.get(tableSelector).contains(cellContent).first().rightclick();
|
||||
};
|
||||
|
||||
const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
if (isLegacy) {
|
||||
interceptExploreJson('legacyData');
|
||||
} else {
|
||||
interceptV1ChartData();
|
||||
}
|
||||
|
||||
cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
|
||||
.should('be.visible')
|
||||
.find("[role='menu'] [role='menuitem']")
|
||||
.contains(/^Drill by$/)
|
||||
.trigger('mouseover', { force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.click();
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).should('not.exist');
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
}
|
||||
return cy.wait('@v1Data');
|
||||
};
|
||||
|
||||
const verifyExpectedFormData = (
|
||||
interceptedRequest: Interception,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expectedFormData: Record<string, any>,
|
||||
) => {
|
||||
const actualFormData = interceptedRequest.request.body?.form_data;
|
||||
Object.entries(expectedFormData).forEach(([key, val]) => {
|
||||
expect(actualFormData?.[key]).to.eql(val);
|
||||
});
|
||||
};
|
||||
|
||||
const testEchart = (
|
||||
vizType: string,
|
||||
chartName: string,
|
||||
drillClickCoordinates: [[number, number], [number, number]],
|
||||
furtherDrillDimension = 'name',
|
||||
) => {
|
||||
cy.get(`[data-test-viz-type='${vizType}'] canvas`).then($canvas => {
|
||||
// click 'boy'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger(
|
||||
'mouseover',
|
||||
drillClickCoordinates[0][0],
|
||||
drillClickCoordinates[0][1],
|
||||
);
|
||||
cy.wrap($canvas).rightclick(
|
||||
drillClickCoordinates[0][0],
|
||||
drillClickCoordinates[0][1],
|
||||
);
|
||||
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel(`"Drill by: ${chartName}-modal"`).as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', chartName);
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// further drill
|
||||
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
|
||||
// click 'other'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger(
|
||||
'mouseover',
|
||||
drillClickCoordinates[1][0],
|
||||
drillClickCoordinates[1][1],
|
||||
);
|
||||
cy.wrap($canvas).rightclick(
|
||||
drillClickCoordinates[1][0],
|
||||
drillClickCoordinates[1][1],
|
||||
);
|
||||
|
||||
drillBy(furtherDrillDimension).then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: [furtherDrillDimension],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'other',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state (other)')
|
||||
.and('contain', furtherDrillDimension)
|
||||
.contains('state (other)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'state (other)')
|
||||
.and('not.contain', furtherDrillDimension)
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Drill by modal', () => {
|
||||
beforeEach(() => {
|
||||
closeModal();
|
||||
});
|
||||
before(() => {
|
||||
interceptDrillInfo();
|
||||
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
|
||||
});
|
||||
|
||||
describe('Modal actions + Table', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Table-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Drill by: Table');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="metadata-bar"]')
|
||||
.should('be.visible');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
// further drilling
|
||||
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
|
||||
drillBy('name').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'CA',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
interceptFormDataKey();
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'state (CA)')
|
||||
.and('contain', 'name')
|
||||
.contains('state (CA)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupby: ['state'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'name')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'state (CA)')
|
||||
.and('not.contain', 'name')
|
||||
.and('contain', 'state');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-display-toggle"]')
|
||||
.contains('Table')
|
||||
.click();
|
||||
|
||||
cy.getBySel('drill-by-chart').should('not.exist');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-results-table"]')
|
||||
.should('be.visible');
|
||||
|
||||
cy.wait('@formDataKey').then(intercept => {
|
||||
cy.get('@drillByModal')
|
||||
.contains('Edit chart')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
`/explore/?form_data_key=${intercept.response?.body?.key}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('Pivot Table', () => {
|
||||
openTableContextMenu('boy', "[data-test-viz-type='pivot_table_v2']");
|
||||
drillBy('name').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyRows: ['state'],
|
||||
groupbyColumns: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Pivot Table-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Drill by: Pivot Table');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num')
|
||||
.and('not.contain', 'Gender');
|
||||
|
||||
openTableContextMenu('CA', '[data-test="drill-by-chart"]');
|
||||
drillBy('ds').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyColumns: ['name'],
|
||||
groupbyRows: ['ds'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'CA',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'state',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'ds')
|
||||
.and('contain', 'sum__num')
|
||||
.and('not.contain', 'state');
|
||||
|
||||
interceptV1ChartData('drillByUndo');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name (CA)')
|
||||
.and('contain', 'ds')
|
||||
.contains('name (CA)')
|
||||
.click();
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
groupbyRows: ['state'],
|
||||
groupbyColumns: ['name'],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
comparator: 'boy',
|
||||
expressionType: 'SIMPLE',
|
||||
operator: '==',
|
||||
operatorId: 'EQUALS',
|
||||
subject: 'gender',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'state')
|
||||
.and('contain', 'name')
|
||||
.and('contain', 'sum__num');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'name (CA)')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'name');
|
||||
});
|
||||
|
||||
it('Line chart', () => {
|
||||
testEchart('echarts_timeseries_line', 'Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Area Chart', () => {
|
||||
testEchart('echarts_area', 'Area Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Scatter Chart', () => {
|
||||
testEchart('echarts_timeseries_scatter', 'Scatter Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Bar Chart', () => {
|
||||
testEchart('echarts_timeseries_bar', 'Bar Chart', [
|
||||
[85, 94],
|
||||
[490, 68],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Pie Chart', () => {
|
||||
testEchart('pie', 'Pie Chart', [
|
||||
[243, 167],
|
||||
[534, 248],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it('Box Plot Chart', () => {
|
||||
testEchart(
|
||||
'box_plot',
|
||||
'Box Plot Chart',
|
||||
[
|
||||
[139, 277],
|
||||
[787, 441],
|
||||
],
|
||||
'ds',
|
||||
);
|
||||
});
|
||||
|
||||
it('Generic Chart', () => {
|
||||
testEchart('echarts_timeseries', 'Generic Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Smooth Line Chart', () => {
|
||||
testEchart('echarts_timeseries_smooth', 'Smooth Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Step Line Chart', () => {
|
||||
testEchart('echarts_timeseries_step', 'Step Line Chart', [
|
||||
[85, 93],
|
||||
[85, 93],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Funnel Chart', () => {
|
||||
testEchart('funnel', 'Funnel Chart', [
|
||||
[154, 80],
|
||||
[421, 39],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Gauge Chart', () => {
|
||||
testEchart('gauge_chart', 'Gauge Chart', [
|
||||
[151, 95],
|
||||
[300, 143],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Radar Chart', () => {
|
||||
testEchart('radar', 'Radar Chart', [
|
||||
[182, 49],
|
||||
[423, 91],
|
||||
]);
|
||||
});
|
||||
|
||||
it('Treemap V2 Chart', () => {
|
||||
testEchart('treemap_v2', 'Treemap V2 Chart', [
|
||||
[145, 84],
|
||||
[220, 105],
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('Mixed Chart', () => {
|
||||
cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
|
||||
// click 'boy'
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger('mouseover', 85, 93);
|
||||
cy.wrap($canvas).rightclick(85, 93);
|
||||
|
||||
drillBy('name').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['state']);
|
||||
expect(queries[1].filters).to.eql([]);
|
||||
});
|
||||
|
||||
cy.getBySel('"Drill by: Mixed Chart-modal"').as('drillByModal');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.draggable-trigger')
|
||||
.should('contain', 'Mixed Chart');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// further drill
|
||||
cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
|
||||
// click second query
|
||||
cy.wrap($canvas).scrollIntoView();
|
||||
cy.wrap($canvas).trigger('mouseover', 261, 114);
|
||||
cy.wrap($canvas).rightclick(261, 114);
|
||||
|
||||
drillBy('ds').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['ds']);
|
||||
expect(queries[1].filters).to.eql([
|
||||
{ col: 'state', op: '==', val: 'other' },
|
||||
]);
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
|
||||
// undo - back to drill by state
|
||||
interceptV1ChartData('drillByUndo');
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('contain', 'name (other)')
|
||||
.and('contain', 'ds')
|
||||
.contains('name (other)')
|
||||
.click();
|
||||
|
||||
cy.wait('@drillByUndo').then(intercepted => {
|
||||
const { queries } = intercepted.request.body;
|
||||
expect(queries[0].columns).to.eql(['name']);
|
||||
expect(queries[0].filters).to.eql([
|
||||
{ col: 'gender', op: '==', val: 'boy' },
|
||||
]);
|
||||
expect(queries[1].columns).to.eql(['state']);
|
||||
expect(queries[1].filters).to.eql([]);
|
||||
});
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('.ant-breadcrumb')
|
||||
.should('be.visible')
|
||||
.and('contain', 'gender (boy)')
|
||||
.and('contain', '/')
|
||||
.and('not.contain', 'name (other)')
|
||||
.and('not.contain', 'ds')
|
||||
.and('contain', 'name');
|
||||
|
||||
cy.get('@drillByModal')
|
||||
.find('[data-test="drill-by-chart"]')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
41
superset-frontend/package-lock.json
generated
@@ -21,7 +21,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.24.13",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.12",
|
||||
"@rjsf/validator-ajv8": "^5.24.13",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -57,13 +57,13 @@
|
||||
"antd": "^5.24.6",
|
||||
"chrono-node": "^2.7.8",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^0.5.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -174,6 +174,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -189,7 +190,6 @@
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-ultimate-pagination": "^1.2.4",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.8",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
@@ -10884,9 +10884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rjsf/validator-ajv8": {
|
||||
"version": "5.24.12",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.12.tgz",
|
||||
"integrity": "sha512-IMXdCjvDNdvb+mDgZC3AlAtr0pjYKq5s0GcLECjG5PuiX7Ib4JaDQHZY5ZJdKblMfgzhsn8AAOi573jXAt7BHQ==",
|
||||
"version": "5.24.13",
|
||||
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.24.13.tgz",
|
||||
"integrity": "sha512-oWHP7YK581M8I5cF1t+UXFavnv+bhcqjtL1a7MG/Kaffi0EwhgcYjODrD8SsnrhncsEYMqSECr4ZOEoirnEUWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
@@ -15250,6 +15250,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/content-disposition": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
|
||||
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/conventional-commits-parser": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz",
|
||||
@@ -16069,16 +16076,6 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-ultimate-pagination": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-ultimate-pagination/-/react-ultimate-pagination-1.2.4.tgz",
|
||||
"integrity": "sha512-1y9jLt3KEFGzFD+99qVpJUI/Eu4cEx48sClB957eGoepWRLVVi+r1UBj0157Mg7HYZcIF4I1/qGZYaBBQWhaqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.8.tgz",
|
||||
@@ -21928,7 +21925,6 @@
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
@@ -24469,12 +24465,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/emotion-rgba": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/emotion-rgba/-/emotion-rgba-0.0.12.tgz",
|
||||
"integrity": "sha512-lvtZ52BWisYDtis+HctQMkxcHwmFbzTiZhgMJGFfWXLsBYEzthfKE7nlysOiUwmmAdTM/8YBAPfwQ4MEDwiaWw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodable": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/encodable/-/encodable-0.7.8.tgz",
|
||||
@@ -60634,7 +60624,7 @@
|
||||
},
|
||||
"packages/superset-core": {
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc2",
|
||||
"version": "0.0.1-rc3",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.26.4",
|
||||
@@ -60644,7 +60634,8 @@
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/react": "^17.0.83",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.1.0"
|
||||
"npm": "^11.1.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.24.6",
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.24.13",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
"@rjsf/validator-ajv8": "^5.24.12",
|
||||
"@rjsf/validator-ajv8": "^5.24.13",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -125,13 +125,13 @@
|
||||
"antd": "^5.24.6",
|
||||
"chrono-node": "^2.7.8",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^0.5.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -242,6 +242,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -257,7 +258,6 @@
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-ultimate-pagination": "^1.2.4",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.8",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/redux-localstorage": "^1.0.8",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc2",
|
||||
"version": "0.0.1-rc3",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"author": "",
|
||||
@@ -19,14 +18,15 @@
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/react": "^17.0.83",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.1.0"
|
||||
"npm": "^11.1.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.24.6",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir lib --extensions \".ts,.tsx\"",
|
||||
"build": "babel src --out-dir lib --extensions \".ts,.tsx\" && tsc --emitDeclarationOnly",
|
||||
"type": "tsc --noEmit"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -31,12 +31,32 @@ import { Contributions } from './contributions';
|
||||
/**
|
||||
* Represents a database column with its name and data type.
|
||||
*/
|
||||
export declare interface Column {
|
||||
/** The name of the column */
|
||||
export type Column = {
|
||||
/**
|
||||
* Label of the column
|
||||
*/
|
||||
name: string;
|
||||
/** The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP') */
|
||||
|
||||
/**
|
||||
* Column name defined
|
||||
*/
|
||||
column_name: string;
|
||||
|
||||
/**
|
||||
* The data type of the column (e.g., 'INTEGER', 'VARCHAR', 'TIMESTAMP')
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic data type format
|
||||
*/
|
||||
type_generic: GenericDataType;
|
||||
|
||||
/**
|
||||
* True if the column is date format
|
||||
*/
|
||||
is_dttm: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a database table with its name and column definitions.
|
||||
@@ -76,6 +96,45 @@ export declare interface Database {
|
||||
schemas: Schema[];
|
||||
}
|
||||
|
||||
// Keep in sync with superset/errors.py
|
||||
export type ErrorLevel = 'info' | 'warning' | 'error';
|
||||
|
||||
/**
|
||||
* Superset error object structure.
|
||||
* Contains details about an error that occurred within Superset.
|
||||
*/
|
||||
export type SupersetError = {
|
||||
/**
|
||||
* Error types, see enum of SupersetErrorType in superset/errors.py
|
||||
*/
|
||||
error_type: string;
|
||||
|
||||
/**
|
||||
* Extra properties based on the error types
|
||||
*/
|
||||
extra: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Level of the error type
|
||||
*/
|
||||
level: ErrorLevel;
|
||||
|
||||
/**
|
||||
* Detail description for the error
|
||||
*/
|
||||
message: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic data types, see enum of the same name in superset/utils/core.py.
|
||||
*/
|
||||
export enum GenericDataType {
|
||||
Numeric = 0,
|
||||
String = 1,
|
||||
Temporal = 2,
|
||||
Boolean = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a type which can release resources, such
|
||||
* as event listening or a timer.
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
* - Global APIs: Functions and events available across the entire SQL Lab interface
|
||||
*/
|
||||
|
||||
import { Event, Database } from './core';
|
||||
import { Event, Database, SupersetError, Column } from './core';
|
||||
|
||||
/**
|
||||
* Represents an SQL editor instance within a SQL Lab tab.
|
||||
@@ -111,6 +111,126 @@ export interface Tab {
|
||||
panels: Panel[];
|
||||
}
|
||||
|
||||
export enum CTASMethod {
|
||||
Table = 'TABLE',
|
||||
View = 'VIEW',
|
||||
}
|
||||
|
||||
export interface CTAS {
|
||||
/**
|
||||
* Create method for CTAS creation request.
|
||||
*/
|
||||
method: CTASMethod;
|
||||
|
||||
/**
|
||||
* Temporary table name for creation using a CTAS query.
|
||||
*/
|
||||
tempTable: string | null;
|
||||
}
|
||||
|
||||
export interface QueryContext {
|
||||
/**
|
||||
* Unique query ID on client side.
|
||||
*/
|
||||
clientId: string;
|
||||
|
||||
/**
|
||||
* Contains CTAS if the query requests table creation.
|
||||
*/
|
||||
ctas: CTAS | null;
|
||||
|
||||
/**
|
||||
* Requested row limit for the query.
|
||||
*/
|
||||
requestedLimit: number | null;
|
||||
|
||||
/**
|
||||
* True if the query execution result will be/was delivered asynchronously
|
||||
*/
|
||||
runAsync?: boolean;
|
||||
|
||||
/**
|
||||
* Start datetime for the query in a numerical timestamp
|
||||
*/
|
||||
startDttm: number;
|
||||
|
||||
/**
|
||||
* The tab instance associated with the request query
|
||||
*/
|
||||
tab: Tab;
|
||||
|
||||
/**
|
||||
* A key-value JSON associated with Jinja template variables
|
||||
*/
|
||||
templateParameters: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface QueryErrorResultContext extends QueryContext {
|
||||
/**
|
||||
* Finished datetime for the query in a numerical timestamp
|
||||
*/
|
||||
endDttm: number;
|
||||
|
||||
/**
|
||||
* Error message returned from DB engine
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
/**
|
||||
* Error details in a SupersetError structure
|
||||
*/
|
||||
errors: SupersetError[] | null;
|
||||
|
||||
/**
|
||||
* Executed SQL after parsing Jinja templates.
|
||||
*/
|
||||
executedSql: string | null;
|
||||
}
|
||||
|
||||
export interface QueryResultContext extends QueryContext {
|
||||
/**
|
||||
* Actual number of rows returned by the query.
|
||||
*/
|
||||
appliedLimit: number;
|
||||
|
||||
/**
|
||||
* Major factor that is determining the row limit of the query results.
|
||||
*/
|
||||
appliedLimitingFactor: string;
|
||||
|
||||
/**
|
||||
* Finished datetime for the query in a numerical timestamp.
|
||||
*/
|
||||
endDttm: number;
|
||||
|
||||
/**
|
||||
* Executed SQL after parsing Jinja templates.
|
||||
*/
|
||||
executedSql: string;
|
||||
|
||||
/**
|
||||
* Remote query id stored in backend.
|
||||
*/
|
||||
remoteId: number;
|
||||
|
||||
/**
|
||||
* Query result data and metadata.
|
||||
*/
|
||||
result: QueryResult;
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
/**
|
||||
* Column metadata associated with the query result.
|
||||
*/
|
||||
columns: Column[];
|
||||
|
||||
/**
|
||||
* Query result data.
|
||||
*/
|
||||
data: Record<string, any>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab-scoped Events and Functions
|
||||
*
|
||||
@@ -240,30 +360,30 @@ export declare const onDidChangeTabTitle: Event<string>;
|
||||
|
||||
/**
|
||||
* Event fired when a query starts running in the current tab.
|
||||
* Provides the editor state at the time of query execution.
|
||||
* Provides the query request state at the time of query execution.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* onDidQueryRun.event((editor) => {
|
||||
* console.log('Query started on database:', editor.databaseId);
|
||||
* console.log('Query content:', editor.content);
|
||||
* onDidQueryRun.event((query) => {
|
||||
* console.log('Query started on database:', query.tab.editor.databaseId);
|
||||
* console.log('Query content:', query.tab.editor.content);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidQueryRun: Event<Editor>;
|
||||
export declare const onDidQueryRun: Event<QueryContext>;
|
||||
|
||||
/**
|
||||
* Event fired when a running query is stopped in the current tab.
|
||||
* Provides the editor state when the query was stopped.
|
||||
* Provides the query request state when the query was stopped.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* onDidQueryStop.event((editor) => {
|
||||
* console.log('Query stopped for database:', editor.databaseId);
|
||||
* onDidQueryStop.event((query) => {
|
||||
* console.log('Query stopped for database:', query.tab.editor.databaseId);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidQueryStop: Event<Editor>;
|
||||
export declare const onDidQueryStop: Event<QueryContext>;
|
||||
|
||||
/**
|
||||
* Event fired when a query fails in the current tab.
|
||||
@@ -273,12 +393,12 @@ export declare const onDidQueryStop: Event<Editor>;
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* onDidQueryFail.event((error) => {
|
||||
* console.error('Query failed:', error);
|
||||
* onDidQueryFail.event((result) => {
|
||||
* console.error('Query failed:', result.errorMessage);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidQueryFail: Event<string>;
|
||||
export declare const onDidQueryFail: Event<QueryErrorResultContext>;
|
||||
|
||||
/**
|
||||
* Event fired when a query succeeds in the current tab.
|
||||
@@ -288,12 +408,13 @@ export declare const onDidQueryFail: Event<string>;
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* onDidQuerySuccess.event((result) => {
|
||||
* console.log('Query succeeded:', result);
|
||||
* onDidQuerySuccess.event((query) => {
|
||||
* console.log('Query succeeded:', query.result.data);
|
||||
* console.log('Query executed content:', query.executedSql);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export declare const onDidQuerySuccess: Event<string>;
|
||||
export declare const onDidQuerySuccess: Event<QueryResultContext>;
|
||||
|
||||
/**
|
||||
* Global Events and Functions
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"target": "es2020",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts*"],
|
||||
"exclude": ["lib"]
|
||||
|
||||
@@ -43,15 +43,17 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
|
||||
// remove or rename top level of column name(metric name) in the MultiIndex when
|
||||
// 1) at least 1 metric
|
||||
// 2) dimension exist or multiple time shift metrics exist
|
||||
// 3) xAxis exist
|
||||
// 4) truncate_metric in form_data and truncate_metric is true
|
||||
// 2) xAxis exist
|
||||
// 3a) isTimeComparisonValue
|
||||
// 3b-1) dimension exist or multiple time shift metrics exist
|
||||
// 3b-2) truncate_metric in form_data and truncate_metric is true
|
||||
if (
|
||||
metrics.length > 0 &&
|
||||
(columns.length > 0 || timeOffsets.length > 1) &&
|
||||
xAxisLabel &&
|
||||
truncate_metric !== undefined &&
|
||||
!!truncate_metric
|
||||
(isTimeComparisonValue ||
|
||||
((columns.length > 0 || timeOffsets.length > 1) &&
|
||||
truncate_metric !== undefined &&
|
||||
!!truncate_metric))
|
||||
) {
|
||||
const renamePairs: [string, string | null][] = [];
|
||||
if (
|
||||
|
||||
@@ -160,6 +160,44 @@ test('should add renameOperator if exists derived metrics', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if isTimeComparisonValue without columns', () => {
|
||||
[
|
||||
ComparisonType.Difference,
|
||||
ComparisonType.Ratio,
|
||||
ComparisonType.Percentage,
|
||||
].forEach(type => {
|
||||
expect(
|
||||
renameOperator(
|
||||
{
|
||||
...formData,
|
||||
...{
|
||||
comparison_type: type,
|
||||
time_compare: ['1 year ago'],
|
||||
},
|
||||
},
|
||||
{
|
||||
...queryObject,
|
||||
...{
|
||||
columns: [],
|
||||
metrics: ['sum(val)', 'avg(val2)'],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'rename',
|
||||
options: {
|
||||
columns: {
|
||||
[`${type}__avg(val2)__avg(val2)__1 year ago`]:
|
||||
'avg(val2), 1 year ago',
|
||||
[`${type}__sum(val)__sum(val)__1 year ago`]: 'sum(val), 1 year ago',
|
||||
},
|
||||
inplace: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should add renameOperator if x_axis does not exist', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
|
||||
@@ -26,6 +26,7 @@ interface LookupTable {
|
||||
|
||||
export interface ExampleImage {
|
||||
url: string;
|
||||
urlDark?: string;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
@@ -38,6 +39,7 @@ export interface ChartMetadataConfig {
|
||||
enableNoResults?: boolean;
|
||||
supportedAnnotationTypes?: string[];
|
||||
thumbnail: string;
|
||||
thumbnailDark?: string;
|
||||
useLegacyApi?: boolean;
|
||||
behaviors?: Behavior[];
|
||||
exampleGallery?: ExampleImage[];
|
||||
@@ -71,6 +73,8 @@ export default class ChartMetadata {
|
||||
|
||||
thumbnail: string;
|
||||
|
||||
thumbnailDark?: string;
|
||||
|
||||
useLegacyApi: boolean;
|
||||
|
||||
behaviors: Behavior[];
|
||||
@@ -107,6 +111,7 @@ export default class ChartMetadata {
|
||||
description = '',
|
||||
supportedAnnotationTypes = [],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi = false,
|
||||
behaviors = [],
|
||||
datasourceCount = 1,
|
||||
@@ -138,6 +143,7 @@ export default class ChartMetadata {
|
||||
);
|
||||
this.supportedAnnotationTypes = supportedAnnotationTypes;
|
||||
this.thumbnail = thumbnail;
|
||||
this.thumbnailDark = thumbnailDark;
|
||||
this.useLegacyApi = useLegacyApi;
|
||||
this.behaviors = behaviors;
|
||||
this.datasourceCount = datasourceCount;
|
||||
|
||||
@@ -393,10 +393,7 @@ export const FullSQLEditor = AsyncAceEditor(
|
||||
},
|
||||
);
|
||||
|
||||
export const MarkdownEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
'theme/textmate',
|
||||
]);
|
||||
export const MarkdownEditor = AsyncAceEditor(['mode/markdown', 'theme/github']);
|
||||
|
||||
export const TextAreaEditor = AsyncAceEditor([
|
||||
'mode/markdown',
|
||||
|
||||
@@ -60,7 +60,7 @@ const EmptyStateContainer = styled.div`
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
@@ -84,7 +84,7 @@ const EmptyStateContainer = styled.div`
|
||||
const Title = styled.p<{ size: EmptyStateSize }>`
|
||||
${({ theme, size }) => css`
|
||||
font-size: ${size === 'large' ? theme.fontSizeLG : theme.fontSize}px;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
margin-top: ${size === 'large' ? theme.sizeUnit * 4 : theme.sizeUnit * 2}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
`}
|
||||
@@ -93,7 +93,7 @@ const Title = styled.p<{ size: EmptyStateSize }>`
|
||||
const Description = styled.p<{ size: EmptyStateSize }>`
|
||||
${({ theme, size }) => css`
|
||||
font-size: ${size === 'large' ? theme.fontSize : theme.fontSizeSM}px;
|
||||
color: ${theme.colorTextQuaternary};
|
||||
color: ${theme.colorTextTertiary};
|
||||
margin-top: ${theme.sizeUnit * 2}px;
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -1,37 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Ellipsis } from './Ellipsis';
|
||||
|
||||
test('Ellipsis - click when the button is enabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Ellipsis onClick={click} />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Ellipsis - click when the button is disabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Ellipsis onClick={click} disabled />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { PaginationButtonProps } from './types';
|
||||
|
||||
export function Ellipsis({ disabled, onClick }: PaginationButtonProps) {
|
||||
return (
|
||||
<li className={classNames({ disabled })}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (!disabled) onClick(e);
|
||||
}}
|
||||
>
|
||||
…
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Item } from './Item';
|
||||
|
||||
test('Item - click when the item is not active', async () => {
|
||||
const click = jest.fn();
|
||||
render(
|
||||
<Item onClick={click}>
|
||||
<div data-test="test" />
|
||||
</Item>,
|
||||
);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Item - click when the item is active', async () => {
|
||||
const click = jest.fn();
|
||||
render(
|
||||
<Item onClick={click} active>
|
||||
<div data-test="test" />
|
||||
</Item>,
|
||||
);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PaginationButtonProps } from './types';
|
||||
|
||||
interface PaginationItemButton extends PaginationButtonProps {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Item({ active, children, onClick }: PaginationItemButton) {
|
||||
return (
|
||||
<li className={classNames({ active })}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (!active) onClick(e);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Next } from './Next';
|
||||
|
||||
test('Next - click when the button is enabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Next onClick={click} />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Next - click when the button is disabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Next onClick={click} disabled />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -1,37 +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 { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { Prev } from './Prev';
|
||||
|
||||
test('Prev - click when the button is enabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Prev onClick={click} />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Prev - click when the button is disabled', async () => {
|
||||
const click = jest.fn();
|
||||
render(<Prev onClick={click} disabled />);
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(click).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -1,75 +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 { render, screen, cleanup } from '@superset-ui/core/spec';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
// Add cleanup after each test
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
// Wait for any pending effects to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
jest.mock('./Next', () => ({
|
||||
Next: () => <div data-test="next" />,
|
||||
}));
|
||||
jest.mock('./Prev', () => ({
|
||||
Prev: () => <div data-test="prev" />,
|
||||
}));
|
||||
jest.mock('./Item', () => ({
|
||||
Item: () => <div data-test="item" />,
|
||||
}));
|
||||
jest.mock('./Ellipsis', () => ({
|
||||
Ellipsis: () => <div data-test="ellipsis" />,
|
||||
}));
|
||||
|
||||
test('Pagination rendering correctly', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<li data-test="test" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Next attribute', async () => {
|
||||
render(<Wrapper.Next onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Prev attribute', async () => {
|
||||
render(<Wrapper.Next onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('next')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Item attribute', async () => {
|
||||
render(
|
||||
<Wrapper.Item onClick={jest.fn()}>
|
||||
<></>
|
||||
</Wrapper.Item>,
|
||||
);
|
||||
expect(screen.getByTestId('item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Ellipsis attribute', async () => {
|
||||
render(<Wrapper.Ellipsis onClick={jest.fn()} />);
|
||||
expect(screen.getByTestId('ellipsis')).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,90 +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 { styled } from '@superset-ui/core';
|
||||
import { Next } from './Next';
|
||||
import { Prev } from './Prev';
|
||||
import { Item } from './Item';
|
||||
import { Ellipsis } from './Ellipsis';
|
||||
|
||||
interface PaginationProps {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
const PaginationList = styled.ul`
|
||||
${({ theme }) => `
|
||||
display: inline-block;
|
||||
padding: ${theme.sizeUnit * 3}px;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
margin: 0 4px;
|
||||
|
||||
> span {
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
background-color: ${theme.colorBgContainer};
|
||||
border: 1px solid ${theme.colorBorder};
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
color: ${theme.colorText};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 2;
|
||||
color: ${theme.colorText};
|
||||
background-color: ${theme.colorBgLayout};
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
span {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
span {
|
||||
z-index: 3;
|
||||
color: ${theme.colorBgLayout};
|
||||
cursor: default;
|
||||
background-color: ${theme.colorPrimary};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function Pagination({ children }: PaginationProps) {
|
||||
return <PaginationList role="navigation">{children}</PaginationList>;
|
||||
}
|
||||
|
||||
Pagination.Next = Next;
|
||||
Pagination.Prev = Prev;
|
||||
Pagination.Item = Item;
|
||||
Pagination.Ellipsis = Ellipsis;
|
||||
|
||||
export default Pagination;
|
||||
@@ -1,47 +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 Pagination from '@superset-ui/core/components/Pagination/Wrapper';
|
||||
import {
|
||||
createUltimatePagination,
|
||||
ITEM_TYPES,
|
||||
} from 'react-ultimate-pagination';
|
||||
|
||||
const ListViewPagination = createUltimatePagination({
|
||||
WrapperComponent: Pagination,
|
||||
itemTypeToComponent: {
|
||||
[ITEM_TYPES.PAGE]: ({ value, isActive, onClick }) => (
|
||||
<Pagination.Item active={isActive} onClick={onClick}>
|
||||
{value}
|
||||
</Pagination.Item>
|
||||
),
|
||||
[ITEM_TYPES.ELLIPSIS]: ({ isActive, onClick }) => (
|
||||
<Pagination.Ellipsis disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.PREVIOUS_PAGE_LINK]: ({ isActive, onClick }) => (
|
||||
<Pagination.Prev disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.NEXT_PAGE_LINK]: ({ isActive, onClick }) => (
|
||||
<Pagination.Next disabled={isActive} onClick={onClick} />
|
||||
),
|
||||
[ITEM_TYPES.FIRST_PAGE_LINK]: () => null,
|
||||
[ITEM_TYPES.LAST_PAGE_LINK]: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
export default ListViewPagination;
|
||||
@@ -100,3 +100,109 @@ test('Should the loading-indicator be visible during loading', () => {
|
||||
|
||||
expect(screen.getByTestId('loading-indicator')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Pagination controls should be rendered when pageSize is provided', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Pagination should call onPageChange when page is changed', async () => {
|
||||
const onPageChange = jest.fn();
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange,
|
||||
};
|
||||
const { rerender } = render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Simulate pagination change
|
||||
await screen.findByTitle('Next Page');
|
||||
|
||||
// Verify onPageChange would be called with correct arguments
|
||||
// The actual AntD pagination will handle the click internally
|
||||
expect(onPageChange).toBeDefined();
|
||||
|
||||
// Verify that re-rendering with new pageIndex works
|
||||
rerender(<TableCollection {...paginationProps} pageIndex={1} />);
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Pagination callback should be stable across re-renders', () => {
|
||||
const onPageChange = jest.fn();
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange,
|
||||
};
|
||||
|
||||
const { rerender } = render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<TableCollection {...paginationProps} />);
|
||||
|
||||
// onPageChange should not have been called during re-render
|
||||
expect(onPageChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should display correct page info when showRowCount is true', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
showRowCount: true,
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// AntD pagination shows page info
|
||||
expect(screen.getByText('1-2 of 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should not display page info when showRowCount is false', () => {
|
||||
const paginationProps = {
|
||||
...defaultProps,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
showRowCount: false,
|
||||
};
|
||||
render(<TableCollection {...paginationProps} />);
|
||||
|
||||
// Page info should not be shown
|
||||
expect(screen.queryByText('1-2 of 3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Bulk selection should work with pagination', () => {
|
||||
const toggleRowSelected = jest.fn();
|
||||
const toggleAllRowsSelected = jest.fn();
|
||||
const selectionProps = {
|
||||
...defaultProps,
|
||||
bulkSelectEnabled: true,
|
||||
selectedFlatRows: [],
|
||||
toggleRowSelected,
|
||||
toggleAllRowsSelected,
|
||||
pageSize: 2,
|
||||
totalCount: 3,
|
||||
pageIndex: 0,
|
||||
onPageChange: jest.fn(),
|
||||
};
|
||||
render(<TableCollection {...selectionProps} />);
|
||||
|
||||
// Check that selection checkboxes are rendered
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { HTMLAttributes, memo, useMemo } from 'react';
|
||||
import { HTMLAttributes, memo, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
ColumnInstance,
|
||||
HeaderGroup,
|
||||
@@ -47,15 +47,25 @@ interface TableCollectionProps<T extends object> {
|
||||
toggleAllRowsSelected?: (value?: boolean) => void;
|
||||
sticky?: boolean;
|
||||
size?: TableSize;
|
||||
pageIndex?: number;
|
||||
pageSize?: number;
|
||||
totalCount?: number;
|
||||
onPageChange?: (page: number, pageSize: number) => void;
|
||||
isPaginationSticky?: boolean;
|
||||
showRowCount?: boolean;
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
${({ theme }) => `
|
||||
const StyledTable = styled(Table)<{
|
||||
isPaginationSticky?: boolean;
|
||||
showRowCount?: boolean;
|
||||
}>`
|
||||
${({ theme, isPaginationSticky, showRowCount }) => `
|
||||
th.ant-column-cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
opacity: 0;
|
||||
font-size: ${theme.fontSizeXL}px;
|
||||
@@ -72,15 +82,18 @@ const StyledTable = styled(Table)`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-column-title {
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.ant-table-row:hover {
|
||||
.actions {
|
||||
opacity: 1;
|
||||
transition: opacity ease-in ${theme.motionDurationMid};
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
max-width: 320px;
|
||||
font-feature-settings: 'tnum' 1;
|
||||
@@ -91,10 +104,37 @@ const StyledTable = styled(Table)`
|
||||
padding-left: ${theme.sizeUnit * 4}px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ant-table-placeholder .ant-table-cell {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.ant-table-wrapper .ant-table-pagination.ant-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: ${showRowCount ? theme.sizeUnit * 4 : 0}px 0 ${showRowCount ? theme.sizeUnit * 14 : 0}px 0;
|
||||
position: relative;
|
||||
|
||||
.ant-pagination-total-text {
|
||||
color: ${theme.colorTextBase};
|
||||
margin-inline-end: 0;
|
||||
position: absolute;
|
||||
top: ${theme.sizeUnit * 12}px;
|
||||
}
|
||||
|
||||
${
|
||||
isPaginationSticky &&
|
||||
`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: ${theme.colorBgElevated};
|
||||
padding: ${theme.sizeUnit * 2}px 0;
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Hotfix - antd doesn't apply background color to overflowing cells
|
||||
& table {
|
||||
background-color: ${theme.colorBgContainer};
|
||||
@@ -116,13 +156,22 @@ function TableCollection<T extends object>({
|
||||
prepareRow,
|
||||
sticky,
|
||||
size = TableSize.Middle,
|
||||
pageIndex = 0,
|
||||
pageSize = 25,
|
||||
totalCount = 0,
|
||||
onPageChange,
|
||||
isPaginationSticky = false,
|
||||
showRowCount = true,
|
||||
}: TableCollectionProps<T>) {
|
||||
const mappedColumns = mapColumns<T>(
|
||||
columns,
|
||||
headerGroups,
|
||||
columnsForWrapText,
|
||||
const mappedColumns = useMemo(
|
||||
() => mapColumns<T>(columns, headerGroups, columnsForWrapText),
|
||||
[columns, headerGroups, columnsForWrapText],
|
||||
);
|
||||
|
||||
const mappedRows = useMemo(
|
||||
() => mapRows(rows, prepareRow),
|
||||
[rows, prepareRow],
|
||||
);
|
||||
const mappedRows = mapRows(rows, prepareRow);
|
||||
|
||||
const selectedRowKeys = useMemo(
|
||||
() => selectedFlatRows?.map(row => row.id) || [],
|
||||
@@ -147,6 +196,68 @@ function TableCollection<T extends object>({
|
||||
toggleRowSelected,
|
||||
toggleAllRowsSelected,
|
||||
]);
|
||||
|
||||
const handlePaginationChange = useCallback(
|
||||
(page: number, size: number) => {
|
||||
const validPage = Math.max(0, (page || 1) - 1);
|
||||
const validSize = size || pageSize;
|
||||
onPageChange?.(validPage, validSize);
|
||||
},
|
||||
[pageSize, onPageChange],
|
||||
);
|
||||
|
||||
const showTotalFunc = useCallback(
|
||||
(total: number, range: [number, number]) =>
|
||||
`${range[0]}-${range[1]} of ${total}`,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
(_pagination: any, _filters: any, sorter: SorterResult) => {
|
||||
if (sorter && sorter.field) {
|
||||
setSortBy?.([
|
||||
{
|
||||
id: sorter.field,
|
||||
desc: sorter.order === 'descend',
|
||||
},
|
||||
] as SortingRule<T>[]);
|
||||
}
|
||||
},
|
||||
[setSortBy],
|
||||
);
|
||||
|
||||
const paginationConfig = useMemo(() => {
|
||||
if (totalCount === 0) return false;
|
||||
|
||||
const config: any = {
|
||||
pageSize,
|
||||
size: 'default' as const,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
align: 'center' as const,
|
||||
showTotal: showRowCount ? showTotalFunc : undefined,
|
||||
};
|
||||
|
||||
if (onPageChange) {
|
||||
config.current = pageIndex + 1;
|
||||
config.total = totalCount;
|
||||
config.onChange = handlePaginationChange;
|
||||
} else {
|
||||
if (pageIndex > 0) config.defaultCurrent = pageIndex + 1;
|
||||
config.total = totalCount;
|
||||
}
|
||||
|
||||
return config;
|
||||
}, [
|
||||
pageSize,
|
||||
totalCount,
|
||||
showRowCount,
|
||||
showTotalFunc,
|
||||
pageIndex,
|
||||
handlePaginationChange,
|
||||
onPageChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledTable
|
||||
loading={loading}
|
||||
@@ -155,12 +266,15 @@ function TableCollection<T extends object>({
|
||||
data={mappedRows}
|
||||
size={size}
|
||||
data-test="listview-table"
|
||||
pagination={false}
|
||||
pagination={paginationConfig}
|
||||
scroll={{ x: 'max-content' }}
|
||||
tableLayout="auto"
|
||||
rowKey="rowId"
|
||||
rowSelection={rowSelection}
|
||||
locale={{ emptyText: null }}
|
||||
sortDirections={['ascend', 'descend', 'ascend']}
|
||||
isPaginationSticky={isPaginationSticky}
|
||||
showRowCount={showRowCount}
|
||||
components={{
|
||||
header: {
|
||||
cell: (props: HTMLAttributes<HTMLTableCellElement>) => (
|
||||
@@ -176,14 +290,7 @@ function TableCollection<T extends object>({
|
||||
),
|
||||
},
|
||||
}}
|
||||
onChange={(_pagination, _filters, sorter: SorterResult) => {
|
||||
setSortBy?.([
|
||||
{
|
||||
id: sorter.field,
|
||||
desc: sorter.order === 'descend',
|
||||
},
|
||||
] as SortingRule<T>[]);
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, userEvent } from '@superset-ui/core/spec';
|
||||
import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
|
||||
import { TableView, TableViewProps } from '.';
|
||||
|
||||
const mockedProps: TableViewProps = {
|
||||
@@ -30,6 +30,7 @@ const mockedProps: TableViewProps = {
|
||||
{
|
||||
accessor: 'age',
|
||||
Header: 'Age',
|
||||
sortable: true,
|
||||
id: 'age',
|
||||
},
|
||||
{
|
||||
@@ -78,10 +79,10 @@ test('should render the cells', () => {
|
||||
|
||||
test('should render the pagination', () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(4);
|
||||
expect(screen.getByText('«')).toBeInTheDocument();
|
||||
expect(screen.getByText('»')).toBeInTheDocument();
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button')).toHaveLength(2);
|
||||
expect(screen.getByTitle('Previous Page')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Next Page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the row count by default', () => {
|
||||
@@ -104,45 +105,63 @@ test('should NOT render the pagination when disabled', () => {
|
||||
withPagination: false,
|
||||
};
|
||||
render(<TableView {...withoutPaginationProps} />);
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render the pagination when fewer rows than page size', () => {
|
||||
test('should render the pagination even when fewer rows than page size', () => {
|
||||
const withoutPaginationProps = {
|
||||
...mockedProps,
|
||||
pageSize: 3,
|
||||
};
|
||||
render(<TableView {...withoutPaginationProps} />);
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should change page when « and » buttons are clicked', async () => {
|
||||
test('should change page when pagination is clicked', async () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
const nextBtn = screen.getByText('»');
|
||||
const prevBtn = screen.getByText('«');
|
||||
|
||||
await userEvent.click(nextBtn);
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(prevBtn);
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kate')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Emily')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const page1 = screen.getByRole('listitem', { name: '1' });
|
||||
await userEvent.click(page1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('cell')).toHaveLength(3);
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('27')).toBeInTheDocument();
|
||||
expect(screen.getByText('Emily')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by age', async () => {
|
||||
render(<TableView {...mockedProps} />);
|
||||
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort by initialSortBy DESC', () => {
|
||||
@@ -208,3 +227,146 @@ test('should render the right wrap content text by columnsForWrapText', () => {
|
||||
'ant-table-cell-ellipsis',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle server-side pagination', async () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
totalCount: 10,
|
||||
pageSize: 2,
|
||||
};
|
||||
render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onServerPagination).toHaveBeenCalledWith({
|
||||
pageIndex: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle server-side sorting', async () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
};
|
||||
render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Click on sortable column
|
||||
await userEvent.click(screen.getAllByTestId('sort-header')[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onServerPagination).toHaveBeenCalledWith({
|
||||
pageIndex: 0,
|
||||
sortBy: [{ id: 'id', desc: false }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('pagination callbacks should be stable across re-renders', () => {
|
||||
const onServerPagination = jest.fn();
|
||||
const serverPaginationProps = {
|
||||
...mockedProps,
|
||||
serverPagination: true,
|
||||
onServerPagination,
|
||||
totalCount: 10,
|
||||
pageSize: 2,
|
||||
};
|
||||
|
||||
const { rerender } = render(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<TableView {...serverPaginationProps} />);
|
||||
|
||||
// onServerPagination should not have been called during re-render
|
||||
expect(onServerPagination).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should scroll to top when scrollTopOnPagination is true', async () => {
|
||||
const scrollToSpy = jest
|
||||
.spyOn(window, 'scrollTo')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const scrollProps = {
|
||||
...mockedProps,
|
||||
scrollTopOnPagination: true,
|
||||
pageSize: 1,
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should NOT scroll to top when scrollTopOnPagination is false', async () => {
|
||||
const scrollToSpy = jest
|
||||
.spyOn(window, 'scrollTo')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const scrollProps = {
|
||||
...mockedProps,
|
||||
scrollTopOnPagination: false,
|
||||
pageSize: 1,
|
||||
};
|
||||
render(<TableView {...scrollProps} />);
|
||||
|
||||
// Click next page
|
||||
const page2 = screen.getByRole('listitem', { name: '2' });
|
||||
await userEvent.click(page2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('321')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
||||
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should handle totalCount of 0 correctly', () => {
|
||||
const emptyProps = {
|
||||
...mockedProps,
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
render(<TableView {...emptyProps} />);
|
||||
|
||||
// Pagination should not be shown when totalCount is 0
|
||||
expect(screen.queryByRole('list')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should handle large datasets with pagination', () => {
|
||||
const largeDataset = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: i,
|
||||
age: 20 + i,
|
||||
name: `Person ${i}`,
|
||||
}));
|
||||
|
||||
const largeDataProps = {
|
||||
...mockedProps,
|
||||
data: largeDataset,
|
||||
pageSize: 10,
|
||||
};
|
||||
render(<TableView {...largeDataProps} />);
|
||||
|
||||
// Should show only first page (10 items)
|
||||
expect(screen.getAllByTestId('table-row')).toHaveLength(10);
|
||||
|
||||
// Should show pagination with correct page count
|
||||
expect(screen.getByRole('list')).toBeInTheDocument();
|
||||
expect(screen.getByText('1-10 of 100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { Empty } from '@superset-ui/core/components';
|
||||
import Pagination from '@superset-ui/core/components/Pagination';
|
||||
import TableCollection from '@superset-ui/core/components/TableCollection';
|
||||
import { TableSize } from '@superset-ui/core/components/Table';
|
||||
import { SortByType, ServerPagination } from './types';
|
||||
|
||||
const NOOP_SERVER_PAGINATION = () => {};
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
export enum EmptyWrapperType {
|
||||
@@ -96,29 +97,6 @@ const TableViewStyles = styled.div<{
|
||||
}
|
||||
`;
|
||||
|
||||
const PaginationStyles = styled.div<{
|
||||
isPaginationSticky?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.colorBgElevated};
|
||||
|
||||
${({ isPaginationSticky }) =>
|
||||
isPaginationSticky &&
|
||||
`
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`};
|
||||
|
||||
.row-count-container {
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
color: ${({ theme }) => theme.colorText};
|
||||
}
|
||||
`;
|
||||
|
||||
const RawTableView = ({
|
||||
columns,
|
||||
data,
|
||||
@@ -133,16 +111,21 @@ const RawTableView = ({
|
||||
showRowCount = true,
|
||||
serverPagination = false,
|
||||
columnsForWrapText,
|
||||
onServerPagination = () => {},
|
||||
scrollTopOnPagination = false,
|
||||
onServerPagination = NOOP_SERVER_PAGINATION,
|
||||
scrollTopOnPagination = true,
|
||||
size = TableSize.Middle,
|
||||
...props
|
||||
}: TableViewProps) => {
|
||||
const initialState = {
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageIndex: initialPageIndex ?? 0,
|
||||
sortBy: initialSortBy,
|
||||
};
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
pageIndex: initialPageIndex ?? 0,
|
||||
sortBy: initialSortBy,
|
||||
}),
|
||||
[initialPageSize, initialPageIndex, initialSortBy],
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
@@ -151,10 +134,9 @@ const RawTableView = ({
|
||||
page,
|
||||
rows,
|
||||
prepareRow,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
setSortBy,
|
||||
state: { pageIndex, pageSize, sortBy },
|
||||
state: { pageIndex, sortBy },
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
@@ -162,36 +144,94 @@ const RawTableView = ({
|
||||
initialState,
|
||||
manualPagination: serverPagination,
|
||||
manualSortBy: serverPagination,
|
||||
pageCount: Math.ceil(totalCount / initialState.pageSize),
|
||||
pageCount: serverPagination
|
||||
? Math.ceil(totalCount / initialState.pageSize)
|
||||
: undefined,
|
||||
autoResetSortBy: false,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
...(withPagination ? [usePagination] : []),
|
||||
);
|
||||
|
||||
const content = withPagination ? page : rows;
|
||||
|
||||
let EmptyWrapperComponent;
|
||||
switch (emptyWrapperType) {
|
||||
case EmptyWrapperType.Small:
|
||||
EmptyWrapperComponent = ({ children }: any) => <>{children}</>;
|
||||
break;
|
||||
case EmptyWrapperType.Default:
|
||||
default:
|
||||
EmptyWrapperComponent = ({ children }: any) => (
|
||||
<EmptyWrapper>{children}</EmptyWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmpty = !loading && content.length === 0;
|
||||
const hasPagination = pageCount > 1 && withPagination;
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const handleGotoPage = (p: number) => {
|
||||
if (scrollTopOnPagination) {
|
||||
tableRef?.current?.scroll(0, 0);
|
||||
const EmptyWrapperComponent = useMemo(() => {
|
||||
switch (emptyWrapperType) {
|
||||
case EmptyWrapperType.Small:
|
||||
return ({ children }: any) => <>{children}</>;
|
||||
case EmptyWrapperType.Default:
|
||||
default:
|
||||
return ({ children }: any) => <EmptyWrapper>{children}</EmptyWrapper>;
|
||||
}
|
||||
gotoPage(p);
|
||||
};
|
||||
}, [emptyWrapperType]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (withPagination ? page : rows),
|
||||
[withPagination, page, rows],
|
||||
);
|
||||
|
||||
const isEmpty = useMemo(
|
||||
() => !loading && content.length === 0,
|
||||
[loading, content.length],
|
||||
);
|
||||
|
||||
const handleScrollToTop = useCallback(() => {
|
||||
if (scrollTopOnPagination) {
|
||||
if (tableRef?.current) {
|
||||
if (typeof tableRef.current.scrollTo === 'function') {
|
||||
tableRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} else if (typeof tableRef.current.scroll === 'function') {
|
||||
tableRef.current.scroll(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.scrollTo)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
}, [scrollTopOnPagination]);
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(p: number) => {
|
||||
if (scrollTopOnPagination) handleScrollToTop();
|
||||
|
||||
gotoPage(p);
|
||||
},
|
||||
[scrollTopOnPagination, handleScrollToTop, gotoPage],
|
||||
);
|
||||
|
||||
const paginationProps = useMemo(() => {
|
||||
if (!withPagination) {
|
||||
return {
|
||||
pageIndex: 0,
|
||||
pageSize: data.length,
|
||||
totalCount: 0,
|
||||
onPageChange: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (serverPagination) {
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
totalCount,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
pageIndex,
|
||||
pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
|
||||
totalCount: data.length,
|
||||
onPageChange: handlePageChange,
|
||||
};
|
||||
}, [
|
||||
withPagination,
|
||||
serverPagination,
|
||||
pageIndex,
|
||||
initialPageSize,
|
||||
totalCount,
|
||||
data.length,
|
||||
handlePageChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && pageIndex !== initialState.pageIndex) {
|
||||
@@ -199,7 +239,7 @@ const RawTableView = ({
|
||||
pageIndex,
|
||||
});
|
||||
}
|
||||
}, [pageIndex]);
|
||||
}, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
|
||||
@@ -208,61 +248,38 @@ const RawTableView = ({
|
||||
sortBy,
|
||||
});
|
||||
}
|
||||
}, [sortBy]);
|
||||
}, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableViewStyles {...props} ref={tableRef}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
setSortBy={setSortBy}
|
||||
size={size}
|
||||
columnsForWrapText={columnsForWrapText}
|
||||
/>
|
||||
{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}
|
||||
onChange={(p: number) => handleGotoPage(p - 1)}
|
||||
hideFirstAndLastPageLinks
|
||||
/>
|
||||
{showRowCount && (
|
||||
<div className="row-count-container">
|
||||
{!loading &&
|
||||
t(
|
||||
'%s-%s of %s',
|
||||
pageSize * pageIndex + (page.length && 1),
|
||||
pageSize * pageIndex + page.length,
|
||||
totalCount,
|
||||
)}
|
||||
</div>
|
||||
<TableViewStyles {...props} ref={tableRef}>
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={content}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
setSortBy={setSortBy}
|
||||
size={size}
|
||||
columnsForWrapText={columnsForWrapText}
|
||||
isPaginationSticky={props.isPaginationSticky}
|
||||
showRowCount={showRowCount}
|
||||
{...paginationProps}
|
||||
/>
|
||||
{isEmpty && (
|
||||
<EmptyWrapperComponent>
|
||||
{noDataText ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={noDataText}
|
||||
/>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</PaginationStyles>
|
||||
</EmptyWrapperComponent>
|
||||
)}
|
||||
</>
|
||||
</TableViewStyles>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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 { fireEvent, render } from '@superset-ui/core/spec';
|
||||
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
|
||||
|
||||
describe('Tabs', () => {
|
||||
const defaultItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Tab 1',
|
||||
children: <div data-testid="tab1-content">Tab 1 content</div>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Tab 2',
|
||||
children: <div data-testid="tab2-content">Tab 2 content</div>,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Tab 3',
|
||||
children: <div data-testid="tab3-content">Tab 3 content</div>,
|
||||
},
|
||||
];
|
||||
|
||||
describe('Basic Tabs', () => {
|
||||
it('should render tabs with default props', () => {
|
||||
const { getByText, container } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
expect(getByText('Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Tab 2')).toBeInTheDocument();
|
||||
expect(getByText('Tab 3')).toBeInTheDocument();
|
||||
|
||||
const activeTabContent = container.querySelector(
|
||||
'.ant-tabs-tabpane-active',
|
||||
);
|
||||
|
||||
expect(activeTabContent).toBeDefined();
|
||||
expect(
|
||||
activeTabContent?.querySelector('[data-testid="tab1-content"]'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render tabs component structure', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav');
|
||||
const tabsContent = container.querySelector('.ant-tabs-content-holder');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
expect(tabsNav).toBeDefined();
|
||||
expect(tabsContent).toBeDefined();
|
||||
});
|
||||
|
||||
it('should apply default tabBarStyle with padding', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
|
||||
|
||||
// Check that tabBarStyle is applied (default padding is added)
|
||||
expect(tabsNav?.style?.paddingLeft).toBeDefined();
|
||||
});
|
||||
|
||||
it('should merge custom tabBarStyle with defaults', () => {
|
||||
const customStyle = { paddingRight: '20px', backgroundColor: 'red' };
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} tabBarStyle={customStyle} />,
|
||||
);
|
||||
const tabsNav = container.querySelector('.ant-tabs-nav') as HTMLElement;
|
||||
|
||||
expect(tabsNav?.style?.paddingLeft).toBeDefined();
|
||||
expect(tabsNav?.style?.paddingRight).toBe('20px');
|
||||
expect(tabsNav?.style?.backgroundColor).toBe('red');
|
||||
});
|
||||
|
||||
it('should handle allowOverflow prop', () => {
|
||||
const { container: allowContainer } = render(
|
||||
<Tabs items={defaultItems} allowOverflow />,
|
||||
);
|
||||
const { container: disallowContainer } = render(
|
||||
<Tabs items={defaultItems} allowOverflow={false} />,
|
||||
);
|
||||
|
||||
expect(allowContainer.querySelector('.ant-tabs')).toBeDefined();
|
||||
expect(disallowContainer.querySelector('.ant-tabs')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should disable animation by default', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).not.toContain('ant-tabs-animated');
|
||||
});
|
||||
|
||||
it('should handle tab change events', () => {
|
||||
const onChangeMock = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Tabs items={defaultItems} onChange={onChangeMock} />,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Tab 2'));
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith('2');
|
||||
});
|
||||
|
||||
it('should pass through additional props to Antd Tabs', () => {
|
||||
const onTabClickMock = jest.fn();
|
||||
const { getByText } = render(
|
||||
<Tabs
|
||||
items={defaultItems}
|
||||
onTabClick={onTabClickMock}
|
||||
size="large"
|
||||
centered
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(getByText('Tab 2'));
|
||||
|
||||
expect(onTabClickMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditableTabs', () => {
|
||||
it('should render with editable features', () => {
|
||||
const { container } = render(<EditableTabs items={defaultItems} />);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('ant-tabs-card');
|
||||
expect(tabsElement?.className).toContain('ant-tabs-editable-card');
|
||||
});
|
||||
|
||||
it('should handle onEdit callback for add/remove actions', () => {
|
||||
const onEditMock = jest.fn();
|
||||
const itemsWithRemove = defaultItems.map(item => ({
|
||||
...item,
|
||||
closable: true,
|
||||
}));
|
||||
|
||||
const { container } = render(
|
||||
<EditableTabs items={itemsWithRemove} onEdit={onEditMock} />,
|
||||
);
|
||||
|
||||
const removeButton = container.querySelector('.ant-tabs-tab-remove');
|
||||
expect(removeButton).toBeDefined();
|
||||
|
||||
fireEvent.click(removeButton!);
|
||||
expect(onEditMock).toHaveBeenCalledWith(expect.any(String), 'remove');
|
||||
});
|
||||
|
||||
it('should have default props set correctly', () => {
|
||||
expect(EditableTabs.defaultProps?.type).toBe('editable-card');
|
||||
expect(EditableTabs.defaultProps?.animated).toEqual({
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineEditableTabs', () => {
|
||||
it('should render as line-style editable tabs', () => {
|
||||
const { container } = render(<LineEditableTabs items={defaultItems} />);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('ant-tabs-card');
|
||||
expect(tabsElement?.className).toContain('ant-tabs-editable-card');
|
||||
});
|
||||
|
||||
it('should render with line-specific styling', () => {
|
||||
const { container } = render(<LineEditableTabs items={defaultItems} />);
|
||||
|
||||
const inkBar = container.querySelector('.ant-tabs-ink-bar');
|
||||
expect(inkBar).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TabPane Legacy Support', () => {
|
||||
it('should support TabPane component access', () => {
|
||||
expect(Tabs.TabPane).toBeDefined();
|
||||
expect(EditableTabs.TabPane).toBeDefined();
|
||||
expect(LineEditableTabs.TabPane).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render using legacy TabPane syntax', () => {
|
||||
const { getByText, container } = render(
|
||||
<Tabs>
|
||||
<Tabs.TabPane tab="Legacy Tab 1" key="1">
|
||||
<div data-testid="legacy-content-1">Legacy content 1</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Legacy Tab 2" key="2">
|
||||
<div data-testid="legacy-content-2">Legacy content 2</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>,
|
||||
);
|
||||
|
||||
expect(getByText('Legacy Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Legacy Tab 2')).toBeInTheDocument();
|
||||
|
||||
const activeTabContent = container.querySelector(
|
||||
'.ant-tabs-tabpane-active [data-testid="legacy-content-1"]',
|
||||
);
|
||||
|
||||
expect(activeTabContent).toBeDefined();
|
||||
expect(activeTabContent?.textContent).toBe('Legacy content 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty items array', () => {
|
||||
const { container } = render(<Tabs items={[]} />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle undefined items', () => {
|
||||
const { container } = render(<Tabs />);
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle tabs with no content', () => {
|
||||
const itemsWithoutContent = [
|
||||
{ key: '1', label: 'Tab 1' },
|
||||
{ key: '2', label: 'Tab 2' },
|
||||
];
|
||||
|
||||
const { getByText } = render(<Tabs items={itemsWithoutContent} />);
|
||||
|
||||
expect(getByText('Tab 1')).toBeInTheDocument();
|
||||
expect(getByText('Tab 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle allowOverflow default value', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
expect(container.querySelector('.ant-tabs')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should render with proper ARIA roles', () => {
|
||||
const { container } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
const tablist = container.querySelector('[role="tablist"]');
|
||||
const tabs = container.querySelectorAll('[role="tab"]');
|
||||
|
||||
expect(tablist).toBeDefined();
|
||||
expect(tabs.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should support keyboard navigation', () => {
|
||||
const { container, getByText } = render(<Tabs items={defaultItems} />);
|
||||
|
||||
const firstTab = container.querySelector('[role="tab"]');
|
||||
const secondTab = getByText('Tab 2');
|
||||
|
||||
if (firstTab) {
|
||||
fireEvent.keyDown(firstTab, { key: 'ArrowRight', code: 'ArrowRight' });
|
||||
}
|
||||
|
||||
fireEvent.click(secondTab);
|
||||
|
||||
expect(secondTab).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling Integration', () => {
|
||||
it('should accept and apply custom CSS classes', () => {
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} className="custom-tabs-class" />,
|
||||
);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs');
|
||||
|
||||
expect(tabsElement?.className).toContain('custom-tabs-class');
|
||||
});
|
||||
|
||||
it('should accept and apply custom styles', () => {
|
||||
const customStyle = { minHeight: '200px' };
|
||||
const { container } = render(
|
||||
<Tabs items={defaultItems} style={customStyle} />,
|
||||
);
|
||||
|
||||
const tabsElement = container.querySelector('.ant-tabs') as HTMLElement;
|
||||
|
||||
expect(tabsElement?.style?.minHeight).toBe('200px');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,14 +29,18 @@ export interface TabsProps extends AntdTabsProps {
|
||||
const StyledTabs = ({
|
||||
animated = false,
|
||||
allowOverflow = true,
|
||||
tabBarStyle,
|
||||
...props
|
||||
}: TabsProps) => {
|
||||
const theme = useTheme();
|
||||
const defaultTabBarStyle = { paddingLeft: theme.sizeUnit * 4 };
|
||||
const mergedStyle = { ...defaultTabBarStyle, ...tabBarStyle };
|
||||
|
||||
return (
|
||||
<AntdTabs
|
||||
animated={animated}
|
||||
{...props}
|
||||
tabBarStyle={{ paddingLeft: theme.sizeUnit * 4 }}
|
||||
tabBarStyle={mergedStyle}
|
||||
css={theme => css`
|
||||
overflow: ${allowOverflow ? 'visible' : 'hidden'};
|
||||
|
||||
|
||||
@@ -60,10 +60,7 @@ export function useTheme() {
|
||||
|
||||
const styled: CreateStyled = emotionStyled;
|
||||
|
||||
// launching in in dark mode for now while iterating
|
||||
const themeObject: Theme = Theme.fromConfig({
|
||||
algorithm: ThemeAlgorithm.DEFAULT,
|
||||
});
|
||||
const themeObject: Theme = Theme.fromConfig();
|
||||
|
||||
const { theme } = themeObject;
|
||||
const supersetTheme = theme;
|
||||
|
||||
@@ -127,6 +127,15 @@ export interface SupersetSpecificTokens {
|
||||
// Spinner-related
|
||||
brandSpinnerUrl?: string;
|
||||
brandSpinnerSvg?: string;
|
||||
|
||||
// ECharts-related
|
||||
/** Global ECharts configuration overrides applied to all chart types */
|
||||
echartsOptionsOverrides?: any;
|
||||
|
||||
/** Chart-specific ECharts configuration overrides keyed by viz_type */
|
||||
echartsOptionsOverridesByChartType?: {
|
||||
[chartType: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ export enum FeatureFlag {
|
||||
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
|
||||
DashboardRbac = 'DASHBOARD_RBAC',
|
||||
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
||||
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
|
||||
/** @deprecated */
|
||||
DrillToDetail = 'DRILL_TO_DETAIL',
|
||||
DrillBy = 'DRILL_BY',
|
||||
|
||||
@@ -33,3 +33,4 @@ export * from './random';
|
||||
export * from './typedMemo';
|
||||
export * from './html';
|
||||
export * from './tooltip';
|
||||
export * from './merge';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 { mergeReplaceArrays } from './merge';
|
||||
|
||||
describe('lodash utilities', () => {
|
||||
describe('mergeReplaceArrays', () => {
|
||||
it('should merge objects and replace arrays', () => {
|
||||
const obj1 = { a: [1, 2], b: { c: 3 } };
|
||||
const obj2 = { a: [4, 5], b: { d: 6 } };
|
||||
|
||||
const result = mergeReplaceArrays(obj1, obj2);
|
||||
|
||||
expect(result).toEqual({
|
||||
a: [4, 5], // array replaced
|
||||
b: { c: 3, d: 6 }, // objects merged
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle precedence with multiple sources', () => {
|
||||
const base = { x: { y: 1 }, z: [1] };
|
||||
const override1 = { x: { y: 2 }, z: [2, 3] };
|
||||
const override2 = { x: { y: 3 }, z: [4] };
|
||||
|
||||
const result = mergeReplaceArrays(base, override1, override2);
|
||||
|
||||
expect(result).toEqual({
|
||||
x: { y: 3 }, // last wins
|
||||
z: [4], // array replaced by last
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty and null values', () => {
|
||||
const base = { a: [1], b: { x: 1 } };
|
||||
const override = { a: [], b: { x: null } };
|
||||
|
||||
const result = mergeReplaceArrays(base, override);
|
||||
|
||||
expect(result).toEqual({
|
||||
a: [], // empty array replaces
|
||||
b: { x: null }, // null overrides
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 { mergeWith } from 'lodash';
|
||||
|
||||
/**
|
||||
* Merges objects using lodash.mergeWith, but replaces arrays instead of concatenating them.
|
||||
* This is useful for configuration objects where you want to completely override array values
|
||||
* rather than merge them by index.
|
||||
*
|
||||
* @example
|
||||
* const obj1 = { a: [1, 2], b: { c: 3 } };
|
||||
* const obj2 = { a: [4, 5], b: { d: 6 } };
|
||||
* mergeReplaceArrays(obj1, obj2);
|
||||
* // Result: { a: [4, 5], b: { c: 3, d: 6 } }
|
||||
*
|
||||
* @example
|
||||
* // ECharts configuration merging
|
||||
* const baseConfig = { series: [{ type: 'line' }], grid: { left: '10%' } };
|
||||
* const overrides = { series: [{ type: 'bar' }], grid: { right: '10%' } };
|
||||
* mergeReplaceArrays(baseConfig, overrides);
|
||||
* // Result: { series: [{ type: 'bar' }], grid: { left: '10%', right: '10%' } }
|
||||
*
|
||||
* @param sources - Objects to merge (rightmost wins for arrays, deep merge for objects)
|
||||
* @returns Merged object with arrays replaced, not concatenated
|
||||
*/
|
||||
export function mergeReplaceArrays<T = any>(...sources: any[]): T {
|
||||
const replaceArrays = (objValue: any, srcValue: any) => {
|
||||
if (Array.isArray(srcValue)) {
|
||||
return srcValue; // Replace arrays entirely
|
||||
}
|
||||
return undefined; // Let lodash handle object merging for non-arrays
|
||||
};
|
||||
|
||||
return mergeWith({}, ...sources, replaceArrays);
|
||||
}
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -19,8 +19,10 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Correlation'),
|
||||
@@ -28,7 +30,7 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
"Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.",
|
||||
),
|
||||
exampleGallery: [{ url: example }],
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Calendar Heatmap'),
|
||||
tags: [
|
||||
t('Business'),
|
||||
@@ -39,6 +41,7 @@ const metadata = new ChartMetadata({
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -19,7 +19,9 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/chord.jpg';
|
||||
import exampleDark from './images/chord-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -29,11 +31,16 @@ const metadata = new ChartMetadata({
|
||||
'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.',
|
||||
),
|
||||
exampleGallery: [
|
||||
{ url: example, caption: t('Relationships between community channels') },
|
||||
{
|
||||
url: example,
|
||||
urlDark: exampleDark,
|
||||
caption: t('Relationships between community channels'),
|
||||
},
|
||||
],
|
||||
name: t('Chord Diagram'),
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -58,11 +58,13 @@ export default styled(CountryMap)`
|
||||
}
|
||||
|
||||
.superset-legacy-chart-country-map text.result-text {
|
||||
fill: ${theme.colorText};
|
||||
font-weight: ${theme.fontWeightLight};
|
||||
font-size: ${theme.fontSizeXL}px;
|
||||
}
|
||||
|
||||
.superset-legacy-chart-country-map text.big-text {
|
||||
fill: ${theme.colorText};
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
font-size: ${theme.fontSizeLG}px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -19,8 +19,11 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -29,7 +32,10 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
"Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.",
|
||||
),
|
||||
exampleGallery: [{ url: exampleUsa }, { url: exampleGermany }],
|
||||
exampleGallery: [
|
||||
{ url: exampleUsa, urlDark: exampleUsaDark },
|
||||
{ url: exampleGermany, urlDark: exampleGermanyDark },
|
||||
],
|
||||
name: t('Country Map'),
|
||||
tags: [
|
||||
t('2D'),
|
||||
@@ -40,6 +46,7 @@ const metadata = new ChartMetadata({
|
||||
t('Stacked'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -19,7 +19,9 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/Horizon_Chart.jpg';
|
||||
import exampleDark from './images/Horizon_Chart-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -28,10 +30,11 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.',
|
||||
),
|
||||
exampleGallery: [{ url: example }],
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Horizon Chart'),
|
||||
tags: [t('Legacy')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 125 KiB |
@@ -18,8 +18,11 @@
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/MapBox.jpg';
|
||||
import example1Dark from './images/MapBox-dark.jpg';
|
||||
import example2 from './images/MapBox2.jpg';
|
||||
import example2Dark from './images/MapBox2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -27,8 +30,8 @@ const metadata = new ChartMetadata({
|
||||
credits: ['https://www.mapbox.com/mapbox-gl-js/api/'],
|
||||
description: '',
|
||||
exampleGallery: [
|
||||
{ url: example1, description: t('Light mode') },
|
||||
{ url: example2, description: t('Dark mode') },
|
||||
{ url: example1, urlDark: example1Dark, description: t('Light mode') },
|
||||
{ url: example2, urlDark: example2Dark, description: t('Dark mode') },
|
||||
],
|
||||
name: t('MapBox'),
|
||||
tags: [
|
||||
@@ -40,6 +43,7 @@ const metadata = new ChartMetadata({
|
||||
t('Transformable'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -18,7 +18,10 @@
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -26,9 +29,11 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Paired t-test Table'),
|
||||
tags: [t('Legacy'), t('Statistical'), t('Tabular')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 123 KiB |
@@ -19,8 +19,11 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/example1.jpg';
|
||||
import example1Dark from './images/example1-dark.jpg';
|
||||
import example2 from './images/example2.jpg';
|
||||
import example2Dark from './images/example2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -29,10 +32,14 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.',
|
||||
),
|
||||
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
name: t('Parallel Coordinates'),
|
||||
tags: [t('Directional'), t('Legacy'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -19,16 +19,19 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Part of a Whole'),
|
||||
description: t('Compare the same summarized metric across multiple groups.'),
|
||||
exampleGallery: [{ url: example }],
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Partition Chart'),
|
||||
tags: [t('Categorical'), t('Comparison'), t('Legacy'), t('Proportional')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -19,8 +19,11 @@
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/example1.jpg';
|
||||
import example1Dark from './images/example1-dark.jpg';
|
||||
import example2 from './images/example2.jpg';
|
||||
import example2Dark from './images/example2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -28,7 +31,10 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
'A polar coordinate chart where the circle is broken into wedges of equal angle, and the value represented by any wedge is illustrated by its area, rather than its radius or sweep angle.',
|
||||
),
|
||||
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
name: t('Nightingale Rose Chart'),
|
||||
tags: [
|
||||
t('Legacy'),
|
||||
@@ -40,6 +46,7 @@ const metadata = new ChartMetadata({
|
||||
t('Trend'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -19,8 +19,11 @@
|
||||
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/WorldMap1.jpg';
|
||||
import example1Dark from './images/WorldMap1-dark.jpg';
|
||||
import example2 from './images/WorldMap2.jpg';
|
||||
import example2Dark from './images/WorldMap2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
@@ -29,7 +32,10 @@ const metadata = new ChartMetadata({
|
||||
description: t(
|
||||
'A map of the world, that can indicate values in different countries.',
|
||||
),
|
||||
exampleGallery: [{ url: example1 }, { url: example2 }],
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
name: t('World Map'),
|
||||
tags: [
|
||||
t('2D'),
|
||||
@@ -43,6 +49,7 @@ const metadata = new ChartMetadata({
|
||||
t('Featured'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
behaviors: [
|
||||
Behavior.InteractiveChart,
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 256 KiB |
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example from './images/example.png';
|
||||
import exampleDark from './images/example-dark.png';
|
||||
import transformProps from '../transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
@@ -26,9 +28,10 @@ const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
description: t('Compose multiple layers together to form complex visuals.'),
|
||||
exampleGallery: [{ url: example }],
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('deck.gl Multiple Layers'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
tags: [t('deckGL'), t('Multi-Layers')],
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |